openmates 0.14.7 → 0.14.8-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -27,6 +27,12 @@ var CIPHERTEXT_HEADER_LENGTH = 2 + FINGERPRINT_LENGTH;
27
27
  var API_KEY_PREFIX = "sk-api-";
28
28
  var API_KEY_RANDOM_LENGTH = 32;
29
29
  var API_KEY_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
30
+ var CHAT_RECOVERY_PROTOCOL_VERSION = 1;
31
+ var CHAT_RECOVERY_MAX_PAYLOAD_BYTES = 16 * 1024 * 1024;
32
+ var CHAT_RECOVERY_KEY_BYTES = 32;
33
+ var CHAT_RECOVERY_NONCE_BYTES = 12;
34
+ var CHAT_RECOVERY_AAD_PREFIX = new TextEncoder().encode("OMCR1");
35
+ var CHAT_RECOVERY_UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
30
36
  function base64ToBytes(input) {
31
37
  return new Uint8Array(Buffer.from(input, "base64"));
32
38
  }
@@ -38,6 +44,144 @@ function toArrayBuffer(input) {
38
44
  new Uint8Array(output).set(input);
39
45
  return output;
40
46
  }
47
+ function bytesToBase64Url(input) {
48
+ return Buffer.from(input).toString("base64url");
49
+ }
50
+ function base64UrlToBytes(input, field, expectedLength) {
51
+ if (!input || input.includes("=")) {
52
+ throw new Error(`${field} must be non-empty unpadded base64url`);
53
+ }
54
+ const decoded = new Uint8Array(Buffer.from(input, "base64url"));
55
+ if (bytesToBase64Url(decoded) !== input) {
56
+ throw new Error(`${field} must be canonical base64url`);
57
+ }
58
+ if (expectedLength !== void 0 && decoded.length !== expectedLength) {
59
+ throw new Error(`${field} must decode to ${expectedLength} bytes`);
60
+ }
61
+ return decoded;
62
+ }
63
+ function uint32(value, field) {
64
+ if (!Number.isInteger(value) || value < 0 || value > 4294967295) {
65
+ throw new Error(`${field} must be an unsigned 32-bit integer`);
66
+ }
67
+ const encoded = new Uint8Array(4);
68
+ new DataView(encoded.buffer).setUint32(0, value, false);
69
+ return encoded;
70
+ }
71
+ function recoveryKeyVersion(value) {
72
+ if (value === 0) {
73
+ throw new Error("key_version must be greater than zero");
74
+ }
75
+ return uint32(value, "key_version");
76
+ }
77
+ function canonicalUuid(value, field) {
78
+ if (!CHAT_RECOVERY_UUID_PATTERN.test(value)) {
79
+ throw new Error(`${field} must be a canonical lowercase UUID`);
80
+ }
81
+ return new TextEncoder().encode(value);
82
+ }
83
+ function concatBytes(...values) {
84
+ const output = new Uint8Array(values.reduce((total, value) => total + value.length, 0));
85
+ let offset = 0;
86
+ for (const value of values) {
87
+ output.set(value, offset);
88
+ offset += value.length;
89
+ }
90
+ return output;
91
+ }
92
+ function lengthPrefix(value) {
93
+ return concatBytes(uint32(value.length, "length"), value);
94
+ }
95
+ async function sha256(input) {
96
+ return new Uint8Array(await cryptoApi.subtle.digest("SHA-256", toArrayBuffer(input)));
97
+ }
98
+ async function hkdfSha256(input, salt, info) {
99
+ const key = await cryptoApi.subtle.importKey("raw", toArrayBuffer(input), "HKDF", false, ["deriveBits"]);
100
+ const bits = await cryptoApi.subtle.deriveBits(
101
+ {
102
+ name: "HKDF",
103
+ hash: "SHA-256",
104
+ salt: toArrayBuffer(salt),
105
+ info: toArrayBuffer(info)
106
+ },
107
+ key,
108
+ 256
109
+ );
110
+ return new Uint8Array(bits);
111
+ }
112
+ async function deriveChatCompletionRecoveryKeypair(chatKey, chatId, keyVersion) {
113
+ const rawChatKey = base64UrlToBytes(chatKey, "chat_key", CHAT_RECOVERY_KEY_BYTES);
114
+ const salt = await sha256(new TextEncoder().encode("openmates:chat-recovery:v1"));
115
+ const info = concatBytes(lengthPrefix(canonicalUuid(chatId, "chat_id")), recoveryKeyVersion(keyVersion));
116
+ const privateKey = await hkdfSha256(rawChatKey, salt, info);
117
+ return {
118
+ privateKey: bytesToBase64Url(privateKey),
119
+ publicKey: bytesToBase64Url(nacl.scalarMult.base(privateKey))
120
+ };
121
+ }
122
+ function buildRecoveryAssociatedData(values) {
123
+ const associatedData = concatBytes(
124
+ CHAT_RECOVERY_AAD_PREFIX,
125
+ lengthPrefix(canonicalUuid(values.owner_id, "owner_id")),
126
+ lengthPrefix(canonicalUuid(values.chat_id, "chat_id")),
127
+ lengthPrefix(canonicalUuid(values.turn_id, "turn_id")),
128
+ lengthPrefix(canonicalUuid(values.job_id, "job_id")),
129
+ lengthPrefix(canonicalUuid(values.assistant_message_id, "assistant_message_id")),
130
+ recoveryKeyVersion(values.key_version)
131
+ );
132
+ return bytesToBase64Url(associatedData);
133
+ }
134
+ function recoveryAssociatedData(identity) {
135
+ return base64UrlToBytes(
136
+ buildRecoveryAssociatedData({
137
+ owner_id: identity.ownerId,
138
+ chat_id: identity.chatId,
139
+ turn_id: identity.turnId,
140
+ job_id: identity.jobId,
141
+ assistant_message_id: identity.assistantMessageId,
142
+ key_version: identity.keyVersion
143
+ }),
144
+ "associated_data"
145
+ );
146
+ }
147
+ async function recoveryEnvelopeKey(sharedSecret, associatedData) {
148
+ if (sharedSecret.every((value) => value === 0)) {
149
+ throw new Error("X25519 shared secret must not be all zero");
150
+ }
151
+ const salt = await sha256(new TextEncoder().encode("openmates:chat-recovery-envelope:v1"));
152
+ return hkdfSha256(sharedSecret, salt, await sha256(associatedData));
153
+ }
154
+ async function openChatCompletionRecoveryEnvelope(envelope, options) {
155
+ if (!envelope || Object.keys(envelope).sort().join(",") !== "ciphertext,epk,nonce,v" || envelope.v !== CHAT_RECOVERY_PROTOCOL_VERSION) {
156
+ throw new Error("invalid recovery envelope fields or version");
157
+ }
158
+ const recoveryPrivateKey = base64UrlToBytes(
159
+ options.recoveryPrivateKey,
160
+ "recovery_private_key",
161
+ CHAT_RECOVERY_KEY_BYTES
162
+ );
163
+ const ephemeralPublicKey = base64UrlToBytes(envelope.epk, "epk", CHAT_RECOVERY_KEY_BYTES);
164
+ const nonce = base64UrlToBytes(envelope.nonce, "nonce", CHAT_RECOVERY_NONCE_BYTES);
165
+ const ciphertext = base64UrlToBytes(envelope.ciphertext, "ciphertext");
166
+ if (ciphertext.length < 16 || ciphertext.length - 16 > CHAT_RECOVERY_MAX_PAYLOAD_BYTES) {
167
+ throw new Error("ciphertext payload size is invalid");
168
+ }
169
+ const associatedData = recoveryAssociatedData(options);
170
+ const sharedSecret = nacl.scalarMult(recoveryPrivateKey, ephemeralPublicKey);
171
+ const envelopeKeyBytes = await recoveryEnvelopeKey(sharedSecret, associatedData);
172
+ const envelopeKey = await cryptoApi.subtle.importKey(
173
+ "raw",
174
+ toArrayBuffer(envelopeKeyBytes),
175
+ { name: "AES-GCM" },
176
+ false,
177
+ ["decrypt"]
178
+ );
179
+ return new Uint8Array(await cryptoApi.subtle.decrypt(
180
+ { name: "AES-GCM", iv: toArrayBuffer(nonce), additionalData: toArrayBuffer(associatedData) },
181
+ envelopeKey,
182
+ toArrayBuffer(ciphertext)
183
+ ));
184
+ }
41
185
  function generateSalt(length = EMAIL_SALT_LENGTH) {
42
186
  return cryptoApi.getRandomValues(new Uint8Array(length));
43
187
  }
@@ -54,9 +198,9 @@ function generateSecureRecoveryKey(length = 24) {
54
198
  const chars = requiredSets[index];
55
199
  result[index] = chars.charAt(randomByte % chars.length);
56
200
  }
57
- const randomBytes3 = cryptoApi.getRandomValues(new Uint8Array(length));
201
+ const randomBytes4 = cryptoApi.getRandomValues(new Uint8Array(length));
58
202
  for (let index = requiredSets.length; index < length; index += 1) {
59
- result[index] = allChars.charAt(randomBytes3[index] % allChars.length);
203
+ result[index] = allChars.charAt(randomBytes4[index] % allChars.length);
60
204
  }
61
205
  for (let index = result.length - 1; index > 0; index -= 1) {
62
206
  const randomByte = cryptoApi.getRandomValues(new Uint8Array(1))[0];
@@ -409,6 +553,9 @@ var OpenMatesHttpClient = class {
409
553
  async patch(path, body, headers = {}) {
410
554
  return this.request("PATCH", path, body, headers);
411
555
  }
556
+ async put(path, body, headers = {}) {
557
+ return this.request("PUT", path, body, headers);
558
+ }
412
559
  async getBinary(path, headers = {}) {
413
560
  const url = `${this.apiUrl}${path.startsWith("/") ? path : `/${path}`}`;
414
561
  const requestHeaders = {
@@ -1042,6 +1189,29 @@ var SUB_CHAT_EVENT_TYPES = /* @__PURE__ */ new Set([
1042
1189
  ]);
1043
1190
  var SUB_CHAT_PARENT_STATUS_MESSAGE = "I've started the sub-chats and will continue once they finish.";
1044
1191
  var SUB_CHAT_COMPLETION_TIMEOUT_MS = 10 * 6e4;
1192
+ var CLIENT_UPDATE_REQUIRED_GUIDANCE = "OpenMates CLI update required. Run `openmates upgrade` and retry.";
1193
+ var WebSocketProtocolError = class extends Error {
1194
+ code;
1195
+ constructor(code, message) {
1196
+ super(message);
1197
+ this.name = "WebSocketProtocolError";
1198
+ this.code = code;
1199
+ }
1200
+ };
1201
+ function websocketProtocolError(envelope) {
1202
+ const payload = envelope.payload && typeof envelope.payload === "object" ? envelope.payload : {};
1203
+ const code = envelope.type === "client_update_required" ? envelope.type : envelope.type === "error" && typeof payload.code === "string" ? payload.code : null;
1204
+ if (code === "client_update_required") {
1205
+ return new WebSocketProtocolError(code, CLIENT_UPDATE_REQUIRED_GUIDANCE);
1206
+ }
1207
+ if (envelope.type === "error") {
1208
+ return new WebSocketProtocolError(
1209
+ code ?? "websocket_error",
1210
+ typeof payload.message === "string" ? payload.message : "Unknown WebSocket error"
1211
+ );
1212
+ }
1213
+ return null;
1214
+ }
1045
1215
  var OpenMatesWsClient = class {
1046
1216
  socket;
1047
1217
  constructor(options) {
@@ -1111,13 +1281,23 @@ var OpenMatesWsClient = class {
1111
1281
  }
1112
1282
  waitForMessage(expectedType, predicate, timeoutMs = 2e4) {
1113
1283
  return new Promise((resolve7, reject) => {
1284
+ const seenTypes = /* @__PURE__ */ new Set();
1285
+ let predicateMisses = 0;
1114
1286
  const onMessage = (rawData) => {
1115
1287
  try {
1116
1288
  const parsed = JSON.parse(rawData.toString());
1289
+ if (typeof parsed.type === "string") seenTypes.add(parsed.type);
1290
+ const protocolError = websocketProtocolError(parsed);
1291
+ if (protocolError) {
1292
+ cleanup();
1293
+ reject(protocolError);
1294
+ return;
1295
+ }
1117
1296
  if (parsed.type !== expectedType) {
1118
1297
  return;
1119
1298
  }
1120
1299
  if (predicate && !predicate(parsed.payload)) {
1300
+ predicateMisses += 1;
1121
1301
  return;
1122
1302
  }
1123
1303
  cleanup();
@@ -1135,7 +1315,12 @@ var OpenMatesWsClient = class {
1135
1315
  };
1136
1316
  const timeout = setTimeout(() => {
1137
1317
  cleanup();
1138
- reject(new Error(`Timeout waiting for '${expectedType}'`));
1318
+ const observed = [...seenTypes].sort().join(", ") || "none";
1319
+ reject(
1320
+ new Error(
1321
+ `Timeout waiting for '${expectedType}' (seen types: ${observed}; predicate misses: ${predicateMisses})`
1322
+ )
1323
+ );
1139
1324
  }, timeoutMs);
1140
1325
  const cleanup = () => {
1141
1326
  clearTimeout(timeout);
@@ -1159,6 +1344,12 @@ var OpenMatesWsClient = class {
1159
1344
  const onMessage = (rawData) => {
1160
1345
  try {
1161
1346
  const parsed = JSON.parse(rawData.toString());
1347
+ const protocolError = websocketProtocolError(parsed);
1348
+ if (protocolError) {
1349
+ cleanup();
1350
+ reject(protocolError);
1351
+ return;
1352
+ }
1162
1353
  if (parsed.type === terminatorType) {
1163
1354
  cleanup();
1164
1355
  resolve7(collected);
@@ -1218,6 +1409,7 @@ var OpenMatesWsClient = class {
1218
1409
  let taskId = null;
1219
1410
  let category = null;
1220
1411
  let modelName = null;
1412
+ let recoveryJobId = null;
1221
1413
  let followUpSuggestions = [];
1222
1414
  let newChatSuggestions = [];
1223
1415
  const subChatEvents = [];
@@ -1248,6 +1440,8 @@ var OpenMatesWsClient = class {
1248
1440
  if (typeof p.category === "string" && p.category) category = p.category;
1249
1441
  if (typeof p.model_name === "string" && p.model_name)
1250
1442
  modelName = p.model_name;
1443
+ if (typeof p.recovery_job_id === "string" && p.recovery_job_id && p.recovery_protocol_version === 1)
1444
+ recoveryJobId = p.recovery_job_id;
1251
1445
  };
1252
1446
  const extractMessageContent = (message) => {
1253
1447
  if (typeof message.content === "string") return message.content;
@@ -1279,7 +1473,8 @@ var OpenMatesWsClient = class {
1279
1473
  followUpSuggestions,
1280
1474
  newChatSuggestions,
1281
1475
  embeds: [...embeds.values()],
1282
- subChatEvents
1476
+ subChatEvents,
1477
+ recoveryJobId
1283
1478
  });
1284
1479
  return;
1285
1480
  }
@@ -1299,7 +1494,8 @@ var OpenMatesWsClient = class {
1299
1494
  followUpSuggestions,
1300
1495
  newChatSuggestions,
1301
1496
  embeds: [...embeds.values()],
1302
- subChatEvents
1497
+ subChatEvents,
1498
+ recoveryJobId
1303
1499
  });
1304
1500
  }, asyncEmbedWaitMs);
1305
1501
  return;
@@ -1316,7 +1512,8 @@ var OpenMatesWsClient = class {
1316
1512
  followUpSuggestions,
1317
1513
  newChatSuggestions,
1318
1514
  embeds: [...embeds.values()],
1319
- subChatEvents
1515
+ subChatEvents,
1516
+ recoveryJobId
1320
1517
  });
1321
1518
  };
1322
1519
  const handleSubChatEvent = (type, p) => {
@@ -1388,13 +1585,10 @@ var OpenMatesWsClient = class {
1388
1585
  const parsed = JSON.parse(rawData.toString());
1389
1586
  const p = parsed.payload ?? {};
1390
1587
  const type = parsed.type;
1391
- if (type === "error") {
1588
+ const protocolError = websocketProtocolError(parsed);
1589
+ if (protocolError) {
1392
1590
  cleanup();
1393
- reject(
1394
- new Error(
1395
- typeof p.message === "string" ? p.message : "Unknown chat error"
1396
- )
1397
- );
1591
+ reject(protocolError);
1398
1592
  return;
1399
1593
  }
1400
1594
  if (SUB_CHAT_EVENT_TYPES.has(type)) {
@@ -1530,7 +1724,8 @@ var OpenMatesWsClient = class {
1530
1724
  followUpSuggestions,
1531
1725
  newChatSuggestions,
1532
1726
  embeds: [...embeds.values()],
1533
- subChatEvents
1727
+ subChatEvents,
1728
+ recoveryJobId
1534
1729
  });
1535
1730
  return;
1536
1731
  }
@@ -1957,7 +2152,7 @@ async function encryptEmbed(embed, masterKey, chatKey, chatId, messageId, userId
1957
2152
  const hashedMessageId = computeSHA256(messageId);
1958
2153
  const hashedUserId = computeSHA256(userId);
1959
2154
  const wrappedWithMaster = await wrapKey(embedKey, masterKey);
1960
- const nowSeconds = Math.floor(Date.now() / 1e3);
2155
+ const nowSeconds2 = Math.floor(Date.now() / 1e3);
1961
2156
  const embedKeys = [
1962
2157
  {
1963
2158
  hashed_embed_id: hashedEmbedId,
@@ -1965,7 +2160,7 @@ async function encryptEmbed(embed, masterKey, chatKey, chatId, messageId, userId
1965
2160
  hashed_chat_id: null,
1966
2161
  encrypted_embed_key: wrappedWithMaster,
1967
2162
  hashed_user_id: hashedUserId,
1968
- created_at: nowSeconds
2163
+ created_at: nowSeconds2
1969
2164
  }
1970
2165
  ];
1971
2166
  if (chatKey) {
@@ -1976,7 +2171,7 @@ async function encryptEmbed(embed, masterKey, chatKey, chatId, messageId, userId
1976
2171
  hashed_chat_id: hashedChatId,
1977
2172
  encrypted_embed_key: wrappedWithChat,
1978
2173
  hashed_user_id: hashedUserId,
1979
- created_at: nowSeconds
2174
+ created_at: nowSeconds2
1980
2175
  });
1981
2176
  }
1982
2177
  return {
@@ -1991,8 +2186,8 @@ async function encryptEmbed(embed, masterKey, chatKey, chatId, messageId, userId
1991
2186
  file_path: embed.filePath,
1992
2187
  content_hash: embed.contentHash,
1993
2188
  text_length_chars: embed.textLengthChars,
1994
- created_at: nowSeconds,
1995
- updated_at: nowSeconds,
2189
+ created_at: nowSeconds2,
2190
+ updated_at: nowSeconds2,
1996
2191
  embed_keys: embedKeys
1997
2192
  };
1998
2193
  } catch (error) {
@@ -2137,11 +2332,11 @@ async function decryptConnectedAccountCliTransferPayload(encryptedPayload, passc
2137
2332
  throw new Error("Unsupported connected account import payload format.");
2138
2333
  }
2139
2334
  try {
2140
- const key = await deriveTransferKey(passcode, base64UrlToBytes(envelope.kdf.salt), envelope.kdf.iterations);
2335
+ const key = await deriveTransferKey(passcode, base64UrlToBytes2(envelope.kdf.salt), envelope.kdf.iterations);
2141
2336
  const plaintext = await webcrypto5.subtle.decrypt(
2142
- { name: "AES-GCM", iv: toArrayBuffer3(base64UrlToBytes(envelope.cipher.iv)) },
2337
+ { name: "AES-GCM", iv: toArrayBuffer3(base64UrlToBytes2(envelope.cipher.iv)) },
2143
2338
  key,
2144
- toArrayBuffer3(base64UrlToBytes(envelope.cipher.text))
2339
+ toArrayBuffer3(base64UrlToBytes2(envelope.cipher.text))
2145
2340
  );
2146
2341
  return validateTransferPayload(JSON.parse(new TextDecoder().decode(plaintext)));
2147
2342
  } catch (error) {
@@ -2189,7 +2384,7 @@ async function buildEncryptedConnectedAccountImportRow(params) {
2189
2384
  }
2190
2385
  function parseEnvelope(encodedEnvelope) {
2191
2386
  try {
2192
- return JSON.parse(new TextDecoder().decode(base64UrlToBytes(encodedEnvelope)));
2387
+ return JSON.parse(new TextDecoder().decode(base64UrlToBytes2(encodedEnvelope)));
2193
2388
  } catch {
2194
2389
  throw new Error("Connected account import payload is malformed.");
2195
2390
  }
@@ -2269,7 +2464,7 @@ function defaultProviderLabel(providerId) {
2269
2464
  function sha256Hex2(value) {
2270
2465
  return createHash4("sha256").update(value).digest("hex");
2271
2466
  }
2272
- function base64UrlToBytes(value) {
2467
+ function base64UrlToBytes2(value) {
2273
2468
  return base64ToBytes(value.replace(/-/g, "+").replace(/_/g, "/"));
2274
2469
  }
2275
2470
  function toArrayBuffer3(input) {
@@ -2863,6 +3058,15 @@ var MEMORY_TYPE_REGISTRY = {
2863
3058
  properties: { url: { type: "string" }, notes: { type: "string" } }
2864
3059
  }
2865
3060
  };
3061
+ function reconcileAuthoritativeChats(chats, evidence) {
3062
+ const deletedIds = new Set(evidence.deleted_chat_ids ?? []);
3063
+ const authoritativeIds = evidence.authoritative && evidence.authoritative_chat_ids ? new Set(evidence.authoritative_chat_ids) : null;
3064
+ if (!authoritativeIds && deletedIds.size === 0) return chats;
3065
+ return chats.filter((chat) => {
3066
+ const chatId = String(chat.details.id ?? "");
3067
+ return (!authoritativeIds || authoritativeIds.has(chatId)) && !deletedIds.has(chatId);
3068
+ });
3069
+ }
2866
3070
  var MATE_NAMES = {
2867
3071
  software_development: "Sophia",
2868
3072
  business_development: "Burton",
@@ -3635,6 +3839,157 @@ var OpenMatesClient = class _OpenMatesClient {
3635
3839
  hasMore: offset + limit < total
3636
3840
  };
3637
3841
  }
3842
+ async saveDraft(params) {
3843
+ const markdown = params.markdown.trim();
3844
+ if (!markdown) throw new Error("Draft markdown must not be empty.");
3845
+ const masterKey = this.getMasterKeyBytes();
3846
+ const encrypted = await this.saveEncryptedDraft({
3847
+ chatId: params.chatId ?? randomUUID3(),
3848
+ encryptedDraftMd: await encryptWithAesGcmCombined(markdown, masterKey),
3849
+ encryptedDraftPreview: params.preview ? await encryptWithAesGcmCombined(params.preview, masterKey) : null
3850
+ });
3851
+ return { ...encrypted, markdown, preview: params.preview ?? null };
3852
+ }
3853
+ async saveEncryptedDraft(params) {
3854
+ const { ws } = await this.openWsClient();
3855
+ try {
3856
+ const receipt = ws.waitForMessage(
3857
+ "draft_update_receipt",
3858
+ (payload) => payload.chat_id === params.chatId
3859
+ );
3860
+ await ws.sendAsync("update_draft", {
3861
+ chat_id: params.chatId,
3862
+ encrypted_draft_md: params.encryptedDraftMd,
3863
+ encrypted_draft_preview: params.encryptedDraftPreview ?? null
3864
+ });
3865
+ const response = await receipt;
3866
+ const draftV = Number(response.payload.draft_v ?? 0);
3867
+ this.storeEncryptedDraft({
3868
+ chatId: params.chatId,
3869
+ encryptedDraftMd: params.encryptedDraftMd,
3870
+ encryptedDraftPreview: params.encryptedDraftPreview ?? null,
3871
+ draftV
3872
+ });
3873
+ return {
3874
+ chatId: params.chatId,
3875
+ encryptedDraftMd: params.encryptedDraftMd,
3876
+ encryptedDraftPreview: params.encryptedDraftPreview ?? null,
3877
+ draftV
3878
+ };
3879
+ } finally {
3880
+ ws.close();
3881
+ }
3882
+ }
3883
+ async listDrafts(forceRefresh = false) {
3884
+ if (forceRefresh) await this.reconcileDraftVersions();
3885
+ const cache = await this.ensureSynced(forceRefresh);
3886
+ const drafts = [];
3887
+ for (const chat of cache.chats) {
3888
+ const draft = await this.decryptCachedDraft(chat);
3889
+ if (draft) drafts.push(draft);
3890
+ }
3891
+ return drafts;
3892
+ }
3893
+ async getDraft(chatId, forceRefresh = false) {
3894
+ if (forceRefresh) await this.reconcileDraftVersions();
3895
+ const cache = await this.ensureSynced(forceRefresh);
3896
+ const chat = cache.chats.find((entry) => String(entry.details.id ?? "") === chatId);
3897
+ return chat ? this.decryptCachedDraft(chat) : null;
3898
+ }
3899
+ async clearDraft(chatId) {
3900
+ const { ws } = await this.openWsClient();
3901
+ try {
3902
+ const receipt = ws.waitForMessage(
3903
+ "draft_delete_receipt",
3904
+ (payload) => payload.chat_id === chatId
3905
+ );
3906
+ await ws.sendAsync("delete_draft", { chat_id: chatId });
3907
+ await receipt;
3908
+ } finally {
3909
+ ws.close();
3910
+ }
3911
+ const cache = loadSyncCache();
3912
+ if (!cache) return;
3913
+ const chat = cache.chats.find((entry) => String(entry.details.id ?? "") === chatId);
3914
+ if (chat) {
3915
+ delete chat.details.encrypted_draft_md;
3916
+ delete chat.details.encrypted_draft_preview;
3917
+ chat.details.draft_v = 0;
3918
+ if (chat.messages.length === 0 && !chat.details.encrypted_chat_key) {
3919
+ cache.chats = cache.chats.filter((entry) => entry !== chat);
3920
+ }
3921
+ saveSyncCache(cache);
3922
+ }
3923
+ }
3924
+ async reconcileDraftVersions() {
3925
+ const cache = loadSyncCache();
3926
+ const drafts = (cache?.chats ?? []).filter(
3927
+ (chat) => typeof chat.details.encrypted_draft_md === "string"
3928
+ );
3929
+ if (drafts.length === 0) return {};
3930
+ const { ws } = await this.openWsClient();
3931
+ try {
3932
+ const response = ws.waitForMessage("draft_versions_response");
3933
+ await ws.sendAsync("get_draft_versions", {
3934
+ chats: drafts.map((chat) => ({
3935
+ chat_id: String(chat.details.id),
3936
+ client_draft_v: Number(chat.details.draft_v ?? 0)
3937
+ }))
3938
+ });
3939
+ const frame = await response;
3940
+ const versions = frame.payload.versions ?? {};
3941
+ for (const chat of drafts) {
3942
+ const chatId = String(chat.details.id);
3943
+ if (versions[chatId] === 0) {
3944
+ delete chat.details.encrypted_draft_md;
3945
+ delete chat.details.encrypted_draft_preview;
3946
+ chat.details.draft_v = 0;
3947
+ }
3948
+ }
3949
+ if (cache) saveSyncCache(cache);
3950
+ return versions;
3951
+ } finally {
3952
+ ws.close();
3953
+ }
3954
+ }
3955
+ storeEncryptedDraft(draft) {
3956
+ const cache = loadSyncCache() ?? {
3957
+ syncedAt: 0,
3958
+ totalChatCount: 0,
3959
+ loadedChatCount: 0,
3960
+ chats: [],
3961
+ embeds: [],
3962
+ embedKeys: []
3963
+ };
3964
+ let chat = cache.chats.find((entry) => String(entry.details.id ?? "") === draft.chatId);
3965
+ if (!chat) {
3966
+ chat = { details: { id: draft.chatId }, messages: [] };
3967
+ cache.chats.unshift(chat);
3968
+ }
3969
+ chat.details.encrypted_draft_md = draft.encryptedDraftMd;
3970
+ chat.details.encrypted_draft_preview = draft.encryptedDraftPreview;
3971
+ chat.details.draft_v = draft.draftV;
3972
+ cache.syncedAt = Date.now();
3973
+ cache.loadedChatCount = cache.chats.length;
3974
+ saveSyncCache(cache);
3975
+ }
3976
+ async decryptCachedDraft(chat) {
3977
+ const encryptedDraftMd = chat.details.encrypted_draft_md;
3978
+ if (typeof encryptedDraftMd !== "string") return null;
3979
+ const encryptedPreview = typeof chat.details.encrypted_draft_preview === "string" ? chat.details.encrypted_draft_preview : null;
3980
+ const masterKey = this.getMasterKeyBytes();
3981
+ const markdown = await decryptWithAesGcmCombined(encryptedDraftMd, masterKey);
3982
+ if (markdown === null) throw new Error("Failed to decrypt draft markdown.");
3983
+ const preview = encryptedPreview ? await decryptWithAesGcmCombined(encryptedPreview, masterKey) : markdown.slice(0, 160);
3984
+ return {
3985
+ chatId: String(chat.details.id ?? ""),
3986
+ encryptedDraftMd,
3987
+ encryptedDraftPreview: encryptedPreview,
3988
+ draftV: Number(chat.details.draft_v ?? 0),
3989
+ markdown,
3990
+ preview
3991
+ };
3992
+ }
3638
3993
  async searchChats(query) {
3639
3994
  const cache = await this.ensureSynced();
3640
3995
  const masterKey = this.getMasterKeyBytes();
@@ -3727,8 +4082,8 @@ var OpenMatesClient = class _OpenMatesClient {
3727
4082
  );
3728
4083
  let msgEmbedIds = [];
3729
4084
  if (clientMsgId && cache.embeds.length > 0) {
3730
- const { createHash: createHash7 } = await import("crypto");
3731
- const hashed = createHash7("sha256").update(clientMsgId).digest("hex");
4085
+ const { createHash: createHash8 } = await import("crypto");
4086
+ const hashed = createHash8("sha256").update(clientMsgId).digest("hex");
3732
4087
  msgEmbedIds = cache.embeds.filter(
3733
4088
  (e) => e.hashed_message_id === hashed && // Only include parent embeds (no parent_embed_id).
3734
4089
  // Child embeds inherit the parent's key and are loaded
@@ -3775,8 +4130,8 @@ var OpenMatesClient = class _OpenMatesClient {
3775
4130
  );
3776
4131
  }
3777
4132
  const embedId = String(embed.embed_id ?? embed.id ?? "");
3778
- const { createHash: createHash7 } = await import("crypto");
3779
- const hashedEmbedId = createHash7("sha256").update(embedId).digest("hex");
4133
+ const { createHash: createHash8 } = await import("crypto");
4134
+ const hashedEmbedId = createHash8("sha256").update(embedId).digest("hex");
3780
4135
  const embedKeyBytes = await this.resolveEmbedKey(
3781
4136
  cache,
3782
4137
  masterKey,
@@ -3884,7 +4239,7 @@ var OpenMatesClient = class _OpenMatesClient {
3884
4239
  async resolveEmbedKey(cache, masterKey, embed, embedId, hashedEmbedId, visited = /* @__PURE__ */ new Set()) {
3885
4240
  if (visited.has(embedId)) return null;
3886
4241
  visited.add(embedId);
3887
- const { createHash: createHash7 } = await import("crypto");
4242
+ const { createHash: createHash8 } = await import("crypto");
3888
4243
  const masterKeyEntry = cache.embedKeys.find(
3889
4244
  (ek) => ek.hashed_embed_id === hashedEmbedId && String(ek.key_type) === "master"
3890
4245
  );
@@ -3901,7 +4256,7 @@ var OpenMatesClient = class _OpenMatesClient {
3901
4256
  if (chatKeyEntry && typeof chatKeyEntry.encrypted_embed_key === "string") {
3902
4257
  const hashedChatId = String(chatKeyEntry.hashed_chat_id ?? "");
3903
4258
  const owningChat = cache.chats.find((c) => {
3904
- const chatHash = createHash7("sha256").update(String(c.details.id ?? "")).digest("hex");
4259
+ const chatHash = createHash8("sha256").update(String(c.details.id ?? "")).digest("hex");
3905
4260
  return chatHash === hashedChatId;
3906
4261
  });
3907
4262
  if (owningChat) {
@@ -3930,7 +4285,7 @@ var OpenMatesClient = class _OpenMatesClient {
3930
4285
  const parentFullId = String(
3931
4286
  parentEmbed2.embed_id ?? parentEmbed2.id ?? ""
3932
4287
  );
3933
- const parentHashedId = createHash7("sha256").update(parentFullId).digest("hex");
4288
+ const parentHashedId = createHash8("sha256").update(parentFullId).digest("hex");
3934
4289
  const parentKey = await this.resolveEmbedKey(
3935
4290
  cache,
3936
4291
  masterKey,
@@ -4074,7 +4429,11 @@ var OpenMatesClient = class _OpenMatesClient {
4074
4429
  }
4075
4430
  }
4076
4431
  }
4077
- const { ws, session } = await this.openWsClient();
4432
+ const { ws, session, ownerId } = await this.openWsClient();
4433
+ if (!params.incognito && !ownerId) {
4434
+ ws.close();
4435
+ throw new Error("Authenticated user identity is required for saved chat recovery.");
4436
+ }
4078
4437
  const messageId = randomUUID3();
4079
4438
  const createdAt = Math.floor(Date.now() / 1e3);
4080
4439
  const isNewChat = !params.chatId;
@@ -4089,6 +4448,40 @@ var OpenMatesClient = class _OpenMatesClient {
4089
4448
  }) : [];
4090
4449
  assertNoConnectedAccountSecretLeak(connectedAccountTokenRefs);
4091
4450
  const piiMappings = params.piiMappings ?? [];
4451
+ let chatKeyBytes = null;
4452
+ let encryptedChatKey = null;
4453
+ let baselineMessagesV = 0;
4454
+ let savedTurnId = null;
4455
+ if (!params.incognito) {
4456
+ const masterKey = this.getMasterKeyBytes();
4457
+ if (isNewChat) {
4458
+ chatKeyBytes = globalThis.crypto ? new Uint8Array(globalThis.crypto.getRandomValues(new Uint8Array(32))) : new Uint8Array(
4459
+ (await import("crypto")).webcrypto.getRandomValues(
4460
+ new Uint8Array(32)
4461
+ )
4462
+ );
4463
+ encryptedChatKey = await encryptBytesWithAesGcm(
4464
+ chatKeyBytes,
4465
+ masterKey
4466
+ );
4467
+ } else {
4468
+ const cache = loadSyncCache() ?? await this.ensureSynced();
4469
+ const chat = cache.chats.find(
4470
+ (c) => String(c.details.id ?? "") === chatId || String(c.details.id ?? "").startsWith(chatId)
4471
+ );
4472
+ if (chat) {
4473
+ baselineMessagesV = typeof chat.details.messages_v === "number" ? chat.details.messages_v : 0;
4474
+ const encKey = typeof chat.details.encrypted_chat_key === "string" ? chat.details.encrypted_chat_key : null;
4475
+ if (encKey) {
4476
+ chatKeyBytes = await decryptBytesWithAesGcm(encKey, masterKey);
4477
+ encryptedChatKey = encKey;
4478
+ }
4479
+ }
4480
+ if (!chatKeyBytes || !encryptedChatKey) {
4481
+ throw new Error(`Encrypted chat key not found for chat '${chatId}'. Sync and try again.`);
4482
+ }
4483
+ }
4484
+ }
4092
4485
  const messagePayload = {
4093
4486
  chat_id: chatId,
4094
4487
  is_incognito: Boolean(params.incognito),
@@ -4103,6 +4496,9 @@ var OpenMatesClient = class _OpenMatesClient {
4103
4496
  chat_has_title: Boolean(params.chatId)
4104
4497
  }
4105
4498
  };
4499
+ if (isNewChat && encryptedChatKey) {
4500
+ messagePayload.encrypted_chat_key = encryptedChatKey;
4501
+ }
4106
4502
  if (memoryMetadataKeys.length > 0) {
4107
4503
  messagePayload.app_settings_memories_metadata = memoryMetadataKeys;
4108
4504
  }
@@ -4144,37 +4540,6 @@ var OpenMatesClient = class _OpenMatesClient {
4144
4540
  created_at: historyMessage.created_at
4145
4541
  }));
4146
4542
  }
4147
- let chatKeyBytes = null;
4148
- let encryptedChatKey = null;
4149
- let baselineMessagesV = 0;
4150
- if (!params.incognito) {
4151
- const masterKey = this.getMasterKeyBytes();
4152
- if (isNewChat) {
4153
- chatKeyBytes = globalThis.crypto ? new Uint8Array(globalThis.crypto.getRandomValues(new Uint8Array(32))) : new Uint8Array(
4154
- (await import("crypto")).webcrypto.getRandomValues(
4155
- new Uint8Array(32)
4156
- )
4157
- );
4158
- encryptedChatKey = await encryptBytesWithAesGcm(
4159
- chatKeyBytes,
4160
- masterKey
4161
- );
4162
- messagePayload.encrypted_chat_key = encryptedChatKey;
4163
- } else {
4164
- const cache = loadSyncCache() ?? await this.ensureSynced();
4165
- const chat = cache.chats.find(
4166
- (c) => String(c.details.id ?? "") === chatId || String(c.details.id ?? "").startsWith(chatId)
4167
- );
4168
- if (chat) {
4169
- baselineMessagesV = typeof chat.details.messages_v === "number" ? chat.details.messages_v : 0;
4170
- const encKey = typeof chat.details.encrypted_chat_key === "string" ? chat.details.encrypted_chat_key : null;
4171
- if (encKey) {
4172
- chatKeyBytes = await decryptBytesWithAesGcm(encKey, masterKey);
4173
- encryptedChatKey = encKey;
4174
- }
4175
- }
4176
- }
4177
- }
4178
4543
  if (params.preparedEmbeds && params.preparedEmbeds.length > 0) {
4179
4544
  messagePayload.embeds = params.preparedEmbeds.map((embed) => ({
4180
4545
  embed_id: embed.embedId,
@@ -4202,47 +4567,93 @@ var OpenMatesClient = class _OpenMatesClient {
4202
4567
  if (encryptedEmbeds.length > 0) {
4203
4568
  messagePayload.encrypted_embeds = encryptedEmbeds;
4204
4569
  }
4205
- const precollectedResponse = params.precollectResponse ? ws.collectAiResponse(messageId, chatId, { onStream: params.onStream }) : null;
4206
- const confirmed = ws.waitForMessage(
4207
- "chat_message_confirmed",
4208
- (payload) => {
4209
- const eventPayload = payload;
4210
- return eventPayload.chat_id === chatId && eventPayload.message_id === messageId;
4211
- },
4212
- 2e4
4213
- );
4214
- await ws.sendAsync("chat_message_added", messagePayload);
4215
- await confirmed;
4216
- if (!params.incognito && chatKeyBytes) {
4570
+ let precollectedResponse = params.precollectResponse && params.incognito ? ws.collectAiResponse(messageId, chatId, { onStream: params.onStream }) : null;
4571
+ if (!params.incognito && chatKeyBytes && encryptedChatKey) {
4572
+ const protocolVersion = 1;
4573
+ const chatKeyVersion = 1;
4574
+ const turnId = randomUUID3();
4575
+ savedTurnId = turnId;
4576
+ const recoveryKeypair = await deriveChatCompletionRecoveryKeypair(
4577
+ Buffer.from(chatKeyBytes).toString("base64url"),
4578
+ chatId,
4579
+ chatKeyVersion
4580
+ );
4217
4581
  const encryptedContent = await encryptWithAesGcmCombined(
4218
4582
  params.message,
4219
4583
  chatKeyBytes
4220
4584
  );
4221
- const encryptedSenderName = await encryptWithAesGcmCombined(
4222
- "User",
4223
- chatKeyBytes
4224
- );
4225
- const metadataPayload = {
4585
+ const encryptedUserMessage = {
4586
+ client_message_id: messageId,
4226
4587
  chat_id: chatId,
4227
- message_id: messageId,
4228
4588
  encrypted_content: encryptedContent,
4229
- encrypted_sender_name: encryptedSenderName,
4589
+ role: "user",
4230
4590
  created_at: createdAt,
4231
- encrypted_chat_key: encryptedChatKey,
4232
- versions: {
4233
- messages_v: 0,
4234
- title_v: 0,
4235
- last_edited_overall_timestamp: createdAt
4236
- }
4591
+ updated_at: createdAt
4237
4592
  };
4238
4593
  if (piiMappings.length > 0) {
4239
- metadataPayload.encrypted_pii_mappings = await encryptWithAesGcmCombined(
4594
+ encryptedUserMessage.encrypted_pii_mappings = await encryptWithAesGcmCombined(
4240
4595
  JSON.stringify(piiMappings),
4241
4596
  chatKeyBytes
4242
4597
  );
4243
4598
  }
4244
- ws.send("encrypted_chat_metadata", metadataPayload);
4599
+ Object.assign(messagePayload, {
4600
+ turn_id: turnId,
4601
+ recovery_public_key: recoveryKeypair.publicKey,
4602
+ chat_key_version: chatKeyVersion
4603
+ });
4604
+ const preflightPayload = {
4605
+ protocol_version: protocolVersion,
4606
+ chat_id: chatId,
4607
+ turn_id: turnId,
4608
+ message_id: messageId,
4609
+ chat_key_version: chatKeyVersion,
4610
+ encrypted_chat_key: encryptedChatKey,
4611
+ recovery_public_key: recoveryKeypair.publicKey,
4612
+ expected_messages_v: baselineMessagesV,
4613
+ encrypted_user_message: encryptedUserMessage,
4614
+ inference_request: messagePayload
4615
+ };
4616
+ if (isNewChat) {
4617
+ preflightPayload.encrypted_chat_metadata = {
4618
+ encrypted_title: await encryptWithAesGcmCombined("", chatKeyBytes),
4619
+ encrypted_chat_key: encryptedChatKey,
4620
+ created_at: createdAt,
4621
+ updated_at: createdAt
4622
+ };
4623
+ }
4624
+ const preflightAck = ws.waitForMessage("chat_turn_preflight_ack");
4625
+ let ackPayload;
4626
+ try {
4627
+ await ws.sendAsync("chat_turn_preflight", preflightPayload);
4628
+ ackPayload = (await preflightAck).payload;
4629
+ } catch (error) {
4630
+ ws.close();
4631
+ throw error;
4632
+ }
4633
+ if (typeof ackPayload.preflight_id !== "string" || !ackPayload.preflight_id) {
4634
+ ws.close();
4635
+ throw new Error("Encrypted chat preflight acknowledgement omitted preflight_id.");
4636
+ }
4637
+ Object.assign(messagePayload, {
4638
+ protocol_version: protocolVersion,
4639
+ preflight_id: ackPayload.preflight_id
4640
+ });
4641
+ }
4642
+ if (params.precollectResponse && !params.incognito) {
4643
+ precollectedResponse = ws.collectAiResponse(messageId, chatId, {
4644
+ onStream: params.onStream
4645
+ });
4245
4646
  }
4647
+ const confirmed = ws.waitForMessage(
4648
+ "chat_message_confirmed",
4649
+ (payload) => {
4650
+ const eventPayload = payload;
4651
+ return eventPayload.chat_id === chatId && eventPayload.message_id === messageId;
4652
+ },
4653
+ 2e4
4654
+ );
4655
+ await ws.sendAsync("chat_message_added", messagePayload);
4656
+ await confirmed;
4246
4657
  let assistant = "";
4247
4658
  let assistantMessageId = null;
4248
4659
  let category = null;
@@ -4442,7 +4853,7 @@ var OpenMatesClient = class _OpenMatesClient {
4442
4853
  }
4443
4854
  } else {
4444
4855
  try {
4445
- const resp = await ws.collectAiResponse(messageId, chatId, streamOpts);
4856
+ const resp = await (precollectedResponse ?? ws.collectAiResponse(messageId, chatId, streamOpts));
4446
4857
  assistantMessageId = resp.messageId;
4447
4858
  assistant = resp.content;
4448
4859
  category = resp.category;
@@ -4463,47 +4874,110 @@ var OpenMatesClient = class _OpenMatesClient {
4463
4874
  appSettingsMemoryRequests
4464
4875
  };
4465
4876
  }
4466
- if (chatKeyBytes && assistant) {
4877
+ if (chatKeyBytes) {
4878
+ if (!resp.recoveryJobId || !savedTurnId || !ownerId) {
4879
+ throw new Error("Saved chat completion did not include recoverable terminal identity.");
4880
+ }
4881
+ const recoveryJobId = resp.recoveryJobId;
4882
+ const claimPromise = ws.waitForMessage(
4883
+ "recovery_job_claimed",
4884
+ (payload) => payload.job_id === recoveryJobId,
4885
+ 2e4
4886
+ );
4887
+ await ws.sendAsync("recovery_job_claim", {
4888
+ protocol_version: 1,
4889
+ job_id: recoveryJobId
4890
+ });
4891
+ const claim = (await claimPromise).payload;
4892
+ const leaseToken = typeof claim.lease_token === "string" ? claim.lease_token : null;
4893
+ const leaseGeneration = typeof claim.lease_generation === "number" && Number.isSafeInteger(claim.lease_generation) ? claim.lease_generation : null;
4894
+ const assistantId = typeof claim.assistant_message_id === "string" ? claim.assistant_message_id : null;
4895
+ const keyVersion = typeof claim.chat_key_version === "number" && Number.isSafeInteger(claim.chat_key_version) ? claim.chat_key_version : null;
4896
+ if (claim.state !== "LEASED" || !leaseToken || leaseGeneration === null || leaseGeneration < 1 || !assistantId || keyVersion === null || keyVersion !== 1 || claim.chat_id !== chatId || claim.turn_id !== savedTurnId || typeof claim.sealed_payload !== "string") {
4897
+ throw new Error("Recovery job claim returned invalid lease or identity data.");
4898
+ }
4899
+ const recoveryKeypair = await deriveChatCompletionRecoveryKeypair(
4900
+ Buffer.from(chatKeyBytes).toString("base64url"),
4901
+ chatId,
4902
+ keyVersion
4903
+ );
4904
+ let envelope;
4905
+ try {
4906
+ envelope = JSON.parse(claim.sealed_payload);
4907
+ } catch {
4908
+ throw new Error("Recovery job contained an invalid sealed envelope.");
4909
+ }
4910
+ const plaintext = await openChatCompletionRecoveryEnvelope(
4911
+ envelope,
4912
+ {
4913
+ recoveryPrivateKey: recoveryKeypair.privateKey,
4914
+ ownerId,
4915
+ chatId,
4916
+ turnId: savedTurnId,
4917
+ jobId: recoveryJobId,
4918
+ assistantMessageId: assistantId,
4919
+ keyVersion
4920
+ }
4921
+ );
4922
+ let recovered;
4923
+ try {
4924
+ recovered = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(plaintext));
4925
+ } catch {
4926
+ throw new Error("Recovery job plaintext was not valid UTF-8 JSON.");
4927
+ }
4928
+ const expectedRecoveryFields = [
4929
+ "assistant_message_id",
4930
+ "category",
4931
+ "chat_id",
4932
+ "content",
4933
+ "job_id",
4934
+ "key_version",
4935
+ "model_name",
4936
+ "turn_id"
4937
+ ];
4938
+ if (Object.keys(recovered).sort().join(",") !== expectedRecoveryFields.join(",") || recovered.assistant_message_id !== assistantId || recovered.chat_id !== chatId || recovered.turn_id !== savedTurnId || recovered.job_id !== recoveryJobId || recovered.key_version !== keyVersion || typeof recovered.content !== "string" || recovered.category !== null && typeof recovered.category !== "string" || recovered.model_name !== null && typeof recovered.model_name !== "string" || recovered.content !== assistant || recovered.category !== category || recovered.model_name !== modelName) {
4939
+ throw new Error("Recovery job plaintext did not match the terminal completion identity.");
4940
+ }
4467
4941
  const completedAt = Math.floor(Date.now() / 1e3);
4468
4942
  const encryptedAssistantContent = await encryptWithAesGcmCombined(
4469
- assistant,
4943
+ recovered.content,
4470
4944
  chatKeyBytes
4471
4945
  );
4472
- const encryptedCategory = category ? await encryptWithAesGcmCombined(category, chatKeyBytes) : void 0;
4473
- const encryptedModelName = modelName ? await encryptWithAesGcmCombined(modelName, chatKeyBytes) : void 0;
4474
- const persistedAssistantMessageId = assistantMessageId ?? randomUUID3();
4475
- ws.send("ai_response_completed", {
4476
- chat_id: chatId,
4477
- message: {
4478
- message_id: persistedAssistantMessageId,
4946
+ const encryptedSenderName = await encryptWithAesGcmCombined("Assistant", chatKeyBytes);
4947
+ const encryptedCategory = recovered.category ? await encryptWithAesGcmCombined(recovered.category, chatKeyBytes) : void 0;
4948
+ const encryptedModelName = recovered.model_name ? await encryptWithAesGcmCombined(recovered.model_name, chatKeyBytes) : void 0;
4949
+ const persistedPromise = ws.waitForMessage(
4950
+ "recovery_job_persisted",
4951
+ (payload) => payload.job_id === recoveryJobId,
4952
+ 2e4
4953
+ );
4954
+ await ws.sendAsync("recovery_job_persist", {
4955
+ protocol_version: 1,
4956
+ job_id: recoveryJobId,
4957
+ lease_token: leaseToken,
4958
+ lease_generation: leaseGeneration,
4959
+ expected_messages_v: baselineMessagesV + 1,
4960
+ encrypted_assistant_message: {
4961
+ client_message_id: assistantId,
4479
4962
  chat_id: chatId,
4480
- role: "assistant",
4481
- created_at: completedAt,
4482
- status: "synced",
4483
- user_message_id: messageId,
4484
4963
  encrypted_content: encryptedAssistantContent,
4964
+ encrypted_sender_name: encryptedSenderName,
4485
4965
  encrypted_category: encryptedCategory,
4486
- encrypted_model_name: encryptedModelName
4487
- },
4488
- versions: {
4489
- messages_v: baselineMessagesV + 2,
4490
- last_edited_overall_timestamp: completedAt
4966
+ encrypted_model_name: encryptedModelName,
4967
+ role: "assistant",
4968
+ user_message_id: messageId,
4969
+ created_at: completedAt,
4970
+ updated_at: completedAt
4491
4971
  }
4492
4972
  });
4493
- await ws.waitForMessage(
4494
- "ai_response_storage_confirmed",
4495
- (payload) => {
4496
- const p = payload;
4497
- return p.message_id === persistedAssistantMessageId;
4498
- },
4499
- 2e4
4500
- );
4973
+ await persistedPromise;
4974
+ assistantMessageId = assistantId;
4501
4975
  await this.persistStreamedEmbeds({
4502
4976
  ws,
4503
4977
  embeds: resp.embeds,
4504
4978
  chatId,
4505
4979
  chatKeyBytes,
4506
- fallbackMessageId: persistedAssistantMessageId
4980
+ fallbackMessageId: assistantId
4507
4981
  });
4508
4982
  await this.persistPostProcessingMetadata({
4509
4983
  ws,
@@ -5041,6 +5515,30 @@ var OpenMatesClient = class _OpenMatesClient {
5041
5515
  }
5042
5516
  return response.data.workflow;
5043
5517
  }
5518
+ async validateWorkflowYaml(source) {
5519
+ this.requireSession();
5520
+ const response = await this.http.post("/v1/workflows/validate", { source }, this.getCliRequestHeaders());
5521
+ if (!response.ok || !response.data.validation) {
5522
+ throw new Error(`Workflow YAML validation failed with HTTP ${response.status}`);
5523
+ }
5524
+ return response.data.validation;
5525
+ }
5526
+ async createWorkflowYaml(source) {
5527
+ this.requireSession();
5528
+ const response = await this.http.post("/v1/workflows/yaml", { source }, this.getCliRequestHeaders());
5529
+ if (!response.ok || !response.data.workflow || !response.data.validation) {
5530
+ throw new Error(`Workflow YAML create failed with HTTP ${response.status}`);
5531
+ }
5532
+ return { workflow: response.data.workflow, validation: response.data.validation };
5533
+ }
5534
+ async updateWorkflowYaml(workflowId, source) {
5535
+ this.requireSession();
5536
+ const createLike = await this.http.post(`/v1/workflows/${encodeURIComponent(workflowId)}/yaml`, { source }, this.getCliRequestHeaders());
5537
+ if (!createLike.ok || !createLike.data.workflow || !createLike.data.validation) {
5538
+ throw new Error(`Workflow YAML update failed with HTTP ${createLike.status}`);
5539
+ }
5540
+ return { workflow: createLike.data.workflow, validation: createLike.data.validation };
5541
+ }
5044
5542
  async getWorkflow(workflowId) {
5045
5543
  this.requireSession();
5046
5544
  const response = await this.http.get(
@@ -5099,12 +5597,15 @@ var OpenMatesClient = class _OpenMatesClient {
5099
5597
  async disableWorkflow(workflowId) {
5100
5598
  return this.setWorkflowEnabled(workflowId, false);
5101
5599
  }
5102
- async runWorkflow(workflowId, params = {}) {
5600
+ async runWorkflow(workflowId, params) {
5103
5601
  this.requireSession();
5602
+ if (!params.idempotencyKey.trim()) {
5603
+ throw new Error("Workflow run requires a stable idempotencyKey");
5604
+ }
5104
5605
  const response = await this.http.post(
5105
5606
  `/v1/workflows/${encodeURIComponent(workflowId)}/run`,
5106
5607
  { mode: params.mode ?? "manual", input: params.input ?? {} },
5107
- this.getCliRequestHeaders()
5608
+ { ...this.getCliRequestHeaders(), "Idempotency-Key": params.idempotencyKey }
5108
5609
  );
5109
5610
  if (!response.ok || !response.data.run) {
5110
5611
  throw new Error(`Workflow run failed with HTTP ${response.status}`);
@@ -5133,6 +5634,42 @@ var OpenMatesClient = class _OpenMatesClient {
5133
5634
  }
5134
5635
  return response.data.run;
5135
5636
  }
5637
+ async cancelWorkflowRun(workflowId, runId) {
5638
+ this.requireSession();
5639
+ const response = await this.http.post(
5640
+ `/v1/workflows/${encodeURIComponent(workflowId)}/runs/${encodeURIComponent(runId)}/cancel`,
5641
+ {},
5642
+ this.getCliRequestHeaders()
5643
+ );
5644
+ if (!response.ok || response.data.status !== "cancellation_requested" && response.data.status !== "cancelled") {
5645
+ throw new Error(`Workflow run cancellation failed with HTTP ${response.status}`);
5646
+ }
5647
+ return response.data;
5648
+ }
5649
+ async testWorkflowStep(workflowId, stepId, params = {}) {
5650
+ this.requireSession();
5651
+ const response = await this.http.post(
5652
+ `/v1/workflows/${encodeURIComponent(workflowId)}/steps/${encodeURIComponent(stepId)}/test`,
5653
+ { input: params.input ?? {}, confirmed: params.confirmed === true },
5654
+ this.getCliRequestHeaders()
5655
+ );
5656
+ if (!response.ok || !response.data.run) {
5657
+ throw new Error(`Workflow step test failed with HTTP ${response.status}`);
5658
+ }
5659
+ return response.data.run;
5660
+ }
5661
+ async respondToWorkflowRun(workflowId, runId, stepId, input) {
5662
+ this.requireSession();
5663
+ const response = await this.http.post(
5664
+ `/v1/workflows/${encodeURIComponent(workflowId)}/runs/${encodeURIComponent(runId)}/respond`,
5665
+ { step_id: stepId, input },
5666
+ this.getCliRequestHeaders()
5667
+ );
5668
+ if (!response.ok || !response.data.run) {
5669
+ throw new Error(`Workflow response failed with HTTP ${response.status}`);
5670
+ }
5671
+ return response.data.run;
5672
+ }
5136
5673
  async listWorkflowCapabilities() {
5137
5674
  this.requireSession();
5138
5675
  const response = await this.http.get(
@@ -5144,6 +5681,114 @@ var OpenMatesClient = class _OpenMatesClient {
5144
5681
  }
5145
5682
  return response.data.capabilities ?? [];
5146
5683
  }
5684
+ async upsertWorkflowTemplateProjection(workflowId, params) {
5685
+ this.requireSession();
5686
+ const response = await this.http.put(
5687
+ `/v1/workflows/${encodeURIComponent(workflowId)}/template-projection`,
5688
+ {
5689
+ template_id: params.templateId,
5690
+ source_version: params.sourceVersion,
5691
+ ciphertext: params.ciphertext,
5692
+ ciphertext_checksum: params.ciphertextChecksum,
5693
+ owner_wrapped_key: params.ownerWrappedKey,
5694
+ projection_schema_version: params.projectionSchemaVersion
5695
+ },
5696
+ this.getCliRequestHeaders()
5697
+ );
5698
+ if (!response.ok) {
5699
+ throw new Error(`Workflow template projection upsert failed with HTTP ${response.status}`);
5700
+ }
5701
+ return response.data;
5702
+ }
5703
+ async getPublicWorkflowTemplateProjection(templateId) {
5704
+ const response = await this.http.get(
5705
+ `/v1/workflows/template-projections/${encodeURIComponent(templateId)}`,
5706
+ this.getCliRequestHeaders()
5707
+ );
5708
+ if (!response.ok) {
5709
+ throw new Error(`Workflow template projection retrieval failed with HTTP ${response.status}`);
5710
+ }
5711
+ return response.data;
5712
+ }
5713
+ async revokeWorkflowTemplateProjection(workflowId) {
5714
+ this.requireSession();
5715
+ const response = await this.http.post(
5716
+ `/v1/workflows/${encodeURIComponent(workflowId)}/template-projection/revoke`,
5717
+ {},
5718
+ this.getCliRequestHeaders()
5719
+ );
5720
+ if (!response.ok) {
5721
+ throw new Error(`Workflow template projection revoke failed with HTTP ${response.status}`);
5722
+ }
5723
+ return response.data;
5724
+ }
5725
+ async unrevokeWorkflowTemplateProjection(workflowId) {
5726
+ this.requireSession();
5727
+ const response = await this.http.post(
5728
+ `/v1/workflows/${encodeURIComponent(workflowId)}/template-projection/unrevoke`,
5729
+ {},
5730
+ this.getCliRequestHeaders()
5731
+ );
5732
+ if (!response.ok) {
5733
+ throw new Error(`Workflow template projection unrevoke failed with HTTP ${response.status}`);
5734
+ }
5735
+ return response.data;
5736
+ }
5737
+ async completeImportedWorkflowBinding(workflowId, params) {
5738
+ this.requireSession();
5739
+ const response = await this.http.post(
5740
+ `/v1/workflows/${encodeURIComponent(workflowId)}/binding-requirements/complete`,
5741
+ { type: params.type, node_id: params.nodeId },
5742
+ this.getCliRequestHeaders()
5743
+ );
5744
+ if (!response.ok) {
5745
+ throw new Error(`Workflow imported binding completion failed with HTTP ${response.status}`);
5746
+ }
5747
+ return response.data;
5748
+ }
5749
+ async createWorkflowTemplateShortUrl(params) {
5750
+ this.requireSession();
5751
+ const response = await this.http.post(
5752
+ "/v1/share/short-url",
5753
+ {
5754
+ token: params.token,
5755
+ encrypted_url: params.encryptedUrl,
5756
+ content_type: "workflow_template",
5757
+ content_id: params.templateId,
5758
+ password_protected: params.passwordProtected ?? false,
5759
+ ...params.ttlSeconds !== void 0 ? { ttl_seconds: params.ttlSeconds } : {}
5760
+ },
5761
+ this.getCliRequestHeaders()
5762
+ );
5763
+ if (!response.ok) {
5764
+ throw new Error(`Workflow template short URL create failed with HTTP ${response.status}`);
5765
+ }
5766
+ return response.data;
5767
+ }
5768
+ async revokeShortUrl(token) {
5769
+ this.requireSession();
5770
+ const response = await this.http.delete(
5771
+ `/v1/share/short-url/${encodeURIComponent(token)}`,
5772
+ void 0,
5773
+ this.getCliRequestHeaders()
5774
+ );
5775
+ if (!response.ok) {
5776
+ throw new Error(`Short URL revoke failed with HTTP ${response.status}`);
5777
+ }
5778
+ return response.data;
5779
+ }
5780
+ async importWorkflowTemplate(payload) {
5781
+ this.requireSession();
5782
+ const response = await this.http.post(
5783
+ "/v1/workflows/template-import",
5784
+ payload,
5785
+ this.getCliRequestHeaders()
5786
+ );
5787
+ if (!response.ok || !response.data.workflow) {
5788
+ throw new Error(`Workflow template import failed with HTTP ${response.status}`);
5789
+ }
5790
+ return response.data.workflow;
5791
+ }
5147
5792
  async startWorkflowInput(params) {
5148
5793
  this.requireSession();
5149
5794
  const response = await this.http.post(
@@ -5314,6 +5959,54 @@ var OpenMatesClient = class _OpenMatesClient {
5314
5959
  }
5315
5960
  return response.data.task;
5316
5961
  }
5962
+ async deleteUserTask(taskId) {
5963
+ this.requireSession();
5964
+ const response = await this.http.delete(
5965
+ `/v1/user-tasks/${encodeURIComponent(taskId)}`,
5966
+ void 0,
5967
+ this.getCliRequestHeaders()
5968
+ );
5969
+ if (!response.ok) {
5970
+ throw new Error(`User task delete failed with HTTP ${response.status}`);
5971
+ }
5972
+ return response.data;
5973
+ }
5974
+ async completeUserTask(taskId, input = {}) {
5975
+ return this.postUserTaskAction(taskId, "complete", input);
5976
+ }
5977
+ async blockUserTask(taskId, input = {}) {
5978
+ return this.postUserTaskAction(taskId, "block", input);
5979
+ }
5980
+ async unblockUserTask(taskId, input = {}) {
5981
+ return this.postUserTaskAction(taskId, "unblock", input);
5982
+ }
5983
+ async skipUserTask(taskId, input = {}) {
5984
+ return this.postUserTaskAction(taskId, "skip", input);
5985
+ }
5986
+ async reorderUserTasks(input) {
5987
+ this.requireSession();
5988
+ const response = await this.http.post(
5989
+ "/v1/user-tasks/reorder",
5990
+ input,
5991
+ this.getCliRequestHeaders()
5992
+ );
5993
+ if (!response.ok) {
5994
+ throw new Error(`User task reorder failed with HTTP ${response.status}`);
5995
+ }
5996
+ return response.data.tasks ?? [];
5997
+ }
5998
+ async postUserTaskAction(taskId, action, input = {}) {
5999
+ this.requireSession();
6000
+ const response = await this.http.post(
6001
+ `/v1/user-tasks/${encodeURIComponent(taskId)}/${encodeURIComponent(action)}`,
6002
+ input,
6003
+ this.getCliRequestHeaders()
6004
+ );
6005
+ if (!response.ok || !response.data.task) {
6006
+ throw new Error(`User task ${action} failed with HTTP ${response.status}`);
6007
+ }
6008
+ return response.data.task;
6009
+ }
5317
6010
  // -------------------------------------------------------------------------
5318
6011
  // User plans
5319
6012
  // -------------------------------------------------------------------------
@@ -6194,7 +6887,7 @@ Required: ${schema.required.join(", ")}`
6194
6887
  const session = this.requireSession();
6195
6888
  const masterKey = base64ToBytes(session.masterKeyExportedB64);
6196
6889
  const cache = await this.ensureSynced();
6197
- const { createHash: createHash7 } = await import("crypto");
6890
+ const { createHash: createHash8 } = await import("crypto");
6198
6891
  const embed = cache.embeds.find(
6199
6892
  (e) => String(e.embed_id ?? "").startsWith(embedIdOrShort) || String(e.id ?? "").startsWith(embedIdOrShort)
6200
6893
  );
@@ -6202,7 +6895,7 @@ Required: ${schema.required.join(", ")}`
6202
6895
  throw new Error(`Embed '${embedIdOrShort}' not found in local cache.`);
6203
6896
  }
6204
6897
  const embedId = String(embed.embed_id ?? embed.id ?? "");
6205
- const hashedEmbedId = createHash7("sha256").update(embedId).digest("hex");
6898
+ const hashedEmbedId = createHash8("sha256").update(embedId).digest("hex");
6206
6899
  const embedKeyBytes = await this.resolveEmbedKey(
6207
6900
  cache,
6208
6901
  masterKey,
@@ -6262,8 +6955,8 @@ Required: ${schema.required.join(", ")}`
6262
6955
  if (!embed) {
6263
6956
  throw new Error(`Embed '${embedId}' not found in local cache. Run 'openmates chats list' to sync first.`);
6264
6957
  }
6265
- const { createHash: createHash7 } = await import("crypto");
6266
- const hashedEmbedId = createHash7("sha256").update(embedId).digest("hex");
6958
+ const { createHash: createHash8 } = await import("crypto");
6959
+ const hashedEmbedId = createHash8("sha256").update(embedId).digest("hex");
6267
6960
  const embedKey = await this.resolveEmbedKey(
6268
6961
  cache,
6269
6962
  masterKey,
@@ -6357,8 +7050,8 @@ Required: ${schema.required.join(", ")}`
6357
7050
  if (!embed) {
6358
7051
  throw new Error(`Embed '${embedId}' not found in local cache. Run 'openmates chats list' to sync first.`);
6359
7052
  }
6360
- const { createHash: createHash7 } = await import("crypto");
6361
- const hashedEmbedId = createHash7("sha256").update(embedId).digest("hex");
7053
+ const { createHash: createHash8 } = await import("crypto");
7054
+ const hashedEmbedId = createHash8("sha256").update(embedId).digest("hex");
6362
7055
  const embedKey = await this.resolveEmbedKey(
6363
7056
  cache,
6364
7057
  masterKey,
@@ -6550,11 +7243,11 @@ Required: ${schema.required.join(", ")}`
6550
7243
  * so every WebSocket usage gets a fresh HMAC token automatically.
6551
7244
  */
6552
7245
  async openWsClient() {
6553
- await this.refreshWsToken();
7246
+ const ownerId = await this.refreshWsToken();
6554
7247
  const session = this.requireSession();
6555
7248
  const ws = this.makeWsClient(session);
6556
7249
  await ws.open();
6557
- return { ws, session };
7250
+ return { ws, session, ownerId };
6558
7251
  }
6559
7252
  /**
6560
7253
  * Refresh the ws_token by calling /auth/session.
@@ -6571,7 +7264,9 @@ Required: ${schema.required.join(", ")}`
6571
7264
  }
6572
7265
  session.cookies = this.http.getCookieMap();
6573
7266
  saveSession(session);
7267
+ return typeof res.data.user?.id === "string" ? res.data.user.id : typeof res.data.user?.user_id === "string" ? res.data.user.user_id : null;
6574
7268
  } catch {
7269
+ return null;
6575
7270
  }
6576
7271
  }
6577
7272
  /**
@@ -6616,6 +7311,7 @@ Required: ${schema.required.join(", ")}`
6616
7311
  const embedKeys = [];
6617
7312
  let newChatSuggestions = [];
6618
7313
  let totalChatCount = 0;
7314
+ let reconciliation = { authoritative: false };
6619
7315
  const pendingAIResponses = [];
6620
7316
  try {
6621
7317
  ws.send("phased_sync_request", {
@@ -6642,6 +7338,11 @@ Required: ${schema.required.join(", ")}`
6642
7338
  } else if (frame.type === "phase_2_last_20_chats_ready") {
6643
7339
  const p = frame.payload;
6644
7340
  totalChatCount = p.total_chat_count ?? 0;
7341
+ reconciliation = {
7342
+ authoritative: p.authoritative === true,
7343
+ authoritative_chat_ids: p.authoritative_chat_ids,
7344
+ deleted_chat_ids: p.deleted_chat_ids
7345
+ };
6645
7346
  for (const wrapper of p.chats ?? []) {
6646
7347
  const details = wrapper.chat_details;
6647
7348
  if (!details || typeof details.id !== "string") continue;
@@ -6721,12 +7422,21 @@ Required: ${schema.required.join(", ")}`
6721
7422
  }
6722
7423
  }
6723
7424
  }
7425
+ const reconciledChats = reconcileAuthoritativeChats(chats, reconciliation);
7426
+ chats.splice(0, chats.length, ...reconciledChats);
7427
+ if (reconciliation.deleted_chat_ids?.length) {
7428
+ const { createHash: createHash8 } = await import("crypto");
7429
+ const deletedHashes = new Set(
7430
+ reconciliation.deleted_chat_ids.map((id) => createHash8("sha256").update(id).digest("hex"))
7431
+ );
7432
+ const keptEmbeds = embeds.filter((embed) => !deletedHashes.has(String(embed.hashed_chat_id ?? "")));
7433
+ const keptKeys = embedKeys.filter((key) => !deletedHashes.has(String(key.hashed_chat_id ?? "")));
7434
+ embeds.splice(0, embeds.length, ...keptEmbeds);
7435
+ embedKeys.splice(0, embedKeys.length, ...keptKeys);
7436
+ }
6724
7437
  chats.sort(
6725
7438
  (a, b) => (typeof b.details.last_edited_overall_timestamp === "number" ? b.details.last_edited_overall_timestamp : 0) - (typeof a.details.last_edited_overall_timestamp === "number" ? a.details.last_edited_overall_timestamp : 0)
6726
7439
  );
6727
- if (totalChatCount > 0 && chats.length > totalChatCount) {
6728
- chats.length = totalChatCount;
6729
- }
6730
7440
  try {
6731
7441
  await this.persistPendingAIResponsesFromSync(ws, chats, pendingAIResponses);
6732
7442
  } finally {
@@ -6979,10 +7689,10 @@ function printLogo() {
6979
7689
 
6980
7690
  // src/cli.ts
6981
7691
  import { createInterface as createInterface4 } from "readline/promises";
6982
- import { realpathSync, writeFileSync as writeFileSync6 } from "fs";
7692
+ import { readFileSync as readFileSync9, realpathSync, writeFileSync as writeFileSync6 } from "fs";
6983
7693
  import { fileURLToPath as fileURLToPath2 } from "url";
6984
7694
  import { basename as basename3, dirname as dirname3 } from "path";
6985
- import { randomUUID as randomUUID5 } from "crypto";
7695
+ import { randomUUID as randomUUID6 } from "crypto";
6986
7696
  import WebSocket2 from "ws";
6987
7697
 
6988
7698
  // ../secret-scanner/src/registry.ts
@@ -9534,6 +10244,22 @@ import { createInterface as createPromptInterface } from "readline/promises";
9534
10244
  import { homedir as homedir5 } from "os";
9535
10245
  import { dirname, join as join3, resolve as resolve3 } from "path";
9536
10246
 
10247
+ // src/branding.ts
10248
+ var OPENMATES_WORDMARK = "OPENMATES";
10249
+ var OPENMATES_ASCII_LARGE = [
10250
+ " \u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588 \u2588\u2588 \u2588\u2588\u2588 \u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588\u2588",
10251
+ "\u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588\u2588\u2588 \u2588\u2588 \u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 ",
10252
+ "\u2588\u2588 \u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588\u2588\u2588 \u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588 \u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588\u2588",
10253
+ "\u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588",
10254
+ " \u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588 \u2588\u2588\u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588\u2588"
10255
+ ];
10256
+ function openMatesAsciiLogo(width = 100) {
10257
+ return width >= 86 ? [...OPENMATES_ASCII_LARGE, OPENMATES_WORDMARK] : [OPENMATES_WORDMARK];
10258
+ }
10259
+ function renderOpenMatesAsciiLogo(width = 100) {
10260
+ return openMatesAsciiLogo(width).join("\n");
10261
+ }
10262
+
9537
10263
  // src/support.ts
9538
10264
  var SUPPORT_URL = "https://openmates.org/#settings/support";
9539
10265
  var SUPPORT_TITLE = "Support OpenMates development";
@@ -10446,6 +11172,38 @@ function missingRequiredEnvKeys(installPath, role) {
10446
11172
  const env = readEnvMap(installPath);
10447
11173
  return requiredRuntimeEnvKeys(role).filter((key) => !env[key]);
10448
11174
  }
11175
+ var LONG_RUNNING_SERVER_COMMANDS = /* @__PURE__ */ new Set([
11176
+ "install",
11177
+ "start",
11178
+ "stop",
11179
+ "restart",
11180
+ "update",
11181
+ "preflight",
11182
+ "caddy",
11183
+ "backup",
11184
+ "restore",
11185
+ "reset",
11186
+ "make-admin",
11187
+ "ai",
11188
+ "uninstall"
11189
+ ]);
11190
+ function shouldPrintServerCommandStart(subcommand, rest, flags) {
11191
+ if (flags.json === true || !LONG_RUNNING_SERVER_COMMANDS.has(subcommand)) return false;
11192
+ if (subcommand === "backup" && rest[0] === "list") return false;
11193
+ if (subcommand === "update" && rest[0] === "status") return false;
11194
+ if (subcommand === "ai" && rest[0] === "models" && (rest[1] === "list" || rest[1] === "remove")) return false;
11195
+ if (subcommand === "caddy" && (rest[0] === "status" || rest[0] === "diff")) return false;
11196
+ return true;
11197
+ }
11198
+ function printServerCommandStart(subcommand, rest, flags) {
11199
+ const command = ["server", subcommand, ...rest].join(" ");
11200
+ console.log(renderOpenMatesAsciiLogo());
11201
+ console.log("");
11202
+ console.log(`Running OpenMates ${command}...`);
11203
+ if (typeof flags.path === "string") console.log(`Path: ${resolve3(flags.path)}`);
11204
+ if (typeof flags.role === "string") console.log(`Role: ${flags.role}`);
11205
+ console.log("");
11206
+ }
10449
11207
  function vaultCheckContainerForRole(role) {
10450
11208
  if (role === "core") return "api";
10451
11209
  if (role === "upload") return "app-uploads";
@@ -11966,6 +12724,9 @@ async function handleServer(subcommand, rest, flags) {
11966
12724
  printServerHelp();
11967
12725
  return;
11968
12726
  }
12727
+ if (shouldPrintServerCommandStart(subcommand, rest, flags)) {
12728
+ printServerCommandStart(subcommand, rest, flags);
12729
+ }
11969
12730
  switch (subcommand) {
11970
12731
  case "status":
11971
12732
  return serverStatus(flags);
@@ -28546,6 +29307,11 @@ Only output the final Markdown table. Do NOT include explanations, notes, or any
28546
29307
  }
28547
29308
  }
28548
29309
  },
29310
+ models3d: {
29311
+ generate: {
29312
+ text: "Generate 3D model"
29313
+ }
29314
+ },
28549
29315
  music: {
28550
29316
  generate: {
28551
29317
  text: "Generate",
@@ -30885,6 +31651,27 @@ Only output the final Markdown table. Do NOT include explanations, notes, or any
30885
31651
  save: {
30886
31652
  text: "Save"
30887
31653
  },
31654
+ detail_title_hint: {
31655
+ text: "Enter to save \xB7 Escape to cancel"
31656
+ },
31657
+ detail_description_hint: {
31658
+ text: "Ctrl/Command+Enter to save \xB7 Escape to cancel"
31659
+ },
31660
+ detail_save_error: {
31661
+ text: "Your changes were not saved."
31662
+ },
31663
+ detail_loading: {
31664
+ text: "Loading {item}\u2026"
31665
+ },
31666
+ detail_load_error: {
31667
+ text: "{item} could not be loaded."
31668
+ },
31669
+ detail_untitled: {
31670
+ text: "Untitled {item}"
31671
+ },
31672
+ detail_items_count: {
31673
+ text: "{count} items"
31674
+ },
30888
31675
  settings: {
30889
31676
  text: "Settings"
30890
31677
  },
@@ -36515,6 +37302,22 @@ As of mid-2026, the severe supply shocks from the 2024\u20132025 avian flu have
36515
37302
  },
36516
37303
  privacy: {
36517
37304
  providers: {
37305
+ model_generation: {
37306
+ heading: {
37307
+ text: "3D model generation (only when you use a 3D model skill)"
37308
+ },
37309
+ description: {
37310
+ text: "This provider is only used when you invoke a 3D model generation skill. If you never use the 3D models app, none of your data is shared with it."
37311
+ },
37312
+ hi3d: {
37313
+ heading: {
37314
+ text: "Hi3D (3D model generation)"
37315
+ },
37316
+ description: {
37317
+ text: "Hi3D receives reference images you supply or images generated from your text prompt, plus generation settings. Its Terms license Inputs and Outputs for AI model improvement and training. Hi3D publishes no exact API retention period or per-task deletion endpoint; its policy provides account and personal-data deletion rights. OpenMates requires your confirmation before first use."
37318
+ }
37319
+ }
37320
+ },
36518
37321
  fitness: {
36519
37322
  heading: {
36520
37323
  text: "Fitness search (only when you use the fitness skill)"
@@ -40605,6 +41408,42 @@ As of mid-2026, the severe supply shocks from the 2024\u20132025 avian flu have
40605
41408
  description: {
40606
41409
  text: "Manage your credits, purchases, and what you used credits for."
40607
41410
  },
41411
+ apple_support: {
41412
+ text: "Support OpenMates"
41413
+ },
41414
+ apple_usage_details: {
41415
+ text: "View usage details"
41416
+ },
41417
+ apple_purchased_gift_cards: {
41418
+ text: "Purchased"
41419
+ },
41420
+ apple_no_gift_cards: {
41421
+ text: "No gift cards yet"
41422
+ },
41423
+ apple_gift_card_purchase_unavailable: {
41424
+ text: "Gift card purchase is unavailable until your account email is fully synchronized."
41425
+ },
41426
+ apple_support_amount: {
41427
+ text: "Amount in EUR"
41428
+ },
41429
+ apple_create_bank_transfer: {
41430
+ text: "Create bank transfer"
41431
+ },
41432
+ apple_invalid_support_amount: {
41433
+ text: "Enter a valid amount and make sure your account has an email address."
41434
+ },
41435
+ apple_fulfillment_delayed: {
41436
+ text: "Your purchase is awaiting fulfillment. It remains retryable and will not be marked complete yet."
41437
+ },
41438
+ apple_product_not_found: {
41439
+ text: "This product is not available in the App Store."
41440
+ },
41441
+ apple_credits_count: {
41442
+ text: "{credits} credits"
41443
+ },
41444
+ apple_bank_transfer_reference: {
41445
+ text: "Bank transfer reference: {reference}"
41446
+ },
40608
41447
  text: "Billing & Usage",
40609
41448
  title: {
40610
41449
  text: "Billing & Credits"
@@ -41385,6 +42224,12 @@ As of mid-2026, the severe supply shocks from the 2024\u20132025 avian flu have
41385
42224
  learning_mode_disabled: {
41386
42225
  text: "Learning Mode disabled."
41387
42226
  },
42227
+ learning_mode_attempts_remaining: {
42228
+ text: "{count} attempts remaining."
42229
+ },
42230
+ learning_mode_locked: {
42231
+ text: "Deactivation is locked for 24 hours after too many incorrect attempts."
42232
+ },
41388
42233
  learning_mode_age_group_prompt: {
41389
42234
  text: "Learner age group: under_10, 10_12, 13_15, 16_18, or adult"
41390
42235
  },
@@ -42629,6 +43474,73 @@ As of mid-2026, the severe supply shocks from the 2024\u20132025 avian flu have
42629
43474
  },
42630
43475
  share_debug_logs_admin_notice: {
42631
43476
  text: "You are an admin. Therefore your debug logs are automatically shared already. Regular users can turn on/off debug logs sharing here."
43477
+ },
43478
+ native: {
43479
+ save_error: {
43480
+ text: "Could not save this privacy setting. Please try again."
43481
+ },
43482
+ master_key_unavailable: {
43483
+ text: "Your encryption key is unavailable. Sign in again to manage this setting."
43484
+ },
43485
+ debug_session: {
43486
+ description: {
43487
+ text: "Temporarily share sanitized native diagnostics with support. Message content, encryption keys, credentials, file names, and personal data are never shared."
43488
+ },
43489
+ duration: {
43490
+ text: "Duration"
43491
+ },
43492
+ start: {
43493
+ text: "Start sharing debug logs"
43494
+ },
43495
+ activating: {
43496
+ text: "Activating..."
43497
+ },
43498
+ active: {
43499
+ text: "Active debug session"
43500
+ },
43501
+ id: {
43502
+ text: "Your Debug ID"
43503
+ },
43504
+ share_hint: {
43505
+ text: "Share this ID with the support team so they can find your sanitized logs."
43506
+ },
43507
+ stop: {
43508
+ text: "Stop sharing"
43509
+ },
43510
+ stopping: {
43511
+ text: "Stopping..."
43512
+ },
43513
+ no_expiry: {
43514
+ text: "Active until manually stopped"
43515
+ },
43516
+ error: {
43517
+ text: "Could not update the debug sharing session. Please try again."
43518
+ },
43519
+ duration_5m: {
43520
+ text: "5 minutes"
43521
+ },
43522
+ duration_1h: {
43523
+ text: "1 hour"
43524
+ },
43525
+ duration_3d: {
43526
+ text: "3 days"
43527
+ },
43528
+ duration_7d: {
43529
+ text: "7 days"
43530
+ },
43531
+ duration_none: {
43532
+ text: "No time limit"
43533
+ },
43534
+ minutes_remaining: {
43535
+ text: "{count}m remaining"
43536
+ },
43537
+ hours_remaining: {
43538
+ text: "{count}h remaining"
43539
+ },
43540
+ days_remaining: {
43541
+ text: "{count}d remaining"
43542
+ }
43543
+ }
42632
43544
  }
42633
43545
  },
42634
43546
  terms_and_conditions: {
@@ -44439,6 +45351,21 @@ As of mid-2026, the severe supply shocks from the 2024\u20132025 avian flu have
44439
45351
  pair_server_use_custom: {
44440
45352
  text: "Use custom"
44441
45353
  },
45354
+ pair_self_hosted_edition: {
45355
+ text: "Connect self-hosted edition"
45356
+ },
45357
+ pair_self_hosted_placeholder: {
45358
+ text: "Server URL"
45359
+ },
45360
+ pair_self_hosted_connect: {
45361
+ text: "Connect"
45362
+ },
45363
+ pair_self_hosted_invalid_url: {
45364
+ text: "Enter a valid HTTPS server URL."
45365
+ },
45366
+ pair_use_production: {
45367
+ text: "Use OpenMates.org"
45368
+ },
44442
45369
  pair_connect_apple_watch_title: {
44443
45370
  text: "Connect Apple Watch"
44444
45371
  },
@@ -46868,22 +47795,6 @@ var TuiTerminal = class {
46868
47795
  }
46869
47796
  };
46870
47797
 
46871
- // src/branding.ts
46872
- var OPENMATES_WORDMARK = "OPENMATES";
46873
- var OPENMATES_ASCII_LARGE = [
46874
- " \u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588 \u2588\u2588 \u2588\u2588\u2588 \u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588\u2588",
46875
- "\u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588\u2588\u2588 \u2588\u2588 \u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 ",
46876
- "\u2588\u2588 \u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588\u2588\u2588 \u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588 \u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588\u2588",
46877
- "\u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588",
46878
- " \u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588 \u2588\u2588\u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588\u2588"
46879
- ];
46880
- function openMatesAsciiLogo(width = 100) {
46881
- return width >= 86 ? [...OPENMATES_ASCII_LARGE, OPENMATES_WORDMARK] : [OPENMATES_WORDMARK];
46882
- }
46883
- function renderOpenMatesAsciiLogo(width = 100) {
46884
- return openMatesAsciiLogo(width).join("\n");
46885
- }
46886
-
46887
47798
  // src/tuiRenderer.ts
46888
47799
  var MIN_WIDTH = 48;
46889
47800
  var CONTENT_PREVIEW_LINES = 12;
@@ -46910,6 +47821,17 @@ function createInitialTuiState() {
46910
47821
  selectedInterests: [],
46911
47822
  examples: [],
46912
47823
  activeExample: null,
47824
+ workflows: [],
47825
+ activeWorkflow: null,
47826
+ workflowRuns: [],
47827
+ tasks: [],
47828
+ activeTask: null,
47829
+ workflowTab: "graph",
47830
+ selectedWorkflowNodeIndex: 0,
47831
+ selectedWorkflowRunIndex: 0,
47832
+ expandedWorkflowNodeId: null,
47833
+ expandedWorkflowRunNodeId: null,
47834
+ workflowEdit: null,
46913
47835
  messages: [],
46914
47836
  status: null,
46915
47837
  isBusy: false
@@ -46943,6 +47865,7 @@ Files:
46943
47865
 
46944
47866
  More:
46945
47867
  openmates apps list
47868
+ openmates workflows list
46946
47869
  openmates mentions list
46947
47870
  openmates embeds show <embed-id>
46948
47871
  openmates help
@@ -46965,7 +47888,7 @@ function renderTuiFrame(state, width, height) {
46965
47888
  const contentHeight = safeHeight - 4;
46966
47889
  const body = renderBody(state, innerWidth);
46967
47890
  const visible = sliceForScroll(body, contentHeight, state.scrollOffset, state.screen === "chat");
46968
- const inputLine = state.screen === "interests" || state.screen === "examples" ? renderHintLine(state, innerWidth) : `> ${state.input || inputPlaceholder(state)}`;
47891
+ const inputLine = state.screen === "interests" || state.screen === "examples" || state.screen === "workflows" || state.screen === "workflow" || state.screen === "tasks" || state.screen === "task" ? renderHintLine(state, innerWidth) : `> ${state.input || inputPlaceholder(state)}`;
46969
47892
  const lines = [
46970
47893
  header2,
46971
47894
  ...padLines(visible, contentHeight, innerWidth).map((line) => boxed(line, innerWidth)),
@@ -46991,6 +47914,14 @@ function renderBody(state, width) {
46991
47914
  return renderStatus(state, width);
46992
47915
  case "embed":
46993
47916
  return renderStatus(state, width);
47917
+ case "workflows":
47918
+ return renderWorkflows(state, width);
47919
+ case "workflow":
47920
+ return renderWorkflowDetail(state, width);
47921
+ case "tasks":
47922
+ return renderTasks(state, width);
47923
+ case "task":
47924
+ return renderTaskDetail(state, width);
46994
47925
  case "start":
46995
47926
  default:
46996
47927
  return renderStart(width);
@@ -47012,7 +47943,7 @@ function renderStart(width) {
47012
47943
  "Type @./notes.md, @~/Downloads/report.pdf, or @src/app.ts in your message.",
47013
47944
  "Images, PDFs, audio, and code files are attached as encrypted embeds when you are signed in.",
47014
47945
  "",
47015
- "Shortcuts: /help /login /signup /examples /exit"
47946
+ "Shortcuts: /help /login /signup /examples /tasks /exit"
47016
47947
  ].flatMap((line) => wrap(line, width));
47017
47948
  }
47018
47949
  function renderHelp(width) {
@@ -47032,6 +47963,9 @@ function renderHelp(width) {
47032
47963
  "",
47033
47964
  "Commands",
47034
47965
  " /examples Choose example chats",
47966
+ " /workflows List and run saved workflows",
47967
+ " /workflow <id> Open a workflow by ID",
47968
+ " /tasks Open your task workspace",
47035
47969
  " /login Pair-auth login",
47036
47970
  " /signup Leave TUI and run guided signup",
47037
47971
  " /embed <id> Open an embed detail view",
@@ -47040,6 +47974,150 @@ function renderHelp(width) {
47040
47974
  "Outside TUI: openmates --help, openmates chats --help, openmates apps --help"
47041
47975
  ].flatMap((line) => wrap(line, width));
47042
47976
  }
47977
+ function renderTasks(state, width) {
47978
+ const lines = ["Tasks", ""];
47979
+ if (state.tasks.length === 0) {
47980
+ lines.push("No tasks found.", "Create one outside TUI with: openmates tasks create --title <title>");
47981
+ return lines.flatMap((line) => wrap(line, width));
47982
+ }
47983
+ const visibleCount = Math.max(1, CONTENT_PREVIEW_LINES);
47984
+ const start = Math.max(0, Math.min(state.selectedIndex, state.tasks.length - visibleCount));
47985
+ for (let i = 0; i < state.tasks.slice(start, start + visibleCount).length; i += 1) {
47986
+ const absoluteIndex = start + i;
47987
+ const task = state.tasks[absoluteIndex];
47988
+ const cursor = absoluteIndex === state.selectedIndex ? ">" : " ";
47989
+ const assignee = task.assigneeType === "ai" ? "OpenMates" : task.assigneeHash ?? "user";
47990
+ lines.push(`${cursor} ${task.shortId} ${task.status} ${assignee} ${task.title}`);
47991
+ if (task.queueState !== "none") lines.push(` queue: ${task.queueState}`);
47992
+ if (task.description) lines.push(` ${task.description}`);
47993
+ lines.push("");
47994
+ }
47995
+ return lines.flatMap((line) => wrap(line, width));
47996
+ }
47997
+ function renderTaskDetail(state, width) {
47998
+ const task = state.activeTask;
47999
+ if (!task) return renderTasks(state, width);
48000
+ const assignee = task.assigneeType === "ai" ? "OpenMates" : task.assigneeHash ?? "user";
48001
+ const lines = [
48002
+ `Task: ${task.shortId}`,
48003
+ `Title: ${task.title}`,
48004
+ `Status: ${task.status}`,
48005
+ `Assignee: ${assignee}`,
48006
+ `Queue: ${task.queueState}`,
48007
+ `ID: ${task.taskId}`,
48008
+ task.description ? `Description: ${task.description}` : null,
48009
+ task.primaryChatId ? `Chat: ${task.primaryChatId}` : null,
48010
+ task.linkedProjectIds.length > 0 ? `Projects: ${task.linkedProjectIds.join(", ")}` : null,
48011
+ task.blockedReasonCode ? `Blocked reason: ${task.blockedReasonCode}` : null,
48012
+ task.aiExecutionState ? `AI state: ${task.aiExecutionState}` : null,
48013
+ "",
48014
+ "Actions: c create, e edit, x delete, r reorder, s start, d done, b block, u unblock, k skip, Esc back"
48015
+ ].filter((line) => line !== null);
48016
+ return lines.flatMap((line) => wrap(line, width));
48017
+ }
48018
+ function renderWorkflows(state, width) {
48019
+ const lines = ["Workflows", ""];
48020
+ if (state.workflows.length === 0) {
48021
+ lines.push("No workflows found.", "Create one outside TUI with: openmates workflows create --file workflow.yml");
48022
+ return lines.flatMap((line) => wrap(line, width));
48023
+ }
48024
+ const visibleCount = Math.max(1, CONTENT_PREVIEW_LINES);
48025
+ const start = Math.max(0, Math.min(state.selectedIndex, state.workflows.length - visibleCount));
48026
+ for (let i = 0; i < state.workflows.slice(start, start + visibleCount).length; i += 1) {
48027
+ const absoluteIndex = start + i;
48028
+ const workflow = state.workflows[absoluteIndex];
48029
+ const cursor = absoluteIndex === state.selectedIndex ? ">" : " ";
48030
+ const status = workflow.enabled ? "enabled" : "disabled";
48031
+ const lastRun = workflow.last_run_status ? ` last: ${workflow.last_run_status}` : "";
48032
+ lines.push(`${cursor} ${workflow.title} (${status})${lastRun}`);
48033
+ lines.push(` ${workflow.id}`);
48034
+ if (workflow.trigger_summary) lines.push(` ${workflow.trigger_summary}`);
48035
+ lines.push("");
48036
+ }
48037
+ return lines.flatMap((line) => wrap(line, width));
48038
+ }
48039
+ function renderWorkflowDetail(state, width) {
48040
+ const workflow = state.activeWorkflow;
48041
+ if (!workflow) return renderWorkflows(state, width);
48042
+ const graphNodes = workflow.graph?.nodes ?? [];
48043
+ const selectedRun = state.workflowRuns[state.selectedWorkflowRunIndex] ?? null;
48044
+ const lines = [
48045
+ `Workflow: ${workflow.title}`,
48046
+ `ID: ${workflow.id}`,
48047
+ `Status: ${workflow.enabled ? "enabled" : "disabled"}`,
48048
+ workflow.trigger_summary ? `Trigger: ${workflow.trigger_summary}` : null,
48049
+ workflow.next_run_at ? `Next run: ${formatTimestamp(workflow.next_run_at)}` : null,
48050
+ workflow.last_run_status ? `Last run: ${workflow.last_run_status}` : null,
48051
+ "",
48052
+ state.workflowTab === "graph" ? "[Graph] Runs" : "Graph [Runs]",
48053
+ ""
48054
+ ].filter((line) => line !== null);
48055
+ if (state.workflowTab === "runs") {
48056
+ lines.push(...renderRunSelector(state, width), "");
48057
+ if (selectedRun) {
48058
+ lines.push(`Run graph: ${selectedRun.id} (${selectedRun.status})`);
48059
+ lines.push(...renderWorkflowGraph({ nodes: graphNodes, state, width, run: selectedRun }));
48060
+ } else {
48061
+ lines.push("No runs yet.");
48062
+ }
48063
+ } else {
48064
+ lines.push("Graph");
48065
+ lines.push(...renderWorkflowGraph({ nodes: graphNodes, state, width, run: null }));
48066
+ if (state.workflowEdit) lines.push("", `Editing ${state.workflowEdit.field}: ${state.workflowEdit.value}`);
48067
+ }
48068
+ if (state.status) lines.push("", state.status);
48069
+ return lines.flatMap((line) => wrap(line, width));
48070
+ }
48071
+ function renderRunSelector(state, width) {
48072
+ if (state.workflowRuns.length === 0) return ["Runs", "No runs yet."];
48073
+ const lines = ["Runs"];
48074
+ const visibleRuns = state.workflowRuns.slice(0, 5);
48075
+ for (let index = 0; index < visibleRuns.length; index += 1) {
48076
+ const run = visibleRuns[index];
48077
+ const cursor = index === state.selectedWorkflowRunIndex ? ">" : " ";
48078
+ lines.push(`${cursor} ${run.id} ${run.status} ${formatTimestamp(run.started_at)}`);
48079
+ }
48080
+ return lines.map((line) => truncateVisible(line, width));
48081
+ }
48082
+ function renderWorkflowGraph(params) {
48083
+ const { nodes, state, run } = params;
48084
+ if (nodes.length === 0) return ["No graph nodes available."];
48085
+ const lines = [];
48086
+ const selectedIndex = state.workflowTab === "runs" ? selectedRunGraphNodeIndex(nodes, state) : state.selectedWorkflowNodeIndex;
48087
+ const expandedId = state.workflowTab === "runs" ? state.expandedWorkflowRunNodeId : state.expandedWorkflowNodeId;
48088
+ const nodeRunsById = new Map((run?.node_runs ?? []).map((nodeRun) => [nodeRun.node_id, nodeRun]));
48089
+ for (let index = 0; index < nodes.length; index += 1) {
48090
+ const node = nodes[index];
48091
+ const nodeRun = nodeRunsById.get(node.id) ?? null;
48092
+ const cursor = index === selectedIndex ? ">" : " ";
48093
+ const status = nodeRun ? ` [${nodeRun.status}]` : "";
48094
+ lines.push(`${cursor} [${nodeTypeLabel(node.type)}] ${node.title ?? cardSummary(node)}${status}`);
48095
+ if (nodeRun?.output_summary) lines.push(` output: ${summarizeObject(nodeRun.output_summary)}`);
48096
+ if (nodeRun?.error_summary) lines.push(` error: ${nodeRun.error_summary}`);
48097
+ if (expandedId === node.id) {
48098
+ lines.push(...renderExpandedNode(node, nodeRun));
48099
+ }
48100
+ if (index < nodes.length - 1) lines.push(" |");
48101
+ }
48102
+ return lines;
48103
+ }
48104
+ function selectedRunGraphNodeIndex(nodes, state) {
48105
+ const run = state.workflowRuns[state.selectedWorkflowRunIndex];
48106
+ const nodeRun = run?.node_runs?.find((candidate) => candidate.node_id === state.expandedWorkflowRunNodeId);
48107
+ if (!nodeRun) return Math.min(state.selectedWorkflowNodeIndex, Math.max(0, nodes.length - 1));
48108
+ return Math.max(0, nodes.findIndex((node) => node.id === nodeRun.node_id));
48109
+ }
48110
+ function renderExpandedNode(node, nodeRun) {
48111
+ const lines = [
48112
+ ` id: ${node.id}`,
48113
+ ` type: ${node.type}`
48114
+ ];
48115
+ if (node.config && Object.keys(node.config).length > 0) lines.push(` config: ${summarizeObject(node.config)}`);
48116
+ if (node.input_mapping && Object.keys(node.input_mapping).length > 0) lines.push(` input: ${summarizeObject(node.input_mapping)}`);
48117
+ if (nodeRun?.input_summary) lines.push(` run input: ${summarizeObject(nodeRun.input_summary)}`);
48118
+ if (nodeRun?.output_summary) lines.push(` run output: ${summarizeObject(nodeRun.output_summary)}`);
48119
+ return lines;
48120
+ }
47043
48121
  function renderInterests(state, width) {
47044
48122
  const lines = [
47045
48123
  "Private local personalization",
@@ -47134,13 +48212,55 @@ function renderMessageContent(content, width) {
47134
48212
  }
47135
48213
  function renderHintLine(state, width) {
47136
48214
  if (state.screen === "interests") return truncateVisible("\u2191/\u2193 move Space select Enter continue Esc back", width);
48215
+ if (state.screen === "tasks") return truncateVisible("\u2191/\u2193 choose Enter open c create Esc back", width);
48216
+ if (state.screen === "task") return truncateVisible("c create e edit x delete r reorder s start d done b block u unblock k skip", width);
48217
+ if (state.screen === "workflows") return truncateVisible("\u2191/\u2193 choose Enter open Esc back", width);
48218
+ if (state.screen === "workflow" && state.workflowEdit) return truncateVisible("Enter save title Esc cancel edit", width);
48219
+ if (state.screen === "workflow") return truncateVisible("g graph r runs \u2191/\u2193 select Enter expand e title E config x run u refresh c cancel", width);
47137
48220
  return truncateVisible("\u2191/\u2193 choose Enter open /search filter Esc back", width);
47138
48221
  }
47139
48222
  function inputPlaceholder(state) {
47140
48223
  if (state.screen === "example") return "Continue from this example, or ask your own question...";
47141
48224
  if (state.screen === "chat") return "Ask a follow-up, use @file, or type /help";
48225
+ if (state.screen === "workflow" || state.screen === "workflows" || state.screen === "tasks" || state.screen === "task") return "Use shortcuts below, or type /help";
47142
48226
  return "Ask anything...";
47143
48227
  }
48228
+ function nodeTypeLabel(type) {
48229
+ switch (type) {
48230
+ case "manual_trigger":
48231
+ return "manual trigger";
48232
+ case "schedule_trigger":
48233
+ return "schedule";
48234
+ case "app_skill_action":
48235
+ return "app skill";
48236
+ case "send_notification":
48237
+ return "notification";
48238
+ case "ask_user":
48239
+ return "ask user";
48240
+ default:
48241
+ return type.replaceAll("_", " ");
48242
+ }
48243
+ }
48244
+ function cardSummary(node) {
48245
+ const config = node.config ?? {};
48246
+ if (node.type === "app_skill_action") {
48247
+ const app = typeof config.app === "string" ? config.app : "app";
48248
+ const skill = typeof config.skill === "string" ? config.skill : "skill";
48249
+ return `${app}.${skill}`;
48250
+ }
48251
+ if (node.type === "decision") return "If condition";
48252
+ if (node.type === "send_notification") return "Send notification";
48253
+ if (node.type === "ask_user") return "Ask for user input";
48254
+ return node.id;
48255
+ }
48256
+ function formatTimestamp(value) {
48257
+ if (!value) return "-";
48258
+ return new Date(value * 1e3).toISOString().replace("T", " ").slice(0, 16);
48259
+ }
48260
+ function summarizeObject(value) {
48261
+ const entries = Object.entries(value).slice(0, 3).map(([key, item]) => `${key}=${String(item)}`);
48262
+ return entries.join(", ") || "object";
48263
+ }
47144
48264
  function interestScore(haystack, interest) {
47145
48265
  let score = haystack.includes(interest) ? 10 : 0;
47146
48266
  for (const token of interest.split(/\s+|&/).filter((part) => part.length > 2)) {
@@ -47191,6 +48311,221 @@ function boxed(line, width) {
47191
48311
  return `\u2502 ${line.padEnd(width)} \u2502`;
47192
48312
  }
47193
48313
 
48314
+ // src/tasksCli.ts
48315
+ import { createHash as createHash7, randomBytes as randomBytes3, randomUUID as randomUUID5 } from "crypto";
48316
+ var TASK_STATUSES = ["backlog", "todo", "in_progress", "blocked", "done"];
48317
+ var DEFAULT_STANDALONE_PREFIX = "TASK";
48318
+ function normalizeTaskStatus(value) {
48319
+ if (value === void 0) return void 0;
48320
+ if (TASK_STATUSES.includes(value)) return value;
48321
+ throw new Error(`Unknown task status '${value}'. Expected one of: ${TASK_STATUSES.join(", ")}`);
48322
+ }
48323
+ function parseAssignee(value) {
48324
+ if (!value || value === "user") return { assigneeType: "user", assigneeHash: null };
48325
+ if (["ai", "openmates", "OpenMates"].includes(value)) return { assigneeType: "ai", assigneeHash: null };
48326
+ return { assigneeType: "user", assigneeHash: value };
48327
+ }
48328
+ function splitCsvFlag(value) {
48329
+ if (typeof value !== "string") return [];
48330
+ return value.split(/[,\n]/).map((item) => item.trim()).filter(Boolean);
48331
+ }
48332
+ function parseDueAt(value) {
48333
+ if (value === void 0) return void 0;
48334
+ if (value === false || value === true) throw new Error("--due requires a timestamp or date value.");
48335
+ const numeric = Number(value);
48336
+ if (Number.isFinite(numeric) && numeric > 0) return Math.floor(numeric);
48337
+ const parsed = Date.parse(value);
48338
+ if (Number.isNaN(parsed)) throw new Error(`Invalid --due value '${value}'.`);
48339
+ return Math.floor(parsed / 1e3);
48340
+ }
48341
+ async function buildCreateUserTaskInput(masterKey, input) {
48342
+ const taskKey = randomBytes3(32);
48343
+ const encryptedTaskKey = await encryptBytesWithAesGcm(taskKey, masterKey);
48344
+ const timestamp = nowSeconds();
48345
+ const assignee = parseAssignee(input.assign);
48346
+ const linkedProjectIds = input.projectIds ?? [];
48347
+ const status = input.status ?? (assignee.assigneeType === "ai" && !input.dueAt ? "in_progress" : "todo");
48348
+ return {
48349
+ task_id: randomUUIDCompat(),
48350
+ short_id: void 0,
48351
+ encrypted_task_key: encryptedTaskKey,
48352
+ encrypted_title: await encryptWithAesGcmCombined(input.title, taskKey),
48353
+ encrypted_description: await encryptWithAesGcmCombined(input.description ?? "", taskKey),
48354
+ encrypted_tags: await encryptWithAesGcmCombined("[]", taskKey),
48355
+ encrypted_linked_project_ids: await encryptWithAesGcmCombined(JSON.stringify(linkedProjectIds), taskKey),
48356
+ status,
48357
+ assignee_type: assignee.assigneeType,
48358
+ assignee_hash: assignee.assigneeHash,
48359
+ primary_chat_id: input.chatId ?? null,
48360
+ linked_project_ids: linkedProjectIds,
48361
+ plan_id: input.planId ?? null,
48362
+ due_at: input.dueAt ?? null,
48363
+ priority: 0,
48364
+ position: timestamp,
48365
+ created_at: timestamp,
48366
+ updated_at: timestamp
48367
+ };
48368
+ }
48369
+ async function buildUpdateUserTaskInput(task, masterKey, input) {
48370
+ const taskKey = await taskKeyFromRecord(task.encrypted, masterKey);
48371
+ const patch = { version: task.version, updated_at: nowSeconds() };
48372
+ if (input.title !== void 0) patch.encrypted_title = await encryptWithAesGcmCombined(input.title, taskKey);
48373
+ if (input.description !== void 0) patch.encrypted_description = await encryptWithAesGcmCombined(input.description, taskKey);
48374
+ if (input.status !== void 0) patch.status = input.status;
48375
+ if (input.assign !== void 0) {
48376
+ const assignee = parseAssignee(input.assign);
48377
+ patch.assignee_type = assignee.assigneeType;
48378
+ patch.assignee_hash = assignee.assigneeHash;
48379
+ }
48380
+ if (input.chatId !== void 0) patch.primary_chat_id = input.chatId;
48381
+ if (input.projectIds !== void 0) {
48382
+ patch.linked_project_ids = input.projectIds;
48383
+ patch.encrypted_linked_project_ids = await encryptWithAesGcmCombined(JSON.stringify(input.projectIds), taskKey);
48384
+ }
48385
+ if (input.planId !== void 0) patch.plan_id = input.planId;
48386
+ return patch;
48387
+ }
48388
+ async function decryptUserTask(record, masterKey) {
48389
+ const taskKey = await taskKeyFromRecord(record, masterKey);
48390
+ const tags = parseStringArray(await decryptOptional(record.encrypted_tags, taskKey));
48391
+ const linkedProjectIds = parseStringArray(await decryptOptional(record.encrypted_linked_project_ids, taskKey));
48392
+ return {
48393
+ taskId: record.task_id,
48394
+ shortId: record.short_id || deriveShortId(record),
48395
+ title: await decryptOptional(record.encrypted_title, taskKey) || "(untitled task)",
48396
+ description: await decryptOptional(record.encrypted_description, taskKey),
48397
+ tags,
48398
+ latestInstruction: await decryptOptional(record.encrypted_latest_instruction, taskKey),
48399
+ status: record.status,
48400
+ assigneeType: record.assignee_type,
48401
+ assigneeHash: record.assignee_hash ?? null,
48402
+ primaryChatId: record.primary_chat_id ?? null,
48403
+ linkedProjectIds: linkedProjectIds.length > 0 ? linkedProjectIds : record.linked_project_ids ?? [],
48404
+ planId: record.plan_id ?? null,
48405
+ dueAt: record.due_at ?? null,
48406
+ priority: record.priority ?? 0,
48407
+ position: record.position ?? 0,
48408
+ queueState: record.queue_state ?? "none",
48409
+ blockedReasonCode: record.blocked_reason_code ?? null,
48410
+ aiExecutionState: record.ai_execution_state ?? null,
48411
+ version: record.version ?? 1,
48412
+ encrypted: record
48413
+ };
48414
+ }
48415
+ async function decryptUserTasks(records, masterKey) {
48416
+ const output = [];
48417
+ for (const record of records) output.push(await decryptUserTask(record, masterKey));
48418
+ return output;
48419
+ }
48420
+ function findTask(tasks, id) {
48421
+ const task = tasks.find((candidate) => candidate.taskId === id || candidate.shortId === id);
48422
+ if (!task) throw new Error(`Task '${id}' was not found in the current task list.`);
48423
+ return task;
48424
+ }
48425
+ function renderTaskList(tasks) {
48426
+ if (tasks.length === 0) return "No tasks found.";
48427
+ const lines = ["Tasks", "ID Status Assignee Queue Title"];
48428
+ for (const task of tasks) {
48429
+ lines.push(`${pad(task.shortId, 9)} ${pad(task.status, 12)} ${pad(assigneeLabel(task), 11)} ${pad(task.queueState, 11)} ${task.title}`);
48430
+ }
48431
+ return lines.join("\n");
48432
+ }
48433
+ function renderTaskDetail2(task) {
48434
+ const lines = [
48435
+ `Task ${task.shortId}`,
48436
+ `Title: ${task.title}`,
48437
+ `Status: ${task.status}`,
48438
+ `Assignee: ${assigneeLabel(task)}`,
48439
+ `Queue: ${task.queueState}`,
48440
+ `Task ID: ${task.taskId}`
48441
+ ];
48442
+ if (task.description) lines.push(`Description: ${task.description}`);
48443
+ if (task.primaryChatId) lines.push(`Chat: ${task.primaryChatId}`);
48444
+ if (task.linkedProjectIds.length > 0) lines.push(`Projects: ${task.linkedProjectIds.join(", ")}`);
48445
+ if (task.planId) lines.push(`Plan: ${task.planId}`);
48446
+ if (task.blockedReasonCode) lines.push(`Blocked reason: ${task.blockedReasonCode}`);
48447
+ if (task.aiExecutionState) lines.push(`AI state: ${task.aiExecutionState}`);
48448
+ return lines.join("\n");
48449
+ }
48450
+ function renderTaskBoard(tasks, width = process.stdout.columns || 100) {
48451
+ const columns = TASK_STATUSES.map((status) => ({ status, tasks: tasks.filter((task) => task.status === status).sort(compareTasks) }));
48452
+ if (width < 96) {
48453
+ const lines2 = ["OpenMates Tasks Board"];
48454
+ for (const column of columns) {
48455
+ lines2.push("", `${columnTitle(column.status)} (${column.tasks.length})`, "-".repeat(24));
48456
+ lines2.push(...boardColumnLines(column.tasks, 8));
48457
+ }
48458
+ return lines2.join("\n");
48459
+ }
48460
+ const perColumn = 6;
48461
+ const columnWidth = 22;
48462
+ const lines = ["OpenMates Tasks Board", ""];
48463
+ lines.push(columns.map((column) => pad(`${columnTitle(column.status)} (${column.tasks.length})`, columnWidth)).join(" "));
48464
+ lines.push(columns.map(() => "-".repeat(columnWidth)).join(" "));
48465
+ const renderedColumns = columns.map((column) => boardColumnLines(column.tasks, perColumn).map((line) => truncate(line, columnWidth)));
48466
+ const maxRows = Math.max(...renderedColumns.map((column) => column.length));
48467
+ for (let row = 0; row < maxRows; row += 1) {
48468
+ lines.push(renderedColumns.map((column) => pad(column[row] ?? "", columnWidth)).join(" "));
48469
+ }
48470
+ return lines.join("\n");
48471
+ }
48472
+ function boardColumnLines(tasks, limit) {
48473
+ if (tasks.length === 0) return ["No tasks here."];
48474
+ const visible = tasks.slice(0, limit).flatMap((task) => [
48475
+ `[${task.shortId}] ${task.title}`,
48476
+ ` ${assigneeLabel(task)} ${task.queueState === "none" ? "" : task.queueState}`.trimEnd()
48477
+ ]);
48478
+ if (tasks.length > limit) visible.push(`... ${tasks.length - limit} more`);
48479
+ return visible;
48480
+ }
48481
+ function compareTasks(a, b) {
48482
+ return a.position - b.position || a.title.localeCompare(b.title);
48483
+ }
48484
+ function columnTitle(status) {
48485
+ if (status === "in_progress") return "In progress";
48486
+ return status.slice(0, 1).toUpperCase() + status.slice(1).replace("_", " ");
48487
+ }
48488
+ function assigneeLabel(task) {
48489
+ return task.assigneeType === "ai" ? "OpenMates" : task.assigneeHash ?? "user";
48490
+ }
48491
+ async function taskKeyFromRecord(record, masterKey) {
48492
+ if (!record.encrypted_task_key) throw new Error(`Task ${record.task_id} is missing encrypted task key.`);
48493
+ const taskKey = await decryptBytesWithAesGcm(record.encrypted_task_key, masterKey);
48494
+ if (!taskKey) throw new Error(`Failed to decrypt task key for ${record.task_id}.`);
48495
+ return taskKey;
48496
+ }
48497
+ async function decryptOptional(value, key) {
48498
+ if (!value) return "";
48499
+ return await decryptWithAesGcmCombined(value, key) ?? "";
48500
+ }
48501
+ function parseStringArray(value) {
48502
+ if (!value) return [];
48503
+ try {
48504
+ const parsed = JSON.parse(value);
48505
+ return Array.isArray(parsed) ? parsed.filter((item) => typeof item === "string") : [];
48506
+ } catch {
48507
+ return [];
48508
+ }
48509
+ }
48510
+ function deriveShortId(record) {
48511
+ const prefix = record.short_id_prefix || DEFAULT_STANDALONE_PREFIX;
48512
+ const source = record.task_id || `${record.created_at ?? ""}-${record.position ?? ""}`;
48513
+ const digest = createHash7("sha256").update(source).digest("hex").slice(0, 4).toUpperCase();
48514
+ return `${prefix}-${parseInt(digest, 16) % 1e4}`;
48515
+ }
48516
+ function nowSeconds() {
48517
+ return Math.floor(Date.now() / 1e3);
48518
+ }
48519
+ function randomUUIDCompat() {
48520
+ return randomUUID5();
48521
+ }
48522
+ function pad(value, length) {
48523
+ return truncate(value, length).padEnd(length);
48524
+ }
48525
+ function truncate(value, length) {
48526
+ return value.length <= length ? value : `${value.slice(0, Math.max(0, length - 3))}...`;
48527
+ }
48528
+
47194
48529
  // src/tui.ts
47195
48530
  function defaultModeForStreams(input, output) {
47196
48531
  return input.isTTY === true && output.isTTY === true ? "tui" : "quickstart";
@@ -47233,11 +48568,78 @@ async function handleKey(params) {
47233
48568
  return;
47234
48569
  }
47235
48570
  if (key.name === "escape") {
47236
- state.screen = "start";
48571
+ if (state.workflowEdit) {
48572
+ state.workflowEdit = null;
48573
+ render();
48574
+ return;
48575
+ }
48576
+ state.screen = state.screen === "workflow" ? "workflows" : state.screen === "task" ? "tasks" : "start";
47237
48577
  state.input = "";
47238
48578
  render();
47239
48579
  return;
47240
48580
  }
48581
+ if (state.screen === "workflow" && state.workflowEdit) {
48582
+ if (key.name === "return") {
48583
+ await saveWorkflowNodeTitle({ state, client, render });
48584
+ return;
48585
+ }
48586
+ if (key.name === "backspace") {
48587
+ state.workflowEdit.value = state.workflowEdit.value.slice(0, -1);
48588
+ render();
48589
+ return;
48590
+ }
48591
+ if (!key.ctrl && !key.meta && chunk && chunk >= " ") {
48592
+ state.workflowEdit.value += chunk;
48593
+ render();
48594
+ }
48595
+ return;
48596
+ }
48597
+ if (state.screen === "workflow" && !state.input && !key.ctrl && !key.meta) {
48598
+ if (chunk === "g") {
48599
+ state.workflowTab = "graph";
48600
+ render();
48601
+ return;
48602
+ }
48603
+ if (chunk === "r") {
48604
+ state.workflowTab = "runs";
48605
+ render();
48606
+ return;
48607
+ }
48608
+ if (chunk === "x") {
48609
+ await runActiveWorkflow({ state, client, render });
48610
+ return;
48611
+ }
48612
+ if (chunk === "u") {
48613
+ await refreshActiveWorkflowRuns({ state, client, render });
48614
+ return;
48615
+ }
48616
+ if (chunk === "c") {
48617
+ await cancelLatestWorkflowRun({ state, client, render });
48618
+ return;
48619
+ }
48620
+ if (chunk === "e") {
48621
+ startWorkflowNodeEdit(state, "title");
48622
+ render();
48623
+ return;
48624
+ }
48625
+ if (chunk === "E") {
48626
+ startWorkflowNodeEdit(state, "config");
48627
+ render();
48628
+ return;
48629
+ }
48630
+ }
48631
+ if ((state.screen === "tasks" || state.screen === "task") && !state.input && !key.ctrl && !key.meta) {
48632
+ if (chunk === "c") {
48633
+ await createTaskFromTui({ state, client, terminal, render });
48634
+ return;
48635
+ }
48636
+ }
48637
+ if (state.screen === "task" && !state.input && !key.ctrl && !key.meta) {
48638
+ if (["s", "d", "b", "u", "k", "e", "x", "r"].includes(chunk)) {
48639
+ await handleActiveTaskAction({ state, client, terminal, actionKey: chunk, render });
48640
+ return;
48641
+ }
48642
+ }
47241
48643
  if (key.name === "up") {
47242
48644
  moveSelectionOrScroll(state, -1);
47243
48645
  render();
@@ -47282,6 +48684,9 @@ async function handleKey(params) {
47282
48684
  await handleEnter({ state, client, terminal, render, finish });
47283
48685
  return;
47284
48686
  }
48687
+ if (state.screen === "workflow" || state.screen === "workflows" || state.screen === "tasks" || state.screen === "task") {
48688
+ return;
48689
+ }
47285
48690
  if (!key.ctrl && !key.meta && chunk && chunk >= " ") {
47286
48691
  state.input += chunk;
47287
48692
  render();
@@ -47300,6 +48705,23 @@ async function handleEnter(params) {
47300
48705
  render();
47301
48706
  return;
47302
48707
  }
48708
+ if (state.screen === "workflows") {
48709
+ const selected = state.workflows[state.selectedIndex];
48710
+ if (selected) await openWorkflowDetail({ state, client, workflow: selected, render });
48711
+ render();
48712
+ return;
48713
+ }
48714
+ if (state.screen === "tasks") {
48715
+ const selected = state.tasks[state.selectedIndex];
48716
+ if (selected) openTaskDetail(state, selected);
48717
+ render();
48718
+ return;
48719
+ }
48720
+ if (state.screen === "workflow") {
48721
+ toggleSelectedWorkflowNode(state);
48722
+ render();
48723
+ return;
48724
+ }
47303
48725
  const text = state.input.trim();
47304
48726
  if (!text) return;
47305
48727
  state.input = "";
@@ -47326,6 +48748,22 @@ async function handleCommand(params) {
47326
48748
  render();
47327
48749
  return;
47328
48750
  }
48751
+ if (name === "/workflows") {
48752
+ await openWorkflowList({ state, client, render });
48753
+ return;
48754
+ }
48755
+ if (name === "/tasks") {
48756
+ await openTaskList({ state, client, render });
48757
+ return;
48758
+ }
48759
+ if (name === "/workflow") {
48760
+ if (!arg) {
48761
+ await openWorkflowList({ state, client, render });
48762
+ return;
48763
+ }
48764
+ await openWorkflowById({ state, client, workflowId: arg, render });
48765
+ return;
48766
+ }
47329
48767
  if (name === "/signup") {
47330
48768
  finish({ action: "signup" });
47331
48769
  return;
@@ -47429,8 +48867,368 @@ function moveSelectionOrScroll(state, direction) {
47429
48867
  state.selectedIndex = clamp(state.selectedIndex + direction, 0, Math.max(0, state.examples.length - 1));
47430
48868
  return;
47431
48869
  }
48870
+ if (state.screen === "workflows") {
48871
+ state.selectedIndex = clamp(state.selectedIndex + direction, 0, Math.max(0, state.workflows.length - 1));
48872
+ return;
48873
+ }
48874
+ if (state.screen === "tasks") {
48875
+ state.selectedIndex = clamp(state.selectedIndex + direction, 0, Math.max(0, state.tasks.length - 1));
48876
+ return;
48877
+ }
48878
+ if (state.screen === "workflow") {
48879
+ if (state.workflowTab === "runs") {
48880
+ state.selectedWorkflowRunIndex = clamp(state.selectedWorkflowRunIndex + direction, 0, Math.max(0, state.workflowRuns.length - 1));
48881
+ const selectedRun = state.workflowRuns[state.selectedWorkflowRunIndex];
48882
+ state.selectedWorkflowNodeIndex = firstRunNodeIndex(state, selectedRun);
48883
+ return;
48884
+ }
48885
+ const nodeCount = state.activeWorkflow?.graph.nodes.length ?? 0;
48886
+ state.selectedWorkflowNodeIndex = clamp(state.selectedWorkflowNodeIndex + direction, 0, Math.max(0, nodeCount - 1));
48887
+ return;
48888
+ }
47432
48889
  state.scrollOffset = Math.max(0, state.scrollOffset - direction);
47433
48890
  }
48891
+ function toggleSelectedWorkflowNode(state) {
48892
+ const workflow = state.activeWorkflow;
48893
+ if (!workflow) return;
48894
+ const node = workflow.graph.nodes[state.selectedWorkflowNodeIndex];
48895
+ if (!node) return;
48896
+ if (state.workflowTab === "runs") {
48897
+ state.expandedWorkflowRunNodeId = state.expandedWorkflowRunNodeId === node.id ? null : node.id;
48898
+ } else {
48899
+ state.expandedWorkflowNodeId = state.expandedWorkflowNodeId === node.id ? null : node.id;
48900
+ }
48901
+ }
48902
+ function firstRunNodeIndex(state, run) {
48903
+ const workflow = state.activeWorkflow;
48904
+ const firstNodeRun = run?.node_runs?.[0];
48905
+ if (!workflow || !firstNodeRun) return 0;
48906
+ return Math.max(0, workflow.graph.nodes.findIndex((node) => node.id === firstNodeRun.node_id));
48907
+ }
48908
+ async function openTaskList(params) {
48909
+ const { state, client, render } = params;
48910
+ state.screen = "status";
48911
+ state.status = "Loading tasks...";
48912
+ render();
48913
+ try {
48914
+ state.tasks = await decryptUserTasks(await client.listUserTasks(), client.getMasterKeyBytes());
48915
+ state.selectedIndex = 0;
48916
+ state.scrollOffset = 0;
48917
+ state.activeTask = null;
48918
+ state.status = null;
48919
+ state.screen = "tasks";
48920
+ } catch (error) {
48921
+ state.status = taskError(error, "Could not load tasks. Use /login first if you are not signed in.");
48922
+ state.screen = "status";
48923
+ }
48924
+ render();
48925
+ }
48926
+ function openTaskDetail(state, task) {
48927
+ state.activeTask = task;
48928
+ state.screen = "task";
48929
+ state.scrollOffset = 0;
48930
+ }
48931
+ async function handleActiveTaskAction(params) {
48932
+ const { state, client, terminal, actionKey, render } = params;
48933
+ const task = state.activeTask;
48934
+ if (!task) return;
48935
+ state.status = "Updating task...";
48936
+ render();
48937
+ try {
48938
+ if (actionKey === "e") {
48939
+ const title = await promptLine(terminal, `New title for ${task.shortId}: `);
48940
+ if (!title.trim()) {
48941
+ state.status = "Edit cancelled.";
48942
+ render();
48943
+ return;
48944
+ }
48945
+ const patch = await buildUpdateUserTaskInput(task, client.getMasterKeyBytes(), { title: title.trim() });
48946
+ const updated2 = await client.updateUserTask(task.taskId, patch);
48947
+ const decrypted2 = await decryptUserTask(updated2, client.getMasterKeyBytes());
48948
+ replaceTask(state, decrypted2);
48949
+ state.activeTask = decrypted2;
48950
+ state.status = null;
48951
+ state.screen = "task";
48952
+ render();
48953
+ return;
48954
+ }
48955
+ if (actionKey === "x") {
48956
+ const confirmation = await promptLine(terminal, `Type DELETE to delete ${task.shortId}: `);
48957
+ if (confirmation !== "DELETE") {
48958
+ state.status = "Delete cancelled.";
48959
+ render();
48960
+ return;
48961
+ }
48962
+ await client.deleteUserTask(task.taskId);
48963
+ state.tasks = state.tasks.filter((candidate) => candidate.taskId !== task.taskId);
48964
+ state.activeTask = null;
48965
+ state.selectedIndex = Math.min(state.selectedIndex, Math.max(0, state.tasks.length - 1));
48966
+ state.status = `Deleted ${task.shortId}.`;
48967
+ state.screen = "tasks";
48968
+ render();
48969
+ return;
48970
+ }
48971
+ if (actionKey === "r") {
48972
+ const positionText = await promptLine(terminal, `New numeric position for ${task.shortId}: `);
48973
+ const position = Number(positionText.trim());
48974
+ if (!Number.isFinite(position)) {
48975
+ state.status = "Reorder cancelled: position must be a number.";
48976
+ render();
48977
+ return;
48978
+ }
48979
+ const updated2 = await client.reorderUserTasks({ moves: [{ task_id: task.taskId, position }] });
48980
+ const [decrypted2] = await decryptUserTasks(updated2, client.getMasterKeyBytes());
48981
+ if (decrypted2) {
48982
+ replaceTask(state, decrypted2);
48983
+ state.activeTask = decrypted2;
48984
+ }
48985
+ state.status = null;
48986
+ state.screen = "task";
48987
+ render();
48988
+ return;
48989
+ }
48990
+ const payload = actionKey === "b" ? { version: task.version, blocked_reason_code: "needs_user_input" } : { version: task.version };
48991
+ const updated = actionKey === "s" ? await client.startUserTaskWithAI(task.taskId, {
48992
+ version: task.version,
48993
+ primary_chat_id: task.primaryChatId ?? void 0,
48994
+ linked_project_ids: task.linkedProjectIds,
48995
+ plaintext_title: task.title,
48996
+ plaintext_description: task.description,
48997
+ plaintext_latest_instruction: task.latestInstruction
48998
+ }) : actionKey === "d" ? await client.completeUserTask(task.taskId, payload) : actionKey === "b" ? await client.blockUserTask(task.taskId, payload) : actionKey === "u" ? await client.unblockUserTask(task.taskId, payload) : await client.skipUserTask(task.taskId, payload);
48999
+ const decrypted = await decryptUserTask(updated, client.getMasterKeyBytes());
49000
+ replaceTask(state, decrypted);
49001
+ state.activeTask = decrypted;
49002
+ state.status = null;
49003
+ state.screen = "task";
49004
+ } catch (error) {
49005
+ state.status = taskError(error, "Could not update task.");
49006
+ state.screen = "status";
49007
+ }
49008
+ render();
49009
+ }
49010
+ async function createTaskFromTui(params) {
49011
+ const { state, client, terminal, render } = params;
49012
+ state.status = "Creating task...";
49013
+ render();
49014
+ try {
49015
+ const title = await promptLine(terminal, "Task title: ");
49016
+ if (!title.trim()) {
49017
+ state.status = "Create cancelled.";
49018
+ render();
49019
+ return;
49020
+ }
49021
+ const description = await promptLine(terminal, "Description (optional): ");
49022
+ const input = await buildCreateUserTaskInput(client.getMasterKeyBytes(), {
49023
+ title: title.trim(),
49024
+ description: description.trim(),
49025
+ assign: "user"
49026
+ });
49027
+ const created = await client.createUserTask(input);
49028
+ const decrypted = await decryptUserTask(created, client.getMasterKeyBytes());
49029
+ replaceTask(state, decrypted);
49030
+ state.activeTask = decrypted;
49031
+ state.selectedIndex = Math.max(0, state.tasks.findIndex((task) => task.taskId === decrypted.taskId));
49032
+ state.status = null;
49033
+ state.screen = "task";
49034
+ } catch (error) {
49035
+ state.status = taskError(error, "Could not create task.");
49036
+ state.screen = "status";
49037
+ }
49038
+ render();
49039
+ }
49040
+ async function promptLine(terminal, prompt) {
49041
+ return terminal.suspend(async () => {
49042
+ const rl = createInterface3({ input: nodeStdin, output: nodeStdout });
49043
+ try {
49044
+ return await rl.question(prompt);
49045
+ } finally {
49046
+ rl.close();
49047
+ }
49048
+ });
49049
+ }
49050
+ function replaceTask(state, task) {
49051
+ const index = state.tasks.findIndex((candidate) => candidate.taskId === task.taskId);
49052
+ if (index >= 0) state.tasks[index] = task;
49053
+ else state.tasks.unshift(task);
49054
+ }
49055
+ function taskError(error, fallback) {
49056
+ return error instanceof Error ? `${fallback} ${error.message}` : fallback;
49057
+ }
49058
+ async function openWorkflowList(params) {
49059
+ const { state, client, render } = params;
49060
+ state.screen = "status";
49061
+ state.status = "Loading workflows...";
49062
+ render();
49063
+ try {
49064
+ state.workflows = await client.listWorkflows();
49065
+ state.selectedIndex = 0;
49066
+ state.scrollOffset = 0;
49067
+ state.status = null;
49068
+ state.screen = "workflows";
49069
+ } catch (error) {
49070
+ state.status = workflowError(error, "Could not load workflows. Use /login first if you are not signed in.");
49071
+ state.screen = "status";
49072
+ }
49073
+ render();
49074
+ }
49075
+ async function openWorkflowById(params) {
49076
+ const { state, client, workflowId, render } = params;
49077
+ state.screen = "status";
49078
+ state.status = `Loading workflow ${workflowId}...`;
49079
+ render();
49080
+ try {
49081
+ const workflow = await client.getWorkflow(workflowId);
49082
+ await openWorkflowDetail({ state, client, workflow, render });
49083
+ } catch (error) {
49084
+ state.status = workflowError(error, `Could not load workflow ${workflowId}.`);
49085
+ state.screen = "status";
49086
+ render();
49087
+ }
49088
+ }
49089
+ async function openWorkflowDetail(params) {
49090
+ const { state, client, workflow, render } = params;
49091
+ state.status = `Loading workflow ${workflow.id}...`;
49092
+ render();
49093
+ let detail;
49094
+ try {
49095
+ detail = await client.getWorkflow(workflow.id);
49096
+ } catch (error) {
49097
+ state.status = workflowError(error, `Could not load workflow ${workflow.id}.`);
49098
+ state.screen = "status";
49099
+ render();
49100
+ return;
49101
+ }
49102
+ state.activeWorkflow = detail;
49103
+ state.workflowRuns = [];
49104
+ state.workflowTab = "graph";
49105
+ state.selectedWorkflowNodeIndex = 0;
49106
+ state.selectedWorkflowRunIndex = 0;
49107
+ state.expandedWorkflowNodeId = null;
49108
+ state.expandedWorkflowRunNodeId = null;
49109
+ state.workflowEdit = null;
49110
+ state.screen = "workflow";
49111
+ state.scrollOffset = 0;
49112
+ state.status = null;
49113
+ render();
49114
+ await refreshActiveWorkflowRuns({ state, client, render });
49115
+ }
49116
+ async function refreshActiveWorkflowRuns(params) {
49117
+ const { state, client, render } = params;
49118
+ const workflow = state.activeWorkflow;
49119
+ if (!workflow) return;
49120
+ state.status = "Refreshing workflow runs...";
49121
+ render();
49122
+ try {
49123
+ state.workflowRuns = await client.listWorkflowRuns(workflow.id);
49124
+ state.selectedWorkflowRunIndex = clamp(state.selectedWorkflowRunIndex, 0, Math.max(0, state.workflowRuns.length - 1));
49125
+ state.status = null;
49126
+ state.screen = "workflow";
49127
+ } catch (error) {
49128
+ state.status = workflowError(error, "Could not refresh workflow runs.");
49129
+ }
49130
+ render();
49131
+ }
49132
+ async function saveWorkflowNodeTitle(params) {
49133
+ const { state, client, render } = params;
49134
+ const workflow = state.activeWorkflow;
49135
+ const edit = state.workflowEdit;
49136
+ if (!workflow || !edit) return;
49137
+ let parsedConfig = null;
49138
+ if (edit.field === "config") {
49139
+ try {
49140
+ const parsed = JSON.parse(edit.value || "{}");
49141
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("Config must be a JSON object");
49142
+ parsedConfig = parsed;
49143
+ } catch (error) {
49144
+ state.status = workflowError(error, "Invalid config JSON.");
49145
+ render();
49146
+ return;
49147
+ }
49148
+ }
49149
+ const graph = {
49150
+ ...workflow.graph,
49151
+ nodes: workflow.graph.nodes.map((node) => {
49152
+ if (node.id !== edit.nodeId) return node;
49153
+ if (edit.field === "config") return { ...node, config: parsedConfig ?? {} };
49154
+ return { ...node, title: edit.value.trim() || null };
49155
+ })
49156
+ };
49157
+ state.status = `Saving node ${edit.field}...`;
49158
+ render();
49159
+ try {
49160
+ state.activeWorkflow = await client.updateWorkflow(workflow.id, { graph });
49161
+ state.workflowEdit = null;
49162
+ state.status = `Saved node ${edit.field}.`;
49163
+ } catch (error) {
49164
+ state.status = workflowError(error, "Could not save node title.");
49165
+ }
49166
+ render();
49167
+ }
49168
+ function startWorkflowNodeEdit(state, field) {
49169
+ const workflow = state.activeWorkflow;
49170
+ if (!workflow || state.workflowTab !== "graph") {
49171
+ state.status = "Switch to the Graph tab before editing node details.";
49172
+ return;
49173
+ }
49174
+ const node = workflow.graph.nodes[state.selectedWorkflowNodeIndex];
49175
+ if (!node) return;
49176
+ state.workflowEdit = {
49177
+ nodeId: node.id,
49178
+ field,
49179
+ value: field === "config" ? JSON.stringify(node.config ?? {}) : node.title ?? ""
49180
+ };
49181
+ state.expandedWorkflowNodeId = node.id;
49182
+ state.status = `Editing ${field} for ${node.id}.`;
49183
+ }
49184
+ async function runActiveWorkflow(params) {
49185
+ const { state, client, render } = params;
49186
+ const workflow = state.activeWorkflow;
49187
+ if (!workflow) return;
49188
+ if (!workflow.enabled) {
49189
+ state.status = "Enable this workflow outside TUI before running it.";
49190
+ render();
49191
+ return;
49192
+ }
49193
+ state.status = "Starting workflow run...";
49194
+ render();
49195
+ try {
49196
+ const run = await client.runWorkflow(workflow.id, {
49197
+ idempotencyKey: `tui-${workflow.id}-${Date.now()}`,
49198
+ mode: "manual",
49199
+ input: {}
49200
+ });
49201
+ state.workflowRuns = [run, ...state.workflowRuns.filter((candidate) => candidate.id !== run.id)];
49202
+ state.status = `Started run ${run.id}. Press u to refresh.`;
49203
+ } catch (error) {
49204
+ state.status = workflowError(error, "Could not start workflow run.");
49205
+ }
49206
+ render();
49207
+ }
49208
+ async function cancelLatestWorkflowRun(params) {
49209
+ const { state, client, render } = params;
49210
+ const workflow = state.activeWorkflow;
49211
+ const run = state.workflowRuns.find((candidate) => ["queued", "running", "waiting"].includes(candidate.status));
49212
+ if (!workflow || !run) {
49213
+ state.status = "No active workflow run to cancel.";
49214
+ render();
49215
+ return;
49216
+ }
49217
+ state.status = `Cancelling run ${run.id}...`;
49218
+ render();
49219
+ try {
49220
+ const result = await client.cancelWorkflowRun(workflow.id, run.id);
49221
+ state.status = `Run ${result.run_id} ${result.status}.`;
49222
+ await refreshActiveWorkflowRuns({ state, client, render });
49223
+ } catch (error) {
49224
+ state.status = workflowError(error, "Could not cancel workflow run.");
49225
+ render();
49226
+ }
49227
+ }
49228
+ function workflowError(error, fallback) {
49229
+ const details = error instanceof Error ? error.message : String(error);
49230
+ return `${fallback} ${details}`;
49231
+ }
47434
49232
  function toggleInterest(state) {
47435
49233
  const interest = TUI_INTERESTS[state.selectedIndex];
47436
49234
  if (!interest) return;
@@ -47928,6 +49726,10 @@ async function main() {
47928
49726
  printChatsHelp();
47929
49727
  return;
47930
49728
  }
49729
+ if (command === "drafts") {
49730
+ printDraftsHelp();
49731
+ return;
49732
+ }
47931
49733
  if (command === "apps") {
47932
49734
  printAppsHelp();
47933
49735
  return;
@@ -47940,6 +49742,10 @@ async function main() {
47940
49742
  printWorkflowsHelp();
47941
49743
  return;
47942
49744
  }
49745
+ if (command === "tasks") {
49746
+ printTasksHelp();
49747
+ return;
49748
+ }
47943
49749
  if (command === "connected-accounts") {
47944
49750
  printConnectedAccountsHelp();
47945
49751
  return;
@@ -48050,6 +49856,14 @@ async function main() {
48050
49856
  await handleChats(client, subcommand, rest, parsed.flags, redactor);
48051
49857
  return;
48052
49858
  }
49859
+ if (command === "tasks") {
49860
+ await handleTasks(client, subcommand, rest, parsed.flags);
49861
+ return;
49862
+ }
49863
+ if (command === "drafts") {
49864
+ await handleDrafts(client, subcommand, rest, parsed.flags);
49865
+ return;
49866
+ }
48053
49867
  if (command === "apps") {
48054
49868
  await handleApps(client, subcommand, rest, parsed.flags);
48055
49869
  return;
@@ -48187,6 +50001,172 @@ function handleCliVersion(flags) {
48187
50001
  }
48188
50002
  console.log("OpenMates CLI is up to date.");
48189
50003
  }
50004
+ async function handleTasks(client, subcommand, rest, flags) {
50005
+ if (!subcommand || subcommand === "help" || flags.help === true) {
50006
+ printTasksHelp();
50007
+ return;
50008
+ }
50009
+ const masterKey = client.getMasterKeyBytes();
50010
+ const scope = taskScopeFromFlags(flags);
50011
+ if (subcommand === "list" || subcommand === "status") {
50012
+ if (subcommand === "status" && rest[0]) {
50013
+ const task = await resolveTask(client, masterKey, rest[0], scope);
50014
+ printTaskOutput(task, flags);
50015
+ return;
50016
+ }
50017
+ const tasks = await loadTasks(client, masterKey, scope);
50018
+ if (flags.json === true) printJson2({ tasks: tasks.map(taskToJson) });
50019
+ else console.log(renderTaskList(tasks));
50020
+ return;
50021
+ }
50022
+ if (subcommand === "board") {
50023
+ const tasks = await loadTasks(client, masterKey, scope);
50024
+ if (flags.json === true) printJson2({ tasks: tasks.map(taskToJson) });
50025
+ else console.log(renderTaskBoard(tasks));
50026
+ return;
50027
+ }
50028
+ if (subcommand === "show") {
50029
+ const id = rest[0];
50030
+ if (!id) throw new Error("Missing task ID. Usage: openmates tasks show <task-id>");
50031
+ const task = await resolveTask(client, masterKey, id, scope);
50032
+ printTaskOutput(task, flags);
50033
+ return;
50034
+ }
50035
+ if (subcommand === "create") {
50036
+ const title = taskTitleFromFlagsOrRest(flags, rest);
50037
+ const input = await buildCreateUserTaskInput(masterKey, {
50038
+ title,
50039
+ description: typeof flags.description === "string" ? flags.description : "",
50040
+ status: normalizeTaskStatus(typeof flags.status === "string" ? flags.status : void 0),
50041
+ assign: taskAssignFlag(flags),
50042
+ chatId: typeof flags.chat === "string" ? flags.chat : null,
50043
+ projectIds: splitCsvFlag(flags.project ?? flags.projects),
50044
+ planId: typeof flags.plan === "string" ? flags.plan : null,
50045
+ dueAt: parseDueAt(flags.due)
50046
+ });
50047
+ const created = await client.createUserTask(input);
50048
+ printTaskOutput(await decryptUserTask(created, masterKey), flags);
50049
+ return;
50050
+ }
50051
+ if (subcommand === "edit") {
50052
+ const id = rest[0];
50053
+ if (!id) throw new Error("Missing task ID. Usage: openmates tasks edit <task-id> [--title ...]");
50054
+ const task = await resolveTask(client, masterKey, id, scope);
50055
+ const patch = await buildUpdateUserTaskInput(task, masterKey, {
50056
+ title: typeof flags.title === "string" ? flags.title : void 0,
50057
+ description: typeof flags.description === "string" ? flags.description : void 0,
50058
+ status: normalizeTaskStatus(typeof flags.status === "string" ? flags.status : void 0),
50059
+ assign: taskAssignFlag(flags),
50060
+ chatId: flags.chat === true ? null : typeof flags.chat === "string" ? flags.chat : void 0,
50061
+ projectIds: flags.project || flags.projects ? splitCsvFlag(flags.project ?? flags.projects) : void 0,
50062
+ planId: flags.plan === true ? null : typeof flags.plan === "string" ? flags.plan : void 0
50063
+ });
50064
+ const updated = await client.updateUserTask(task.taskId, patch);
50065
+ printTaskOutput(await decryptUserTask(updated, masterKey), flags);
50066
+ return;
50067
+ }
50068
+ if (subcommand === "delete") {
50069
+ const task = await requiredResolvedTask(client, masterKey, rest[0], scope, "delete");
50070
+ if (flags.confirm !== true) throw new Error("Deleting a task requires --confirm.");
50071
+ const result = await client.deleteUserTask(task.taskId);
50072
+ if (flags.json === true) printJson2(result);
50073
+ else console.log(`Task deleted: ${task.shortId}`);
50074
+ return;
50075
+ }
50076
+ if (subcommand === "start") {
50077
+ const task = await requiredResolvedTask(client, masterKey, rest[0], scope, "start");
50078
+ const started = await client.startUserTaskWithAI(task.taskId, {
50079
+ version: task.version,
50080
+ primary_chat_id: task.primaryChatId ?? void 0,
50081
+ linked_project_ids: task.linkedProjectIds,
50082
+ plaintext_title: task.title,
50083
+ plaintext_description: task.description,
50084
+ plaintext_latest_instruction: task.latestInstruction
50085
+ });
50086
+ printTaskOutput(await decryptUserTask(started, masterKey), flags);
50087
+ return;
50088
+ }
50089
+ if (["block", "unblock", "skip", "done"].includes(subcommand)) {
50090
+ const task = await requiredResolvedTask(client, masterKey, rest[0], scope, subcommand);
50091
+ const payload = { version: task.version };
50092
+ if (subcommand === "block") {
50093
+ if (typeof flags.reason !== "string") throw new Error("Blocking a task requires --reason <code>.");
50094
+ payload.blocked_reason_code = flags.reason;
50095
+ }
50096
+ const updated = subcommand === "block" ? await client.blockUserTask(task.taskId, payload) : subcommand === "unblock" ? await client.unblockUserTask(task.taskId, payload) : subcommand === "skip" ? await client.skipUserTask(task.taskId, payload) : await client.completeUserTask(task.taskId, payload);
50097
+ printTaskOutput(await decryptUserTask(updated, masterKey), flags);
50098
+ return;
50099
+ }
50100
+ if (subcommand === "reorder") {
50101
+ const task = await requiredResolvedTask(client, masterKey, rest[0], scope, "reorder");
50102
+ const move = { task_id: task.taskId };
50103
+ if (typeof flags.before === "string") move.before_task_id = (await resolveTask(client, masterKey, flags.before, scope)).taskId;
50104
+ if (typeof flags.after === "string") move.after_task_id = (await resolveTask(client, masterKey, flags.after, scope)).taskId;
50105
+ if (typeof flags.status === "string") move.status = normalizeTaskStatus(flags.status);
50106
+ if (typeof flags.position === "string") move.position = Number(flags.position);
50107
+ const updated = await client.reorderUserTasks({ moves: [move] });
50108
+ const decrypted = await decryptUserTasks(updated, masterKey);
50109
+ if (flags.json === true) printJson2({ tasks: decrypted.map(taskToJson) });
50110
+ else console.log(`Task reordered: ${task.shortId}`);
50111
+ return;
50112
+ }
50113
+ throw new Error(`Unknown tasks command '${subcommand}'. Run 'openmates tasks --help'.`);
50114
+ }
50115
+ function taskScopeFromFlags(flags) {
50116
+ return {
50117
+ status: normalizeTaskStatus(typeof flags.status === "string" ? flags.status : void 0),
50118
+ chatId: typeof flags.chat === "string" ? flags.chat : void 0,
50119
+ projectId: typeof flags.project === "string" ? flags.project : void 0,
50120
+ planId: typeof flags.plan === "string" ? flags.plan : void 0
50121
+ };
50122
+ }
50123
+ async function loadTasks(client, masterKey, scope) {
50124
+ const records = await client.listUserTasks({ status: scope.status, chatId: scope.chatId, projectId: scope.projectId });
50125
+ const tasks = await decryptUserTasks(records, masterKey);
50126
+ return scope.planId ? tasks.filter((task) => task.planId === scope.planId) : tasks;
50127
+ }
50128
+ async function resolveTask(client, masterKey, id, scope) {
50129
+ return findTask(await loadTasks(client, masterKey, { ...scope, status: void 0 }), id);
50130
+ }
50131
+ async function requiredResolvedTask(client, masterKey, id, scope, action) {
50132
+ if (!id) throw new Error(`Missing task ID. Usage: openmates tasks ${action} <task-id>`);
50133
+ return resolveTask(client, masterKey, id, scope);
50134
+ }
50135
+ function printTaskOutput(task, flags) {
50136
+ if (flags.json === true) printJson2({ task: taskToJson(task) });
50137
+ else console.log(renderTaskDetail2(task));
50138
+ }
50139
+ function taskToJson(task) {
50140
+ return {
50141
+ task_id: task.taskId,
50142
+ short_id: task.shortId,
50143
+ title: task.title,
50144
+ description: task.description,
50145
+ tags: task.tags,
50146
+ latest_instruction: task.latestInstruction,
50147
+ status: task.status,
50148
+ assignee_type: task.assigneeType,
50149
+ assignee_hash: task.assigneeHash,
50150
+ primary_chat_id: task.primaryChatId,
50151
+ linked_project_ids: task.linkedProjectIds,
50152
+ plan_id: task.planId,
50153
+ due_at: task.dueAt,
50154
+ priority: task.priority,
50155
+ position: task.position,
50156
+ queue_state: task.queueState,
50157
+ blocked_reason_code: task.blockedReasonCode,
50158
+ ai_execution_state: task.aiExecutionState,
50159
+ version: task.version
50160
+ };
50161
+ }
50162
+ function taskTitleFromFlagsOrRest(flags, rest) {
50163
+ const title = typeof flags.title === "string" ? flags.title : rest.join(" ").trim();
50164
+ if (!title) throw new Error("Missing task title. Usage: openmates tasks create --title <title>");
50165
+ return title;
50166
+ }
50167
+ function taskAssignFlag(flags) {
50168
+ return typeof flags.assign === "string" ? flags.assign : typeof flags.assignee === "string" ? flags.assignee : void 0;
50169
+ }
48190
50170
  async function handleRemoteAccess(client, subcommand, rest, flags) {
48191
50171
  if (!subcommand || subcommand === "help" || flags.help === true) {
48192
50172
  printRemoteAccessHelp();
@@ -48197,7 +50177,7 @@ async function handleRemoteAccess(client, subcommand, rest, flags) {
48197
50177
  if (!rootPath) {
48198
50178
  throw new Error("Missing source path. Usage: openmates remote-access start --path <folder>");
48199
50179
  }
48200
- const sourceId = typeof flags["source-id"] === "string" ? flags["source-id"] : randomUUID5();
50180
+ const sourceId = typeof flags["source-id"] === "string" ? flags["source-id"] : randomUUID6();
48201
50181
  const projectId = typeof flags.project === "string" ? flags.project : void 0;
48202
50182
  validateRemoteSourceRegistrationFlags(projectId, flags);
48203
50183
  const sourceType = parseRemoteAccessSourceType(flags.type);
@@ -48303,11 +50283,57 @@ function parseJsonFlag(value, flagName) {
48303
50283
  throw new Error(`Invalid JSON for ${flagName}: ${message}`);
48304
50284
  }
48305
50285
  }
50286
+ async function handleDrafts(client, subcommand, rest, flags) {
50287
+ if (!subcommand || subcommand === "help" || flags.help === true) {
50288
+ printDraftsHelp();
50289
+ return;
50290
+ }
50291
+ if (subcommand === "create" || subcommand === "update") {
50292
+ const chatId = subcommand === "update" ? rest[0] : typeof flags.chat === "string" ? flags.chat : void 0;
50293
+ const markdown = subcommand === "update" ? rest.slice(1).join(" ").trim() : rest.join(" ").trim();
50294
+ if (subcommand === "update" && !chatId) throw new Error("Missing chat ID for draft update.");
50295
+ if (!markdown) throw new Error(`Missing draft text for draft ${subcommand}.`);
50296
+ const draft = await client.saveDraft({
50297
+ chatId,
50298
+ markdown,
50299
+ preview: typeof flags.preview === "string" ? flags.preview : markdown.slice(0, 160)
50300
+ });
50301
+ printJson2(draft);
50302
+ return;
50303
+ }
50304
+ if (subcommand === "list") {
50305
+ const drafts = await client.listDrafts(flags.refresh === true);
50306
+ printJson2({ drafts });
50307
+ return;
50308
+ }
50309
+ if (subcommand === "get") {
50310
+ const chatId = rest[0];
50311
+ if (!chatId) throw new Error("Missing chat ID for draft get.");
50312
+ printJson2({ draft: await client.getDraft(chatId, flags.refresh === true) });
50313
+ return;
50314
+ }
50315
+ if (subcommand === "clear") {
50316
+ const chatId = rest[0];
50317
+ if (!chatId) throw new Error("Missing chat ID for draft clear.");
50318
+ await client.clearDraft(chatId);
50319
+ printJson2({ success: true, chat_id: chatId });
50320
+ return;
50321
+ }
50322
+ if (subcommand === "sync") {
50323
+ printJson2({ versions: await client.reconcileDraftVersions() });
50324
+ return;
50325
+ }
50326
+ throw new Error(`Unknown drafts subcommand '${subcommand}'.`);
50327
+ }
48306
50328
  async function handleChats(client, subcommand, rest, flags, redactor) {
48307
50329
  if (!subcommand || subcommand === "help" || flags.help === true) {
48308
50330
  printChatsHelp();
48309
50331
  return;
48310
50332
  }
50333
+ if (rest[0] === "tasks") {
50334
+ await handleTasks(client, rest[1], rest.slice(2), { ...flags, chat: subcommand });
50335
+ return;
50336
+ }
48311
50337
  if (subcommand === "list") {
48312
50338
  const limit = typeof flags.limit === "string" ? parseInt(flags.limit, 10) : 10;
48313
50339
  const page = typeof flags.page === "string" ? parseInt(flags.page, 10) : 1;
@@ -48990,7 +51016,37 @@ async function handleWorkflows(client, subcommand, rest, flags) {
48990
51016
  }
48991
51017
  return;
48992
51018
  }
51019
+ if (subcommand === "validate") {
51020
+ const file = typeof flags.file === "string" ? flags.file : "";
51021
+ if (!file) throw new Error("Missing --file. Example: openmates workflows validate --file workflow.yml");
51022
+ const validation = await client.validateWorkflowYaml(readFileSync9(file, "utf8"));
51023
+ if (flags.json === true) {
51024
+ printJson2(validation);
51025
+ } else {
51026
+ kv("Draft valid", validation.draft_valid ? "yes" : "no");
51027
+ kv("Ready to enable", validation.enable_ready ? "yes" : "no");
51028
+ for (const diagnostic of validation.diagnostics) {
51029
+ console.log(` - ${String(diagnostic.path ?? "$")}: ${String(diagnostic.message ?? diagnostic.code ?? "invalid")}`);
51030
+ }
51031
+ }
51032
+ if (!validation.draft_valid) process.exitCode = 1;
51033
+ return;
51034
+ }
48993
51035
  if (subcommand === "create") {
51036
+ const yamlFile = typeof flags.file === "string" ? flags.file : "";
51037
+ if (yamlFile) {
51038
+ const result = await client.createWorkflowYaml(readFileSync9(yamlFile, "utf8"));
51039
+ if (flags.json === true) {
51040
+ printJson2(result);
51041
+ } else {
51042
+ printWorkflowDetail(result.workflow);
51043
+ kv("Ready to enable", result.validation.enable_ready ? "yes" : "no");
51044
+ for (const diagnostic of result.validation.diagnostics) {
51045
+ console.log(` - ${String(diagnostic.path ?? "$")}: ${String(diagnostic.message ?? diagnostic.code ?? "input required")}`);
51046
+ }
51047
+ }
51048
+ return;
51049
+ }
48994
51050
  const title = typeof flags.title === "string" ? flags.title.trim() : "";
48995
51051
  const graphJson = typeof flags.graph === "string" ? flags.graph : "";
48996
51052
  if (!title) throw new Error("Missing --title for workflow create.");
@@ -49008,6 +51064,22 @@ async function handleWorkflows(client, subcommand, rest, flags) {
49008
51064
  }
49009
51065
  return;
49010
51066
  }
51067
+ if (subcommand === "update") {
51068
+ const workflowId = rest[0];
51069
+ const yamlFile = typeof flags.file === "string" ? flags.file : "";
51070
+ if (!workflowId || !yamlFile) throw new Error("Missing workflow ID or --file. Example: openmates workflows update <id> --file workflow.yml");
51071
+ const result = await client.updateWorkflowYaml(workflowId, readFileSync9(yamlFile, "utf8"));
51072
+ if (flags.json === true) {
51073
+ printJson2(result);
51074
+ } else {
51075
+ printWorkflowDetail(result.workflow);
51076
+ kv("Ready to enable", result.validation.enable_ready ? "yes" : "no");
51077
+ for (const diagnostic of result.validation.diagnostics) {
51078
+ console.log(` - ${String(diagnostic.path ?? "$")}: ${String(diagnostic.message ?? diagnostic.code ?? "input required")}`);
51079
+ }
51080
+ }
51081
+ return;
51082
+ }
49011
51083
  if (subcommand === "input") {
49012
51084
  const text = typeof flags.text === "string" ? flags.text : rest.join(" ").trim();
49013
51085
  if (!text) throw new Error('Missing workflow input text. Example: openmates workflows input "alert me if it rains"');
@@ -49114,9 +51186,11 @@ async function handleWorkflows(client, subcommand, rest, flags) {
49114
51186
  if (subcommand === "run") {
49115
51187
  const workflowId = rest[0];
49116
51188
  if (!workflowId) throw new Error("Missing workflow ID. Example: openmates workflows run <id>");
51189
+ const idempotencyKey = typeof flags["idempotency-key"] === "string" ? flags["idempotency-key"] : "";
51190
+ if (!idempotencyKey) throw new Error("Missing --idempotency-key. Reuse this stable key when retrying the same workflow run.");
49117
51191
  const mode = flags.mode === "test" ? "test" : "manual";
49118
51192
  const input = typeof flags.input === "string" ? parseJsonFlag(flags.input, "--input") : {};
49119
- const run = await client.runWorkflow(workflowId, { mode, input });
51193
+ const run = await client.runWorkflow(workflowId, { idempotencyKey, mode, input });
49120
51194
  if (flags.json === true) {
49121
51195
  printJson2(run);
49122
51196
  } else {
@@ -49147,6 +51221,58 @@ async function handleWorkflows(client, subcommand, rest, flags) {
49147
51221
  }
49148
51222
  return;
49149
51223
  }
51224
+ if (subcommand === "run-cancel") {
51225
+ const workflowId = rest[0];
51226
+ const runId = rest[1];
51227
+ if (!workflowId || !runId) throw new Error("Missing workflow/run ID. Example: openmates workflows run-cancel <workflow-id> <run-id>");
51228
+ const result = await client.cancelWorkflowRun(workflowId, runId);
51229
+ if (flags.json === true) {
51230
+ printJson2(result);
51231
+ } else {
51232
+ kv("Status", result.status);
51233
+ }
51234
+ return;
51235
+ }
51236
+ if (subcommand === "step-test") {
51237
+ const workflowId = rest[0];
51238
+ const stepId = rest[1];
51239
+ if (!workflowId || !stepId) throw new Error("Missing workflow/step ID. Example: openmates workflows step-test <workflow-id> <step-id> --yes");
51240
+ const input = typeof flags.input === "string" ? parseJsonFlag(flags.input, "--input") : {};
51241
+ const run = await client.testWorkflowStep(workflowId, stepId, { input, confirmed: flags.yes === true });
51242
+ if (flags.json === true) {
51243
+ printJson2(run);
51244
+ } else {
51245
+ printWorkflowRun(run);
51246
+ }
51247
+ return;
51248
+ }
51249
+ if (subcommand === "respond") {
51250
+ const workflowId = rest[0];
51251
+ const runId = rest[1];
51252
+ const stepId = rest[2];
51253
+ if (!workflowId || !runId || !stepId) throw new Error(`Missing workflow/run/step ID. Example: openmates workflows respond <workflow-id> <run-id> <step-id> --input '{"answer":"Berlin"}'`);
51254
+ const input = typeof flags.input === "string" ? parseJsonFlag(flags.input, "--input") : {};
51255
+ const run = await client.respondToWorkflowRun(workflowId, runId, stepId, input);
51256
+ if (flags.json === true) {
51257
+ printJson2(run);
51258
+ } else {
51259
+ printWorkflowRun(run);
51260
+ }
51261
+ return;
51262
+ }
51263
+ if (subcommand === "help-app") {
51264
+ const capabilityId = rest[0];
51265
+ if (!capabilityId) throw new Error("Missing app skill. Example: openmates workflows help-app weather.forecast");
51266
+ const capabilities = await client.listWorkflowCapabilities();
51267
+ const capability = capabilities.find((item) => item.id === capabilityId || item.id === capabilityId.replace(":", "."));
51268
+ if (!capability) throw new Error(`Workflow capability not found: ${capabilityId}`);
51269
+ if (flags.json === true) {
51270
+ printJson2(capability);
51271
+ } else {
51272
+ printWorkflowCapabilityHelp(capability);
51273
+ }
51274
+ return;
51275
+ }
49150
51276
  throw new Error(`Unknown workflows command '${subcommand}'. Run 'openmates workflows --help'.`);
49151
51277
  }
49152
51278
  function printWorkflowList(workflows) {
@@ -49216,6 +51342,30 @@ function printWorkflowCapabilities(capabilities) {
49216
51342
  kv(capability.id, `${capability.title} \xB7 ${state}`, 24);
49217
51343
  }
49218
51344
  }
51345
+ function printWorkflowCapabilityHelp(capability) {
51346
+ header(`${capability.title}
51347
+ `);
51348
+ kv("ID", capability.id);
51349
+ kv("Type", capability.type);
51350
+ kv("Enabled", capability.enabled ? "yes" : "no");
51351
+ if (capability.reason) kv("Reason", capability.reason);
51352
+ const metadata = capability.metadata ?? {};
51353
+ const workflow = metadata.workflow;
51354
+ if (workflow) {
51355
+ kv("Effect", String(workflow.effect ?? "unknown"));
51356
+ kv("Execution", String(workflow.execution_mode ?? "unknown"));
51357
+ kv("Approval", String(workflow.approval ?? "unknown"));
51358
+ kv("Unattended", workflow.unattended === true ? "yes" : "no");
51359
+ }
51360
+ if (metadata.input_schema) {
51361
+ console.log("\nInput schema:");
51362
+ console.log(JSON.stringify(metadata.input_schema, null, 2));
51363
+ }
51364
+ if (workflow?.test_example_input) {
51365
+ console.log("\nTest example:");
51366
+ console.log(JSON.stringify(workflow.test_example_input, null, 2));
51367
+ }
51368
+ }
49219
51369
  async function handleApps(client, subcommand, rest, flags) {
49220
51370
  const apiKey = resolveApiKey(flags) ?? void 0;
49221
51371
  if (!subcommand || subcommand === "help") {
@@ -49452,7 +51602,7 @@ Run with --help for full parameter details:
49452
51602
  }
49453
51603
  }
49454
51604
  try {
49455
- const inputData = buildSkillInput(flags, inlineTokens, schemaParams);
51605
+ const inputData = app === "models3d" && skill === "generate" ? await buildModels3DGenerateInput(client, flags, inlineTokens, schemaParams) : buildSkillInput(flags, inlineTokens, schemaParams);
49456
51606
  const data = await client.runSkill({ app, skill, inputData, apiKey });
49457
51607
  if (flags.json === true) {
49458
51608
  printJson2(data);
@@ -49657,6 +51807,27 @@ function buildSkillInput(flags, inlineTokens, schemaParams) {
49657
51807
  }
49658
51808
  return {};
49659
51809
  }
51810
+ async function buildModels3DGenerateInput(client, flags, inlineTokens, schemaParams) {
51811
+ const imagePath = typeof flags.image === "string" ? flags.image : void 0;
51812
+ if (!imagePath) return buildSkillInput(flags, inlineTokens, schemaParams);
51813
+ if (typeof flags.input === "string" || inlineTokens.length > 0) {
51814
+ throw new Error("Use either a text prompt or --image <path> for 3D generation, not both.");
51815
+ }
51816
+ if (!client.hasSession()) {
51817
+ throw new Error("Image-based 3D generation requires `openmates login` to upload the reference image.");
51818
+ }
51819
+ const processed = processFiles([imagePath], null);
51820
+ if (processed.blocked.length > 0 || processed.errors.length > 0 || processed.embeds.length !== 1) {
51821
+ const reason = [...processed.blocked, ...processed.errors].map((entry) => entry.error).join("; ") || "no image embed produced";
51822
+ throw new Error(`Failed to prepare 3D reference image: ${reason}`);
51823
+ }
51824
+ const fileEmbed = processed.embeds[0];
51825
+ if (!fileEmbed.requiresUpload || !fileEmbed.localPath || fileEmbed.embed.type !== "image") {
51826
+ throw new Error("3D generation requires a supported image file.");
51827
+ }
51828
+ const uploadResult = await uploadFile(fileEmbed.localPath, client.getSession());
51829
+ return { image_embed_refs: [uploadResult.embed_id] };
51830
+ }
49660
51831
  function getEffectiveRequiredParams(schemaParams) {
49661
51832
  const required = schemaParams.filter((p) => p.required);
49662
51833
  if (required.length > 0) return required;
@@ -50201,7 +52372,7 @@ async function confirmOrExit(question) {
50201
52372
  process.exit(0);
50202
52373
  }
50203
52374
  }
50204
- async function promptLine(question) {
52375
+ async function promptLine2(question) {
50205
52376
  const rl = await import("readline");
50206
52377
  const iface = rl.createInterface({ input: process.stdin, output: process.stdout });
50207
52378
  const answer = await new Promise((resolve7) => iface.question(question, resolve7));
@@ -50210,7 +52381,7 @@ async function promptLine(question) {
50210
52381
  }
50211
52382
  async function promptSecret(question) {
50212
52383
  if (!process.stdin.isTTY) {
50213
- return promptLine(question);
52384
+ return promptLine2(question);
50214
52385
  }
50215
52386
  return new Promise((resolve7) => {
50216
52387
  const stdin2 = process.stdin;
@@ -50297,8 +52468,8 @@ async function writeSecretFile(filePath, content, force = false) {
50297
52468
  return filePath;
50298
52469
  }
50299
52470
  async function generateProvisioningPassword() {
50300
- const { randomBytes: randomBytes3 } = await import("crypto");
50301
- return `OM-${randomBytes3(18).toString("base64url")}-aA2#`;
52471
+ const { randomBytes: randomBytes4 } = await import("crypto");
52472
+ return `OM-${randomBytes4(18).toString("base64url")}-aA2#`;
50302
52473
  }
50303
52474
  function decodeBase32(input) {
50304
52475
  const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
@@ -50333,7 +52504,7 @@ async function runSecuritySetup(client, flags, options = {}) {
50333
52504
  result.otpSecret = setup.secret ?? null;
50334
52505
  if (setup.otpauth_url) client.renderTotpQrCode(setup.otpauth_url);
50335
52506
  if (setup.secret) console.log(`Manual TOTP secret: ${setup.secret}`);
50336
- const code = typeof process.env.OPENMATES_CLI_SIGNUP_TOTP_CODE === "string" ? process.env.OPENMATES_CLI_SIGNUP_TOTP_CODE : options.autoGenerateTotp && setup.secret ? await generateTotpCode(setup.secret) : await promptLine("Enter current 2FA code: ");
52507
+ const code = typeof process.env.OPENMATES_CLI_SIGNUP_TOTP_CODE === "string" ? process.env.OPENMATES_CLI_SIGNUP_TOTP_CODE : options.autoGenerateTotp && setup.secret ? await generateTotpCode(setup.secret) : await promptLine2("Enter current 2FA code: ");
50337
52508
  await client.verifyTotpSetup(code);
50338
52509
  const provider = typeof flags.provider === "string" ? flags.provider : "Authenticator app";
50339
52510
  await client.setTotpProvider(provider);
@@ -50373,8 +52544,8 @@ async function handleSignup(client, flags, options = {}) {
50373
52544
  if (flags.password !== void 0) {
50374
52545
  throw new Error("Passwords must be entered through hidden prompts, not command-line flags.");
50375
52546
  }
50376
- const email = typeof flags.email === "string" ? flags.email : await promptLine("Email: ");
50377
- const username = typeof flags.username === "string" ? flags.username : await promptLine("Username: ");
52547
+ const email = typeof flags.email === "string" ? flags.email : await promptLine2("Email: ");
52548
+ const username = typeof flags.username === "string" ? flags.username : await promptLine2("Username: ");
50378
52549
  const inviteCode = typeof flags["invite-code"] === "string" ? flags["invite-code"] : "";
50379
52550
  const language = typeof flags.language === "string" ? flags.language : "en";
50380
52551
  const password = process.env.OPENMATES_CLI_SIGNUP_PASSWORD ?? await promptSecret("Password: ");
@@ -50383,7 +52554,7 @@ async function handleSignup(client, flags, options = {}) {
50383
52554
  if (password !== confirmPassword) throw new Error("Passwords do not match.");
50384
52555
  }
50385
52556
  await client.requestSignupEmailCode({ email, inviteCode, language });
50386
- const emailCode = process.env.OPENMATES_CLI_SIGNUP_EMAIL_CODE ?? await promptLine("Email verification code: ");
52557
+ const emailCode = process.env.OPENMATES_CLI_SIGNUP_EMAIL_CODE ?? await promptLine2("Email verification code: ");
50387
52558
  await client.verifySignupEmailCode({ email, username, inviteCode, code: emailCode, language });
50388
52559
  const signup = await client.setupPasswordAccount({ email, username, password, inviteCode, language });
50389
52560
  const security = await runSecuritySetup(client, flags, options);
@@ -50425,13 +52596,15 @@ async function handleE2E(client, subcommand, rest, flags) {
50425
52596
  throw new Error("E2E provisioning refuses production API URLs.");
50426
52597
  }
50427
52598
  if (flags.password !== void 0) throw new Error("Use generated passwords or OPENMATES_CLI_SIGNUP_PASSWORD, not --password.");
52599
+ const inviteCode = process.env.OPENMATES_CLI_SIGNUP_INVITE_CODE;
52600
+ if (!inviteCode) throw new Error("OPENMATES_CLI_SIGNUP_INVITE_CODE is required for E2E provisioning.");
50428
52601
  const slot = parseRequiredNumber(flags.slot, "--slot");
50429
- if (![15, 16, 17, 18, 19, 20].includes(slot)) {
50430
- throw new Error("Only reserved slots 15-20 are supported.");
52602
+ if (![14, 15, 16, 17, 18, 19, 20].includes(slot)) {
52603
+ throw new Error("Only reserved slots 14-20 are supported.");
50431
52604
  }
50432
52605
  const artifact = typeof flags.artifact === "string" ? flags.artifact : `test-results/credential-updates/slot-${slot}.env`;
50433
52606
  const domain = typeof flags.domain === "string" ? flags.domain : process.env.OPENMATES_CLI_E2E_EMAIL_DOMAIN;
50434
- const email = typeof flags.email === "string" ? flags.email : domain ? `cli-e2e-slot-${slot}-${Date.now()}@${domain}` : await promptLine("E2E account email: ");
52607
+ const email = typeof flags.email === "string" ? flags.email : domain ? `cli-e2e-slot-${slot}-${Date.now()}@${domain}` : await promptLine2("E2E account email: ");
50435
52608
  const username = typeof flags.username === "string" ? flags.username : `cli_e2e_slot_${slot}_${Date.now()}`;
50436
52609
  const generatedPassword = process.env.OPENMATES_CLI_SIGNUP_PASSWORD ?? await generateProvisioningPassword();
50437
52610
  const originalSignupPassword = process.env.OPENMATES_CLI_SIGNUP_PASSWORD;
@@ -50442,6 +52615,7 @@ async function handleE2E(client, subcommand, rest, flags) {
50442
52615
  ...flags,
50443
52616
  email,
50444
52617
  username,
52618
+ "invite-code": inviteCode,
50445
52619
  yes: true,
50446
52620
  json: false,
50447
52621
  "backup-codes-output": typeof flags["backup-codes-output"] === "string" ? flags["backup-codes-output"] : `${artifact}.backup-codes`,
@@ -50636,10 +52810,10 @@ async function handleSettings(client, subcommand, rest, flags) {
50636
52810
  await confirmOrExit("Delete this account and all associated data? [y/N] ");
50637
52811
  }
50638
52812
  await client.requestDeleteAccountEmailCode();
50639
- const emailCode = await promptLine("Enter email verification code: ");
52813
+ const emailCode = await promptLine2("Enter email verification code: ");
50640
52814
  await client.verifyDeleteAccountEmailCode(emailCode);
50641
52815
  const authMethods = await client.getAuthMethodsStatus();
50642
- const totpCode = authMethods.has_2fa ? await promptLine("Enter 2FA code: ") : void 0;
52816
+ const totpCode = authMethods.has_2fa ? await promptLine2("Enter 2FA code: ") : void 0;
50643
52817
  const result = await client.deleteAccountWithCliVerification(totpCode);
50644
52818
  if (flags.json === true) printJson2(result);
50645
52819
  else console.log("\x1B[32m\u2713\x1B[0m Account deletion requested and local CLI session cleared");
@@ -51358,11 +53532,11 @@ function ansiMateBlock(category, mateName) {
51358
53532
  const [r, g, b] = CATEGORY_ANSI_COLORS[category];
51359
53533
  return `\x1B[48;2;${r};${g};${b}m\x1B[97m ${label} \x1B[0m`;
51360
53534
  }
51361
- function formatTimestamp(ts) {
53535
+ function formatTimestamp2(ts) {
51362
53536
  if (!ts) return "\u2014";
51363
53537
  const d = new Date(ts * 1e3);
51364
- const pad = (n) => String(n).padStart(2, "0");
51365
- return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
53538
+ const pad2 = (n) => String(n).padStart(2, "0");
53539
+ return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())} ${pad2(d.getHours())}:${pad2(d.getMinutes())}`;
51366
53540
  }
51367
53541
  function printChatsTable(result) {
51368
53542
  const { chats, total, page, limit, hasMore } = result;
@@ -51381,7 +53555,7 @@ function printChatsTable(result) {
51381
53555
  );
51382
53556
  for (const chat of chats) {
51383
53557
  const block = ansiColorBlock(chat.category);
51384
- const time = formatTimestamp(chat.updatedAt);
53558
+ const time = formatTimestamp2(chat.updatedAt);
51385
53559
  const title = chat.title ?? "(no title)";
51386
53560
  const idStr = chat.shortId;
51387
53561
  const sourceLabel = chat.source === "example" ? " \x1B[33mEXAMPLE CHAT\x1B[0m" : "";
@@ -51942,7 +54116,7 @@ async function renderMessageText(text, embedRefIndex, client) {
51942
54116
  async function printChatConversation(client, chat, messages, followUpSuggestions, options = {}) {
51943
54117
  const block = ansiColorBlock(chat.category);
51944
54118
  const title = chat.title ?? "(no title)";
51945
- const ts = formatTimestamp(chat.updatedAt);
54119
+ const ts = formatTimestamp2(chat.updatedAt);
51946
54120
  if (options.example) {
51947
54121
  process.stdout.write("\x1B[33mEXAMPLE CHAT\x1B[0m\n");
51948
54122
  process.stdout.write("\x1B[2mThis is a public example chat. Log in to create and sync your own private chats.\x1B[0m\n\n");
@@ -51972,7 +54146,7 @@ async function printChatConversation(client, chat, messages, followUpSuggestions
51972
54146
  const embedRefIndex = hasEmbedRefs ? await client.buildEmbedRefIndex(parentEmbedIds) : /* @__PURE__ */ new Map();
51973
54147
  for (let i = 0; i < messages.length; i++) {
51974
54148
  const msg = messages[i];
51975
- const msgTs = formatTimestamp(msg.createdAt);
54149
+ const msgTs = formatTimestamp2(msg.createdAt);
51976
54150
  process.stdout.write(`${SEP}
51977
54151
  `);
51978
54152
  if (msg.role === "user") {
@@ -52033,7 +54207,7 @@ async function printChatConversation(client, chat, messages, followUpSuggestions
52033
54207
  }
52034
54208
  async function printChatConversationRaw(chat, messages, options = {}) {
52035
54209
  const title = chat.title ?? "(no title)";
52036
- const ts = formatTimestamp(chat.updatedAt);
54210
+ const ts = formatTimestamp2(chat.updatedAt);
52037
54211
  if (options.example) {
52038
54212
  process.stdout.write("EXAMPLE CHAT\n");
52039
54213
  process.stdout.write("This is a public example chat. Log in to create and sync your own private chats.\n\n");
@@ -52048,7 +54222,7 @@ async function printChatConversationRaw(chat, messages, options = {}) {
52048
54222
  return;
52049
54223
  }
52050
54224
  for (const msg of messages) {
52051
- const msgTs = formatTimestamp(msg.createdAt);
54225
+ const msgTs = formatTimestamp2(msg.createdAt);
52052
54226
  process.stdout.write(`${SEP}
52053
54227
  `);
52054
54228
  const role = msg.role === "user" ? "You" : msg.senderName ?? msg.role;
@@ -52551,18 +54725,18 @@ function printWhoAmI(user) {
52551
54725
  }
52552
54726
  }
52553
54727
  function printGenericObject(value, indent = 0) {
52554
- const pad = " ".repeat(indent);
54728
+ const pad2 = " ".repeat(indent);
52555
54729
  if (value === null || value === void 0) {
52556
- console.log(`${pad}(empty)`);
54730
+ console.log(`${pad2}(empty)`);
52557
54731
  return;
52558
54732
  }
52559
54733
  if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
52560
- console.log(`${pad}${value}`);
54734
+ console.log(`${pad2}${value}`);
52561
54735
  return;
52562
54736
  }
52563
54737
  if (Array.isArray(value)) {
52564
54738
  if (value.length === 0) {
52565
- console.log(`${pad}(empty list)`);
54739
+ console.log(`${pad2}(empty list)`);
52566
54740
  return;
52567
54741
  }
52568
54742
  for (let i = 0; i < value.length; i++) {
@@ -52570,11 +54744,11 @@ function printGenericObject(value, indent = 0) {
52570
54744
  if (item && typeof item === "object") {
52571
54745
  printGenericObject(item, indent);
52572
54746
  if (i < value.length - 1) {
52573
- process.stdout.write(`${pad}\x1B[2m${"\u2500".repeat(40)}\x1B[0m
54747
+ process.stdout.write(`${pad2}\x1B[2m${"\u2500".repeat(40)}\x1B[0m
52574
54748
  `);
52575
54749
  }
52576
54750
  } else {
52577
- console.log(`${pad}${String(item)}`);
54751
+ console.log(`${pad2}${String(item)}`);
52578
54752
  }
52579
54753
  }
52580
54754
  return;
@@ -52594,34 +54768,34 @@ function printGenericObject(value, indent = 0) {
52594
54768
  if (Array.isArray(v)) {
52595
54769
  if (v.length === 0) {
52596
54770
  process.stdout.write(
52597
- `${pad} \x1B[2m${k.padEnd(20)}\x1B[0m (none)
54771
+ `${pad2} \x1B[2m${k.padEnd(20)}\x1B[0m (none)
52598
54772
  `
52599
54773
  );
52600
54774
  } else if (v.length > 0 && typeof v[0] === "object") {
52601
54775
  process.stdout.write(
52602
54776
  `
52603
- ${pad} \x1B[1m${k}\x1B[0m \x1B[2m(${v.length})\x1B[0m
54777
+ ${pad2} \x1B[1m${k}\x1B[0m \x1B[2m(${v.length})\x1B[0m
52604
54778
  `
52605
54779
  );
52606
54780
  printGenericObject(v, indent + 1);
52607
54781
  } else {
52608
54782
  process.stdout.write(
52609
- `${pad} \x1B[2m${k.padEnd(20)}\x1B[0m ${v.join(", ")}
54783
+ `${pad2} \x1B[2m${k.padEnd(20)}\x1B[0m ${v.join(", ")}
52610
54784
  `
52611
54785
  );
52612
54786
  }
52613
54787
  } else if (typeof v === "object") {
52614
- process.stdout.write(`${pad} \x1B[1m${k}\x1B[0m
54788
+ process.stdout.write(`${pad2} \x1B[1m${k}\x1B[0m
52615
54789
  `);
52616
54790
  printGenericObject(v, indent + 1);
52617
54791
  } else {
52618
- process.stdout.write(`${pad} \x1B[2m${k.padEnd(20)}\x1B[0m ${v}
54792
+ process.stdout.write(`${pad2} \x1B[2m${k.padEnd(20)}\x1B[0m ${v}
52619
54793
  `);
52620
54794
  }
52621
54795
  }
52622
54796
  return;
52623
54797
  }
52624
- console.log(`${pad}${String(value)}`);
54798
+ console.log(`${pad2}${String(value)}`);
52625
54799
  }
52626
54800
  function printBillingResponse(obj) {
52627
54801
  header("Billing\n");
@@ -52697,7 +54871,7 @@ function printMemoriesList(memories) {
52697
54871
  header(`Memories (${memories.length})
52698
54872
  `);
52699
54873
  for (const mem of memories) {
52700
- const ts = formatTimestamp(mem.updated_at);
54874
+ const ts = formatTimestamp2(mem.updated_at);
52701
54875
  process.stdout.write(
52702
54876
  `\x1B[1m${mem.app_id}/${mem.item_type}\x1B[0m \x1B[2mv${mem.item_version} ${ts} id:${mem.id.slice(0, 8)}\x1B[0m
52703
54877
  `
@@ -52947,46 +55121,46 @@ async function handleMentions(client, subcommand, _rest, flags) {
52947
55121
  process.exit(1);
52948
55122
  }
52949
55123
  function serializeToYaml(data, indent = 0) {
52950
- const pad = " ".repeat(indent);
55124
+ const pad2 = " ".repeat(indent);
52951
55125
  let out = "";
52952
55126
  for (const [key, val] of Object.entries(data)) {
52953
55127
  if (val === null || val === void 0) {
52954
- out += `${pad}${key}: null
55128
+ out += `${pad2}${key}: null
52955
55129
  `;
52956
55130
  } else if (typeof val === "boolean" || typeof val === "number") {
52957
- out += `${pad}${key}: ${val}
55131
+ out += `${pad2}${key}: ${val}
52958
55132
  `;
52959
55133
  } else if (typeof val === "string") {
52960
55134
  if (val.includes("\n")) {
52961
- out += `${pad}${key}: |
55135
+ out += `${pad2}${key}: |
52962
55136
  `;
52963
55137
  for (const line of val.split("\n")) {
52964
- out += `${pad} ${line}
55138
+ out += `${pad2} ${line}
52965
55139
  `;
52966
55140
  }
52967
55141
  } else {
52968
55142
  const needsQuote = val.includes(":") || val.includes("#") || val.startsWith("{") || val.startsWith("[") || val.startsWith("'") || val.startsWith('"') || val === "" || val === "true" || val === "false" || val === "null";
52969
- out += `${pad}${key}: ${needsQuote ? `"${val.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"` : val}
55143
+ out += `${pad2}${key}: ${needsQuote ? `"${val.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"` : val}
52970
55144
  `;
52971
55145
  }
52972
55146
  } else if (Array.isArray(val)) {
52973
- out += `${pad}${key}:
55147
+ out += `${pad2}${key}:
52974
55148
  `;
52975
55149
  for (const item of val) {
52976
55150
  if (typeof item === "object" && item !== null) {
52977
- out += `${pad}-
55151
+ out += `${pad2}-
52978
55152
  `;
52979
55153
  out += serializeToYaml(
52980
55154
  item,
52981
55155
  indent + 2
52982
55156
  );
52983
55157
  } else {
52984
- out += `${pad}- ${item}
55158
+ out += `${pad2}- ${item}
52985
55159
  `;
52986
55160
  }
52987
55161
  }
52988
55162
  } else if (typeof val === "object") {
52989
- out += `${pad}${key}:
55163
+ out += `${pad2}${key}:
52990
55164
  `;
52991
55165
  out += serializeToYaml(val, indent + 1);
52992
55166
  }
@@ -53074,6 +55248,8 @@ Commands:
53074
55248
  openmates logout Log out and clear session
53075
55249
  openmates whoami [--json] Show account info
53076
55250
  openmates chats [--help] Chat commands (list, search, show, ...)
55251
+ openmates tasks [--help] Task commands (list, create, board, ...)
55252
+ openmates drafts [--help] Encrypted draft lifecycle commands
53077
55253
  openmates apps [--help] App skill commands (list, run, ...)
53078
55254
  openmates workflows [--help] Server-side workflow commands
53079
55255
  openmates mentions [--help] List available @mentions
@@ -53212,7 +55388,7 @@ Creates local ignored credential artifacts for reserved E2E auth accounts. The
53212
55388
  command refuses production API URLs and does not upload GitHub secrets.
53213
55389
 
53214
55390
  Options:
53215
- --slot <15-20> Reserved auth-account slot
55391
+ --slot <14-20> Reserved auth-account slot
53216
55392
  --artifact <path> Output .env artifact path
53217
55393
  --email <email> Test email; prompted/generated when omitted
53218
55394
  --domain <mail-domain> Generate email at this domain
@@ -53326,6 +55502,46 @@ Examples:
53326
55502
  openmates chats share last --expires 604800
53327
55503
  openmates chats share d262cb68 --password mypass`);
53328
55504
  }
55505
+ function printTasksHelp() {
55506
+ console.log(`Tasks commands:
55507
+ openmates tasks list [--status <status>] [--chat <id>] [--project <id>] [--json]
55508
+ openmates tasks board [--chat <id>] [--project <id>] [--json]
55509
+ openmates tasks show <task-id|short-id> [--json]
55510
+ openmates tasks create --title <title> [--description <text>] [--assign user|ai] [--chat <id>] [--project <id>] [--status <status>] [--due <date>] [--json]
55511
+ openmates tasks edit <task-id|short-id> [--title <title>] [--description <text>] [--assign user|ai] [--status <status>] [--json]
55512
+ openmates tasks delete <task-id|short-id> --confirm [--json]
55513
+ openmates tasks start <task-id|short-id> [--json]
55514
+ openmates tasks status [<task-id|short-id>] [--json]
55515
+ openmates tasks block <task-id|short-id> --reason <code> [--json]
55516
+ openmates tasks unblock <task-id|short-id> [--json]
55517
+ openmates tasks skip <task-id|short-id> [--json]
55518
+ openmates tasks done <task-id|short-id> [--json]
55519
+ openmates tasks reorder <task-id|short-id> [--before <task-id>] [--after <task-id>] [--position <n>] [--status <status>] [--json]
55520
+
55521
+ Chat-scoped aliases:
55522
+ openmates chats <chat-id> tasks list
55523
+ openmates chats <chat-id> tasks board
55524
+ openmates chats <chat-id> tasks create --title <title>
55525
+
55526
+ Statuses:
55527
+ backlog, todo, in_progress, blocked, done
55528
+
55529
+ Notes:
55530
+ Task IDs accept full task_id or human short IDs such as OM-6.
55531
+ Normal output decrypts task title and description locally; use --json for machine-readable plaintext fields.`);
55532
+ }
55533
+ function printDraftsHelp() {
55534
+ console.log(`Draft commands:
55535
+ openmates drafts create <text> [--chat <uuid>] [--preview <text>] [--json]
55536
+ openmates drafts update <chat-id> <text> [--preview <text>] [--json]
55537
+ openmates drafts list [--refresh] [--json]
55538
+ openmates drafts get <chat-id> [--refresh] [--json]
55539
+ openmates drafts clear <chat-id> [--json]
55540
+ openmates drafts sync [--json]
55541
+
55542
+ Draft plaintext is encrypted locally with the account master key. Only Format-D
55543
+ ciphertext and version metadata are sent to the server or written to CLI cache.`);
55544
+ }
53329
55545
  function printAppsHelp() {
53330
55546
  console.log(`Apps commands:
53331
55547
  openmates apps list [--json]
@@ -53334,6 +55550,7 @@ function printAppsHelp() {
53334
55550
  openmates apps skill-info <app-id> <skill-id> [--json]
53335
55551
  openmates apps <app-id> <skill-id> "<query>" [--json]
53336
55552
  openmates apps <app-id> <skill-id> --input '<json>' [--json]
55553
+ openmates apps models3d generate --image ./reference.png [--json]
53337
55554
  openmates apps code run --language python --code 'print("Hello")'
53338
55555
  openmates apps code run --entry main.py --file main.py [--file requirements.txt]
53339
55556
  openmates apps code run --entry main.py --dir ./project [--exclude node_modules]
@@ -53349,6 +55566,7 @@ Examples:
53349
55566
  openmates apps web search "latest AI news"
53350
55567
  openmates apps news search "climate change"
53351
55568
  openmates apps ai ask "Summarise this: ..."
55569
+ openmates apps models3d generate --image ./chair.png
53352
55570
  openmates apps code run --language python --filename hello.py --code 'print("Hello from CLI")'
53353
55571
  openmates apps travel search_connections --input '{"requests":[{"legs":[{"origin":"BER","destination":"LHR","date":"2026-04-15"}]}]}'
53354
55572
  openmates apps travel booking-link --token "<booking_token from search result>"
@@ -53358,6 +55576,9 @@ function printWorkflowsHelp() {
53358
55576
  console.log(`Workflows commands:
53359
55577
  openmates workflows list [--json]
53360
55578
  openmates workflows capabilities [--json]
55579
+ openmates workflows validate --file workflow.yml [--json]
55580
+ openmates workflows create --file workflow.yml [--json]
55581
+ openmates workflows update <workflow-id> --file workflow.yml [--json]
53361
55582
  openmates workflows create --title <title> --graph '<json>' [--enabled] [--run-content-retention last_5|none] [--json]
53362
55583
  openmates workflows input <text> [--workflow-id <id>] [--project-id <id>] [--json]
53363
55584
  openmates workflows input-show <session-id> [--json]
@@ -53368,9 +55589,13 @@ function printWorkflowsHelp() {
53368
55589
  openmates workflows show <workflow-id> [--json]
53369
55590
  openmates workflows enable <workflow-id> [--json]
53370
55591
  openmates workflows disable <workflow-id> [--json]
53371
- openmates workflows run <workflow-id> [--mode manual|test] [--input '<json>'] [--json]
55592
+ openmates workflows run <workflow-id> --idempotency-key <stable-key> [--mode manual|test] [--input '<json>'] [--json]
53372
55593
  openmates workflows runs <workflow-id> [--json]
53373
55594
  openmates workflows run-show <workflow-id> <run-id> [--json]
55595
+ openmates workflows run-cancel <workflow-id> <run-id> [--json]
55596
+ openmates workflows step-test <workflow-id> <step-id> [--input '<json>'] [--yes] [--json]
55597
+ openmates workflows respond <workflow-id> <run-id> <step-id> --input '<json>' [--json]
55598
+ openmates workflows help-app <app.skill> [--json]
53374
55599
  openmates workflows delete <workflow-id> --yes [--json]
53375
55600
 
53376
55601
  Workflows run on the OpenMates server, not in this terminal process. The CLI
@@ -53380,6 +55605,7 @@ and Apple clients.
53380
55605
  Examples:
53381
55606
  openmates workflows list
53382
55607
  openmates workflows capabilities --json
55608
+ openmates workflows help-app weather.forecast
53383
55609
  openmates workflows input "alert me if it rains tomorrow"
53384
55610
  openmates workflows run wf_123 --mode test --json`);
53385
55611
  }
@@ -53631,9 +55857,12 @@ if (isCliEntrypoint()) {
53631
55857
  }
53632
55858
 
53633
55859
  export {
55860
+ deriveChatCompletionRecoveryKeypair,
55861
+ openChatCompletionRecoveryEnvelope,
53634
55862
  unwrapApiKeyMasterKey,
53635
55863
  decryptWithAesGcmCombined,
53636
55864
  decryptBytesWithAesGcm,
55865
+ encryptBytesWithAesGcm,
53637
55866
  encryptWithAesGcmCombined,
53638
55867
  hashItemKey,
53639
55868
  generateChatShareBlob,
@@ -53646,6 +55875,7 @@ export {
53646
55875
  INTEREST_TAG_IDS,
53647
55876
  normalizeInterestTagIds,
53648
55877
  MEMORY_TYPE_REGISTRY,
55878
+ reconcileAuthoritativeChats,
53649
55879
  MATE_NAMES,
53650
55880
  deriveAppUrl,
53651
55881
  OpenMatesClient,