openmates 0.14.7 → 0.14.8-alpha.1

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,58 @@ 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 TASK_STATUSES = /* @__PURE__ */ new Set(["backlog", "todo", "in_progress", "blocked", "done"]);
1194
+ var TASK_ASSIGNEES = /* @__PURE__ */ new Set(["ai", "user"]);
1195
+ var WebSocketProtocolError = class extends Error {
1196
+ code;
1197
+ constructor(code, message) {
1198
+ super(message);
1199
+ this.name = "WebSocketProtocolError";
1200
+ this.code = code;
1201
+ }
1202
+ };
1203
+ function websocketProtocolError(envelope) {
1204
+ const payload = envelope.payload && typeof envelope.payload === "object" ? envelope.payload : {};
1205
+ const code = envelope.type === "client_update_required" ? envelope.type : envelope.type === "error" && typeof payload.code === "string" ? payload.code : null;
1206
+ if (code === "client_update_required") {
1207
+ return new WebSocketProtocolError(code, CLIENT_UPDATE_REQUIRED_GUIDANCE);
1208
+ }
1209
+ if (envelope.type === "error") {
1210
+ return new WebSocketProtocolError(
1211
+ code ?? "websocket_error",
1212
+ typeof payload.message === "string" ? payload.message : "Unknown WebSocket error"
1213
+ );
1214
+ }
1215
+ return null;
1216
+ }
1217
+ function parseTaskProposals(value) {
1218
+ if (!Array.isArray(value)) return [];
1219
+ return value.flatMap((item) => {
1220
+ if (!item || typeof item !== "object" || Array.isArray(item)) return [];
1221
+ const raw = item;
1222
+ if (typeof raw.title !== "string" || raw.title.trim().length === 0) return [];
1223
+ const proposal = { title: raw.title };
1224
+ if (typeof raw.description === "string" || raw.description === null) proposal.description = raw.description;
1225
+ if (typeof raw.status === "string" && TASK_STATUSES.has(raw.status)) proposal.status = raw.status;
1226
+ if (typeof raw.assignee_type === "string" && TASK_ASSIGNEES.has(raw.assignee_type)) proposal.assignee_type = raw.assignee_type;
1227
+ return [proposal];
1228
+ });
1229
+ }
1230
+ function parseTaskUpdateProposals(value) {
1231
+ if (!Array.isArray(value)) return [];
1232
+ return value.flatMap((item) => {
1233
+ if (!item || typeof item !== "object" || Array.isArray(item)) return [];
1234
+ const raw = item;
1235
+ if (typeof raw.task_id !== "string" || raw.task_id.trim().length === 0) return [];
1236
+ const proposal = { task_id: raw.task_id };
1237
+ if (typeof raw.title === "string" || raw.title === null) proposal.title = raw.title;
1238
+ if (typeof raw.description === "string" || raw.description === null) proposal.description = raw.description;
1239
+ if (typeof raw.status === "string" && TASK_STATUSES.has(raw.status) || raw.status === null) proposal.status = raw.status;
1240
+ if (typeof raw.assignee_type === "string" && TASK_ASSIGNEES.has(raw.assignee_type) || raw.assignee_type === null) proposal.assignee_type = raw.assignee_type;
1241
+ return [proposal];
1242
+ });
1243
+ }
1045
1244
  var OpenMatesWsClient = class {
1046
1245
  socket;
1047
1246
  constructor(options) {
@@ -1111,13 +1310,23 @@ var OpenMatesWsClient = class {
1111
1310
  }
1112
1311
  waitForMessage(expectedType, predicate, timeoutMs = 2e4) {
1113
1312
  return new Promise((resolve7, reject) => {
1313
+ const seenTypes = /* @__PURE__ */ new Set();
1314
+ let predicateMisses = 0;
1114
1315
  const onMessage = (rawData) => {
1115
1316
  try {
1116
1317
  const parsed = JSON.parse(rawData.toString());
1318
+ if (typeof parsed.type === "string") seenTypes.add(parsed.type);
1319
+ const protocolError = websocketProtocolError(parsed);
1320
+ if (protocolError) {
1321
+ cleanup();
1322
+ reject(protocolError);
1323
+ return;
1324
+ }
1117
1325
  if (parsed.type !== expectedType) {
1118
1326
  return;
1119
1327
  }
1120
1328
  if (predicate && !predicate(parsed.payload)) {
1329
+ predicateMisses += 1;
1121
1330
  return;
1122
1331
  }
1123
1332
  cleanup();
@@ -1135,7 +1344,12 @@ var OpenMatesWsClient = class {
1135
1344
  };
1136
1345
  const timeout = setTimeout(() => {
1137
1346
  cleanup();
1138
- reject(new Error(`Timeout waiting for '${expectedType}'`));
1347
+ const observed = [...seenTypes].sort().join(", ") || "none";
1348
+ reject(
1349
+ new Error(
1350
+ `Timeout waiting for '${expectedType}' (seen types: ${observed}; predicate misses: ${predicateMisses})`
1351
+ )
1352
+ );
1139
1353
  }, timeoutMs);
1140
1354
  const cleanup = () => {
1141
1355
  clearTimeout(timeout);
@@ -1159,6 +1373,12 @@ var OpenMatesWsClient = class {
1159
1373
  const onMessage = (rawData) => {
1160
1374
  try {
1161
1375
  const parsed = JSON.parse(rawData.toString());
1376
+ const protocolError = websocketProtocolError(parsed);
1377
+ if (protocolError) {
1378
+ cleanup();
1379
+ reject(protocolError);
1380
+ return;
1381
+ }
1162
1382
  if (parsed.type === terminatorType) {
1163
1383
  cleanup();
1164
1384
  resolve7(collected);
@@ -1218,8 +1438,11 @@ var OpenMatesWsClient = class {
1218
1438
  let taskId = null;
1219
1439
  let category = null;
1220
1440
  let modelName = null;
1441
+ let recoveryJobId = null;
1221
1442
  let followUpSuggestions = [];
1222
1443
  let newChatSuggestions = [];
1444
+ let taskProposals = [];
1445
+ let taskUpdateProposals = [];
1223
1446
  const subChatEvents = [];
1224
1447
  const pendingSubChatHandlers = /* @__PURE__ */ new Set();
1225
1448
  const pendingMemoryRequestHandlers = /* @__PURE__ */ new Set();
@@ -1248,6 +1471,8 @@ var OpenMatesWsClient = class {
1248
1471
  if (typeof p.category === "string" && p.category) category = p.category;
1249
1472
  if (typeof p.model_name === "string" && p.model_name)
1250
1473
  modelName = p.model_name;
1474
+ if (typeof p.recovery_job_id === "string" && p.recovery_job_id && p.recovery_protocol_version === 1)
1475
+ recoveryJobId = p.recovery_job_id;
1251
1476
  };
1252
1477
  const extractMessageContent = (message) => {
1253
1478
  if (typeof message.content === "string") return message.content;
@@ -1278,8 +1503,11 @@ var OpenMatesWsClient = class {
1278
1503
  modelName,
1279
1504
  followUpSuggestions,
1280
1505
  newChatSuggestions,
1506
+ taskProposals,
1507
+ taskUpdateProposals,
1281
1508
  embeds: [...embeds.values()],
1282
- subChatEvents
1509
+ subChatEvents,
1510
+ recoveryJobId
1283
1511
  });
1284
1512
  return;
1285
1513
  }
@@ -1298,8 +1526,11 @@ var OpenMatesWsClient = class {
1298
1526
  modelName,
1299
1527
  followUpSuggestions,
1300
1528
  newChatSuggestions,
1529
+ taskProposals,
1530
+ taskUpdateProposals,
1301
1531
  embeds: [...embeds.values()],
1302
- subChatEvents
1532
+ subChatEvents,
1533
+ recoveryJobId
1303
1534
  });
1304
1535
  }, asyncEmbedWaitMs);
1305
1536
  return;
@@ -1315,8 +1546,11 @@ var OpenMatesWsClient = class {
1315
1546
  modelName,
1316
1547
  followUpSuggestions,
1317
1548
  newChatSuggestions,
1549
+ taskProposals,
1550
+ taskUpdateProposals,
1318
1551
  embeds: [...embeds.values()],
1319
- subChatEvents
1552
+ subChatEvents,
1553
+ recoveryJobId
1320
1554
  });
1321
1555
  };
1322
1556
  const handleSubChatEvent = (type, p) => {
@@ -1388,13 +1622,10 @@ var OpenMatesWsClient = class {
1388
1622
  const parsed = JSON.parse(rawData.toString());
1389
1623
  const p = parsed.payload ?? {};
1390
1624
  const type = parsed.type;
1391
- if (type === "error") {
1625
+ const protocolError = websocketProtocolError(parsed);
1626
+ if (protocolError) {
1392
1627
  cleanup();
1393
- reject(
1394
- new Error(
1395
- typeof p.message === "string" ? p.message : "Unknown chat error"
1396
- )
1397
- );
1628
+ reject(protocolError);
1398
1629
  return;
1399
1630
  }
1400
1631
  if (SUB_CHAT_EVENT_TYPES.has(type)) {
@@ -1501,6 +1732,8 @@ var OpenMatesWsClient = class {
1501
1732
  (s) => typeof s === "string" && s.length > 0
1502
1733
  );
1503
1734
  }
1735
+ taskProposals = parseTaskProposals(p.task_proposals);
1736
+ taskUpdateProposals = parseTaskUpdateProposals(p.task_update_proposals);
1504
1737
  if (aiResponseDone) {
1505
1738
  if (postProcessingTimer) {
1506
1739
  clearTimeout(postProcessingTimer);
@@ -1529,8 +1762,11 @@ var OpenMatesWsClient = class {
1529
1762
  modelName,
1530
1763
  followUpSuggestions,
1531
1764
  newChatSuggestions,
1765
+ taskProposals,
1766
+ taskUpdateProposals,
1532
1767
  embeds: [...embeds.values()],
1533
- subChatEvents
1768
+ subChatEvents,
1769
+ recoveryJobId
1534
1770
  });
1535
1771
  return;
1536
1772
  }
@@ -1957,7 +2193,7 @@ async function encryptEmbed(embed, masterKey, chatKey, chatId, messageId, userId
1957
2193
  const hashedMessageId = computeSHA256(messageId);
1958
2194
  const hashedUserId = computeSHA256(userId);
1959
2195
  const wrappedWithMaster = await wrapKey(embedKey, masterKey);
1960
- const nowSeconds = Math.floor(Date.now() / 1e3);
2196
+ const nowSeconds2 = Math.floor(Date.now() / 1e3);
1961
2197
  const embedKeys = [
1962
2198
  {
1963
2199
  hashed_embed_id: hashedEmbedId,
@@ -1965,7 +2201,7 @@ async function encryptEmbed(embed, masterKey, chatKey, chatId, messageId, userId
1965
2201
  hashed_chat_id: null,
1966
2202
  encrypted_embed_key: wrappedWithMaster,
1967
2203
  hashed_user_id: hashedUserId,
1968
- created_at: nowSeconds
2204
+ created_at: nowSeconds2
1969
2205
  }
1970
2206
  ];
1971
2207
  if (chatKey) {
@@ -1976,7 +2212,7 @@ async function encryptEmbed(embed, masterKey, chatKey, chatId, messageId, userId
1976
2212
  hashed_chat_id: hashedChatId,
1977
2213
  encrypted_embed_key: wrappedWithChat,
1978
2214
  hashed_user_id: hashedUserId,
1979
- created_at: nowSeconds
2215
+ created_at: nowSeconds2
1980
2216
  });
1981
2217
  }
1982
2218
  return {
@@ -1991,8 +2227,8 @@ async function encryptEmbed(embed, masterKey, chatKey, chatId, messageId, userId
1991
2227
  file_path: embed.filePath,
1992
2228
  content_hash: embed.contentHash,
1993
2229
  text_length_chars: embed.textLengthChars,
1994
- created_at: nowSeconds,
1995
- updated_at: nowSeconds,
2230
+ created_at: nowSeconds2,
2231
+ updated_at: nowSeconds2,
1996
2232
  embed_keys: embedKeys
1997
2233
  };
1998
2234
  } catch (error) {
@@ -2137,11 +2373,11 @@ async function decryptConnectedAccountCliTransferPayload(encryptedPayload, passc
2137
2373
  throw new Error("Unsupported connected account import payload format.");
2138
2374
  }
2139
2375
  try {
2140
- const key = await deriveTransferKey(passcode, base64UrlToBytes(envelope.kdf.salt), envelope.kdf.iterations);
2376
+ const key = await deriveTransferKey(passcode, base64UrlToBytes2(envelope.kdf.salt), envelope.kdf.iterations);
2141
2377
  const plaintext = await webcrypto5.subtle.decrypt(
2142
- { name: "AES-GCM", iv: toArrayBuffer3(base64UrlToBytes(envelope.cipher.iv)) },
2378
+ { name: "AES-GCM", iv: toArrayBuffer3(base64UrlToBytes2(envelope.cipher.iv)) },
2143
2379
  key,
2144
- toArrayBuffer3(base64UrlToBytes(envelope.cipher.text))
2380
+ toArrayBuffer3(base64UrlToBytes2(envelope.cipher.text))
2145
2381
  );
2146
2382
  return validateTransferPayload(JSON.parse(new TextDecoder().decode(plaintext)));
2147
2383
  } catch (error) {
@@ -2189,7 +2425,7 @@ async function buildEncryptedConnectedAccountImportRow(params) {
2189
2425
  }
2190
2426
  function parseEnvelope(encodedEnvelope) {
2191
2427
  try {
2192
- return JSON.parse(new TextDecoder().decode(base64UrlToBytes(encodedEnvelope)));
2428
+ return JSON.parse(new TextDecoder().decode(base64UrlToBytes2(encodedEnvelope)));
2193
2429
  } catch {
2194
2430
  throw new Error("Connected account import payload is malformed.");
2195
2431
  }
@@ -2269,7 +2505,7 @@ function defaultProviderLabel(providerId) {
2269
2505
  function sha256Hex2(value) {
2270
2506
  return createHash4("sha256").update(value).digest("hex");
2271
2507
  }
2272
- function base64UrlToBytes(value) {
2508
+ function base64UrlToBytes2(value) {
2273
2509
  return base64ToBytes(value.replace(/-/g, "+").replace(/_/g, "/"));
2274
2510
  }
2275
2511
  function toArrayBuffer3(input) {
@@ -2863,6 +3099,15 @@ var MEMORY_TYPE_REGISTRY = {
2863
3099
  properties: { url: { type: "string" }, notes: { type: "string" } }
2864
3100
  }
2865
3101
  };
3102
+ function reconcileAuthoritativeChats(chats, evidence) {
3103
+ const deletedIds = new Set(evidence.deleted_chat_ids ?? []);
3104
+ const authoritativeIds = evidence.authoritative && evidence.authoritative_chat_ids ? new Set(evidence.authoritative_chat_ids) : null;
3105
+ if (!authoritativeIds && deletedIds.size === 0) return chats;
3106
+ return chats.filter((chat) => {
3107
+ const chatId = String(chat.details.id ?? "");
3108
+ return (!authoritativeIds || authoritativeIds.has(chatId)) && !deletedIds.has(chatId);
3109
+ });
3110
+ }
2866
3111
  var MATE_NAMES = {
2867
3112
  software_development: "Sophia",
2868
3113
  business_development: "Burton",
@@ -3148,6 +3393,8 @@ var OpenMatesClient = class _OpenMatesClient {
3148
3393
  modelName: response.data.modelName ?? null,
3149
3394
  mateName: null,
3150
3395
  followUpSuggestions: response.data.followUpSuggestions ?? [],
3396
+ taskProposals: [],
3397
+ taskUpdateProposals: [],
3151
3398
  subChatEvents: [],
3152
3399
  appSettingsMemoryRequests: []
3153
3400
  };
@@ -3635,6 +3882,157 @@ var OpenMatesClient = class _OpenMatesClient {
3635
3882
  hasMore: offset + limit < total
3636
3883
  };
3637
3884
  }
3885
+ async saveDraft(params) {
3886
+ const markdown = params.markdown.trim();
3887
+ if (!markdown) throw new Error("Draft markdown must not be empty.");
3888
+ const masterKey = this.getMasterKeyBytes();
3889
+ const encrypted = await this.saveEncryptedDraft({
3890
+ chatId: params.chatId ?? randomUUID3(),
3891
+ encryptedDraftMd: await encryptWithAesGcmCombined(markdown, masterKey),
3892
+ encryptedDraftPreview: params.preview ? await encryptWithAesGcmCombined(params.preview, masterKey) : null
3893
+ });
3894
+ return { ...encrypted, markdown, preview: params.preview ?? null };
3895
+ }
3896
+ async saveEncryptedDraft(params) {
3897
+ const { ws } = await this.openWsClient();
3898
+ try {
3899
+ const receipt = ws.waitForMessage(
3900
+ "draft_update_receipt",
3901
+ (payload) => payload.chat_id === params.chatId
3902
+ );
3903
+ await ws.sendAsync("update_draft", {
3904
+ chat_id: params.chatId,
3905
+ encrypted_draft_md: params.encryptedDraftMd,
3906
+ encrypted_draft_preview: params.encryptedDraftPreview ?? null
3907
+ });
3908
+ const response = await receipt;
3909
+ const draftV = Number(response.payload.draft_v ?? 0);
3910
+ this.storeEncryptedDraft({
3911
+ chatId: params.chatId,
3912
+ encryptedDraftMd: params.encryptedDraftMd,
3913
+ encryptedDraftPreview: params.encryptedDraftPreview ?? null,
3914
+ draftV
3915
+ });
3916
+ return {
3917
+ chatId: params.chatId,
3918
+ encryptedDraftMd: params.encryptedDraftMd,
3919
+ encryptedDraftPreview: params.encryptedDraftPreview ?? null,
3920
+ draftV
3921
+ };
3922
+ } finally {
3923
+ ws.close();
3924
+ }
3925
+ }
3926
+ async listDrafts(forceRefresh = false) {
3927
+ if (forceRefresh) await this.reconcileDraftVersions();
3928
+ const cache = await this.ensureSynced(forceRefresh);
3929
+ const drafts = [];
3930
+ for (const chat of cache.chats) {
3931
+ const draft = await this.decryptCachedDraft(chat);
3932
+ if (draft) drafts.push(draft);
3933
+ }
3934
+ return drafts;
3935
+ }
3936
+ async getDraft(chatId, forceRefresh = false) {
3937
+ if (forceRefresh) await this.reconcileDraftVersions();
3938
+ const cache = await this.ensureSynced(forceRefresh);
3939
+ const chat = cache.chats.find((entry) => String(entry.details.id ?? "") === chatId);
3940
+ return chat ? this.decryptCachedDraft(chat) : null;
3941
+ }
3942
+ async clearDraft(chatId) {
3943
+ const { ws } = await this.openWsClient();
3944
+ try {
3945
+ const receipt = ws.waitForMessage(
3946
+ "draft_delete_receipt",
3947
+ (payload) => payload.chat_id === chatId
3948
+ );
3949
+ await ws.sendAsync("delete_draft", { chat_id: chatId });
3950
+ await receipt;
3951
+ } finally {
3952
+ ws.close();
3953
+ }
3954
+ const cache = loadSyncCache();
3955
+ if (!cache) return;
3956
+ const chat = cache.chats.find((entry) => String(entry.details.id ?? "") === chatId);
3957
+ if (chat) {
3958
+ delete chat.details.encrypted_draft_md;
3959
+ delete chat.details.encrypted_draft_preview;
3960
+ chat.details.draft_v = 0;
3961
+ if (chat.messages.length === 0 && !chat.details.encrypted_chat_key) {
3962
+ cache.chats = cache.chats.filter((entry) => entry !== chat);
3963
+ }
3964
+ saveSyncCache(cache);
3965
+ }
3966
+ }
3967
+ async reconcileDraftVersions() {
3968
+ const cache = loadSyncCache();
3969
+ const drafts = (cache?.chats ?? []).filter(
3970
+ (chat) => typeof chat.details.encrypted_draft_md === "string"
3971
+ );
3972
+ if (drafts.length === 0) return {};
3973
+ const { ws } = await this.openWsClient();
3974
+ try {
3975
+ const response = ws.waitForMessage("draft_versions_response");
3976
+ await ws.sendAsync("get_draft_versions", {
3977
+ chats: drafts.map((chat) => ({
3978
+ chat_id: String(chat.details.id),
3979
+ client_draft_v: Number(chat.details.draft_v ?? 0)
3980
+ }))
3981
+ });
3982
+ const frame = await response;
3983
+ const versions = frame.payload.versions ?? {};
3984
+ for (const chat of drafts) {
3985
+ const chatId = String(chat.details.id);
3986
+ if (versions[chatId] === 0) {
3987
+ delete chat.details.encrypted_draft_md;
3988
+ delete chat.details.encrypted_draft_preview;
3989
+ chat.details.draft_v = 0;
3990
+ }
3991
+ }
3992
+ if (cache) saveSyncCache(cache);
3993
+ return versions;
3994
+ } finally {
3995
+ ws.close();
3996
+ }
3997
+ }
3998
+ storeEncryptedDraft(draft) {
3999
+ const cache = loadSyncCache() ?? {
4000
+ syncedAt: 0,
4001
+ totalChatCount: 0,
4002
+ loadedChatCount: 0,
4003
+ chats: [],
4004
+ embeds: [],
4005
+ embedKeys: []
4006
+ };
4007
+ let chat = cache.chats.find((entry) => String(entry.details.id ?? "") === draft.chatId);
4008
+ if (!chat) {
4009
+ chat = { details: { id: draft.chatId }, messages: [] };
4010
+ cache.chats.unshift(chat);
4011
+ }
4012
+ chat.details.encrypted_draft_md = draft.encryptedDraftMd;
4013
+ chat.details.encrypted_draft_preview = draft.encryptedDraftPreview;
4014
+ chat.details.draft_v = draft.draftV;
4015
+ cache.syncedAt = Date.now();
4016
+ cache.loadedChatCount = cache.chats.length;
4017
+ saveSyncCache(cache);
4018
+ }
4019
+ async decryptCachedDraft(chat) {
4020
+ const encryptedDraftMd = chat.details.encrypted_draft_md;
4021
+ if (typeof encryptedDraftMd !== "string") return null;
4022
+ const encryptedPreview = typeof chat.details.encrypted_draft_preview === "string" ? chat.details.encrypted_draft_preview : null;
4023
+ const masterKey = this.getMasterKeyBytes();
4024
+ const markdown = await decryptWithAesGcmCombined(encryptedDraftMd, masterKey);
4025
+ if (markdown === null) throw new Error("Failed to decrypt draft markdown.");
4026
+ const preview = encryptedPreview ? await decryptWithAesGcmCombined(encryptedPreview, masterKey) : markdown.slice(0, 160);
4027
+ return {
4028
+ chatId: String(chat.details.id ?? ""),
4029
+ encryptedDraftMd,
4030
+ encryptedDraftPreview: encryptedPreview,
4031
+ draftV: Number(chat.details.draft_v ?? 0),
4032
+ markdown,
4033
+ preview
4034
+ };
4035
+ }
3638
4036
  async searchChats(query) {
3639
4037
  const cache = await this.ensureSynced();
3640
4038
  const masterKey = this.getMasterKeyBytes();
@@ -3727,8 +4125,8 @@ var OpenMatesClient = class _OpenMatesClient {
3727
4125
  );
3728
4126
  let msgEmbedIds = [];
3729
4127
  if (clientMsgId && cache.embeds.length > 0) {
3730
- const { createHash: createHash7 } = await import("crypto");
3731
- const hashed = createHash7("sha256").update(clientMsgId).digest("hex");
4128
+ const { createHash: createHash8 } = await import("crypto");
4129
+ const hashed = createHash8("sha256").update(clientMsgId).digest("hex");
3732
4130
  msgEmbedIds = cache.embeds.filter(
3733
4131
  (e) => e.hashed_message_id === hashed && // Only include parent embeds (no parent_embed_id).
3734
4132
  // Child embeds inherit the parent's key and are loaded
@@ -3775,8 +4173,8 @@ var OpenMatesClient = class _OpenMatesClient {
3775
4173
  );
3776
4174
  }
3777
4175
  const embedId = String(embed.embed_id ?? embed.id ?? "");
3778
- const { createHash: createHash7 } = await import("crypto");
3779
- const hashedEmbedId = createHash7("sha256").update(embedId).digest("hex");
4176
+ const { createHash: createHash8 } = await import("crypto");
4177
+ const hashedEmbedId = createHash8("sha256").update(embedId).digest("hex");
3780
4178
  const embedKeyBytes = await this.resolveEmbedKey(
3781
4179
  cache,
3782
4180
  masterKey,
@@ -3884,7 +4282,7 @@ var OpenMatesClient = class _OpenMatesClient {
3884
4282
  async resolveEmbedKey(cache, masterKey, embed, embedId, hashedEmbedId, visited = /* @__PURE__ */ new Set()) {
3885
4283
  if (visited.has(embedId)) return null;
3886
4284
  visited.add(embedId);
3887
- const { createHash: createHash7 } = await import("crypto");
4285
+ const { createHash: createHash8 } = await import("crypto");
3888
4286
  const masterKeyEntry = cache.embedKeys.find(
3889
4287
  (ek) => ek.hashed_embed_id === hashedEmbedId && String(ek.key_type) === "master"
3890
4288
  );
@@ -3901,7 +4299,7 @@ var OpenMatesClient = class _OpenMatesClient {
3901
4299
  if (chatKeyEntry && typeof chatKeyEntry.encrypted_embed_key === "string") {
3902
4300
  const hashedChatId = String(chatKeyEntry.hashed_chat_id ?? "");
3903
4301
  const owningChat = cache.chats.find((c) => {
3904
- const chatHash = createHash7("sha256").update(String(c.details.id ?? "")).digest("hex");
4302
+ const chatHash = createHash8("sha256").update(String(c.details.id ?? "")).digest("hex");
3905
4303
  return chatHash === hashedChatId;
3906
4304
  });
3907
4305
  if (owningChat) {
@@ -3930,7 +4328,7 @@ var OpenMatesClient = class _OpenMatesClient {
3930
4328
  const parentFullId = String(
3931
4329
  parentEmbed2.embed_id ?? parentEmbed2.id ?? ""
3932
4330
  );
3933
- const parentHashedId = createHash7("sha256").update(parentFullId).digest("hex");
4331
+ const parentHashedId = createHash8("sha256").update(parentFullId).digest("hex");
3934
4332
  const parentKey = await this.resolveEmbedKey(
3935
4333
  cache,
3936
4334
  masterKey,
@@ -4074,7 +4472,11 @@ var OpenMatesClient = class _OpenMatesClient {
4074
4472
  }
4075
4473
  }
4076
4474
  }
4077
- const { ws, session } = await this.openWsClient();
4475
+ const { ws, session, ownerId } = await this.openWsClient();
4476
+ if (!params.incognito && !ownerId) {
4477
+ ws.close();
4478
+ throw new Error("Authenticated user identity is required for saved chat recovery.");
4479
+ }
4078
4480
  const messageId = randomUUID3();
4079
4481
  const createdAt = Math.floor(Date.now() / 1e3);
4080
4482
  const isNewChat = !params.chatId;
@@ -4089,6 +4491,41 @@ var OpenMatesClient = class _OpenMatesClient {
4089
4491
  }) : [];
4090
4492
  assertNoConnectedAccountSecretLeak(connectedAccountTokenRefs);
4091
4493
  const piiMappings = params.piiMappings ?? [];
4494
+ let chatKeyBytes = null;
4495
+ let encryptedChatKey = null;
4496
+ let baselineMessagesV = 0;
4497
+ let terminalExpectedMessagesV = 1;
4498
+ let savedTurnId = null;
4499
+ if (!params.incognito) {
4500
+ const masterKey = this.getMasterKeyBytes();
4501
+ if (isNewChat) {
4502
+ chatKeyBytes = globalThis.crypto ? new Uint8Array(globalThis.crypto.getRandomValues(new Uint8Array(32))) : new Uint8Array(
4503
+ (await import("crypto")).webcrypto.getRandomValues(
4504
+ new Uint8Array(32)
4505
+ )
4506
+ );
4507
+ encryptedChatKey = await encryptBytesWithAesGcm(
4508
+ chatKeyBytes,
4509
+ masterKey
4510
+ );
4511
+ } else {
4512
+ const cache = loadSyncCache() ?? await this.ensureSynced();
4513
+ const chat = cache.chats.find(
4514
+ (c) => String(c.details.id ?? "") === chatId || String(c.details.id ?? "").startsWith(chatId)
4515
+ );
4516
+ if (chat) {
4517
+ baselineMessagesV = typeof chat.details.messages_v === "number" ? chat.details.messages_v : 0;
4518
+ const encKey = typeof chat.details.encrypted_chat_key === "string" ? chat.details.encrypted_chat_key : null;
4519
+ if (encKey) {
4520
+ chatKeyBytes = await decryptBytesWithAesGcm(encKey, masterKey);
4521
+ encryptedChatKey = encKey;
4522
+ }
4523
+ }
4524
+ if (!chatKeyBytes || !encryptedChatKey) {
4525
+ throw new Error(`Encrypted chat key not found for chat '${chatId}'. Sync and try again.`);
4526
+ }
4527
+ }
4528
+ }
4092
4529
  const messagePayload = {
4093
4530
  chat_id: chatId,
4094
4531
  is_incognito: Boolean(params.incognito),
@@ -4103,6 +4540,9 @@ var OpenMatesClient = class _OpenMatesClient {
4103
4540
  chat_has_title: Boolean(params.chatId)
4104
4541
  }
4105
4542
  };
4543
+ if (isNewChat && encryptedChatKey) {
4544
+ messagePayload.encrypted_chat_key = encryptedChatKey;
4545
+ }
4106
4546
  if (memoryMetadataKeys.length > 0) {
4107
4547
  messagePayload.app_settings_memories_metadata = memoryMetadataKeys;
4108
4548
  }
@@ -4144,37 +4584,6 @@ var OpenMatesClient = class _OpenMatesClient {
4144
4584
  created_at: historyMessage.created_at
4145
4585
  }));
4146
4586
  }
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
4587
  if (params.preparedEmbeds && params.preparedEmbeds.length > 0) {
4179
4588
  messagePayload.embeds = params.preparedEmbeds.map((embed) => ({
4180
4589
  embed_id: embed.embedId,
@@ -4202,52 +4611,107 @@ var OpenMatesClient = class _OpenMatesClient {
4202
4611
  if (encryptedEmbeds.length > 0) {
4203
4612
  messagePayload.encrypted_embeds = encryptedEmbeds;
4204
4613
  }
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) {
4614
+ let precollectedResponse = params.precollectResponse && params.incognito ? ws.collectAiResponse(messageId, chatId, { onStream: params.onStream }) : null;
4615
+ if (!params.incognito && chatKeyBytes && encryptedChatKey) {
4616
+ const protocolVersion = 1;
4617
+ const chatKeyVersion = 1;
4618
+ const turnId = randomUUID3();
4619
+ savedTurnId = turnId;
4620
+ terminalExpectedMessagesV = baselineMessagesV + 1;
4621
+ const recoveryKeypair = await deriveChatCompletionRecoveryKeypair(
4622
+ Buffer.from(chatKeyBytes).toString("base64url"),
4623
+ chatId,
4624
+ chatKeyVersion
4625
+ );
4217
4626
  const encryptedContent = await encryptWithAesGcmCombined(
4218
4627
  params.message,
4219
4628
  chatKeyBytes
4220
4629
  );
4221
- const encryptedSenderName = await encryptWithAesGcmCombined(
4222
- "User",
4223
- chatKeyBytes
4224
- );
4225
- const metadataPayload = {
4630
+ const encryptedUserMessage = {
4631
+ client_message_id: messageId,
4226
4632
  chat_id: chatId,
4227
- message_id: messageId,
4228
4633
  encrypted_content: encryptedContent,
4229
- encrypted_sender_name: encryptedSenderName,
4634
+ role: "user",
4230
4635
  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
- }
4636
+ updated_at: createdAt
4237
4637
  };
4238
4638
  if (piiMappings.length > 0) {
4239
- metadataPayload.encrypted_pii_mappings = await encryptWithAesGcmCombined(
4639
+ encryptedUserMessage.encrypted_pii_mappings = await encryptWithAesGcmCombined(
4240
4640
  JSON.stringify(piiMappings),
4241
4641
  chatKeyBytes
4242
4642
  );
4243
4643
  }
4244
- ws.send("encrypted_chat_metadata", metadataPayload);
4644
+ Object.assign(messagePayload, {
4645
+ turn_id: turnId,
4646
+ recovery_public_key: recoveryKeypair.publicKey,
4647
+ chat_key_version: chatKeyVersion
4648
+ });
4649
+ const preflightPayload = {
4650
+ protocol_version: protocolVersion,
4651
+ chat_id: chatId,
4652
+ turn_id: turnId,
4653
+ message_id: messageId,
4654
+ chat_key_version: chatKeyVersion,
4655
+ encrypted_chat_key: encryptedChatKey,
4656
+ recovery_public_key: recoveryKeypair.publicKey,
4657
+ expected_messages_v: baselineMessagesV,
4658
+ encrypted_user_message: encryptedUserMessage,
4659
+ inference_request: messagePayload
4660
+ };
4661
+ if (isNewChat) {
4662
+ preflightPayload.encrypted_chat_metadata = {
4663
+ encrypted_title: await encryptWithAesGcmCombined("", chatKeyBytes),
4664
+ encrypted_chat_key: encryptedChatKey,
4665
+ created_at: createdAt,
4666
+ updated_at: createdAt
4667
+ };
4668
+ }
4669
+ const preflightAck = ws.waitForMessage("chat_turn_preflight_ack");
4670
+ let ackPayload;
4671
+ try {
4672
+ await ws.sendAsync("chat_turn_preflight", preflightPayload);
4673
+ ackPayload = (await preflightAck).payload;
4674
+ } catch (error) {
4675
+ ws.close();
4676
+ throw error;
4677
+ }
4678
+ if (typeof ackPayload.preflight_id !== "string" || !ackPayload.preflight_id) {
4679
+ ws.close();
4680
+ throw new Error("Encrypted chat preflight acknowledgement omitted preflight_id.");
4681
+ }
4682
+ Object.assign(messagePayload, {
4683
+ protocol_version: protocolVersion,
4684
+ preflight_id: ackPayload.preflight_id
4685
+ });
4686
+ if (typeof ackPayload.committed_messages_v === "number" && Number.isSafeInteger(ackPayload.committed_messages_v)) {
4687
+ terminalExpectedMessagesV = ackPayload.committed_messages_v;
4688
+ }
4689
+ }
4690
+ if (params.precollectResponse && !params.incognito) {
4691
+ precollectedResponse = ws.collectAiResponse(messageId, chatId, {
4692
+ onStream: params.onStream
4693
+ });
4694
+ }
4695
+ const confirmed = ws.waitForMessage(
4696
+ "chat_message_confirmed",
4697
+ (payload) => {
4698
+ const eventPayload = payload;
4699
+ return eventPayload.chat_id === chatId && eventPayload.message_id === messageId;
4700
+ },
4701
+ 2e4
4702
+ );
4703
+ await ws.sendAsync("chat_message_added", messagePayload);
4704
+ const confirmedPayload = (await confirmed).payload;
4705
+ if (typeof confirmedPayload.new_messages_v === "number" && Number.isSafeInteger(confirmedPayload.new_messages_v)) {
4706
+ terminalExpectedMessagesV = confirmedPayload.new_messages_v + 1;
4245
4707
  }
4246
4708
  let assistant = "";
4247
4709
  let assistantMessageId = null;
4248
4710
  let category = null;
4249
4711
  let modelName = null;
4250
4712
  let followUpSuggestions = [];
4713
+ let taskProposals = [];
4714
+ let taskUpdateProposals = [];
4251
4715
  let subChatEvents = [];
4252
4716
  const appSettingsMemoryRequests = [];
4253
4717
  const numberOrNull = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
@@ -4433,6 +4897,8 @@ var OpenMatesClient = class _OpenMatesClient {
4433
4897
  modelName,
4434
4898
  mateName: category ? MATE_NAMES[category] ?? null : null,
4435
4899
  followUpSuggestions,
4900
+ taskProposals,
4901
+ taskUpdateProposals,
4436
4902
  subChatEvents,
4437
4903
  appSettingsMemoryRequests
4438
4904
  };
@@ -4442,12 +4908,14 @@ var OpenMatesClient = class _OpenMatesClient {
4442
4908
  }
4443
4909
  } else {
4444
4910
  try {
4445
- const resp = await ws.collectAiResponse(messageId, chatId, streamOpts);
4911
+ const resp = await (precollectedResponse ?? ws.collectAiResponse(messageId, chatId, streamOpts));
4446
4912
  assistantMessageId = resp.messageId;
4447
4913
  assistant = resp.content;
4448
4914
  category = resp.category;
4449
4915
  modelName = resp.modelName;
4450
4916
  followUpSuggestions = resp.followUpSuggestions;
4917
+ taskProposals = resp.taskProposals;
4918
+ taskUpdateProposals = resp.taskUpdateProposals;
4451
4919
  subChatEvents = resp.subChatEvents;
4452
4920
  if (resp.status === "waiting_for_user") {
4453
4921
  return {
@@ -4459,51 +4927,116 @@ var OpenMatesClient = class _OpenMatesClient {
4459
4927
  modelName,
4460
4928
  mateName: category ? MATE_NAMES[category] ?? null : null,
4461
4929
  followUpSuggestions,
4930
+ taskProposals,
4931
+ taskUpdateProposals,
4462
4932
  subChatEvents,
4463
4933
  appSettingsMemoryRequests
4464
4934
  };
4465
4935
  }
4466
- if (chatKeyBytes && assistant) {
4936
+ if (chatKeyBytes) {
4937
+ if (!resp.recoveryJobId || !savedTurnId || !ownerId) {
4938
+ throw new Error("Saved chat completion did not include recoverable terminal identity.");
4939
+ }
4940
+ const recoveryJobId = resp.recoveryJobId;
4941
+ const claimPromise = ws.waitForMessage(
4942
+ "recovery_job_claimed",
4943
+ (payload) => payload.job_id === recoveryJobId,
4944
+ 2e4
4945
+ );
4946
+ await ws.sendAsync("recovery_job_claim", {
4947
+ protocol_version: 1,
4948
+ job_id: recoveryJobId
4949
+ });
4950
+ const claim = (await claimPromise).payload;
4951
+ const leaseToken = typeof claim.lease_token === "string" ? claim.lease_token : null;
4952
+ const leaseGeneration = typeof claim.lease_generation === "number" && Number.isSafeInteger(claim.lease_generation) ? claim.lease_generation : null;
4953
+ const assistantId = typeof claim.assistant_message_id === "string" ? claim.assistant_message_id : null;
4954
+ const keyVersion = typeof claim.chat_key_version === "number" && Number.isSafeInteger(claim.chat_key_version) ? claim.chat_key_version : null;
4955
+ 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") {
4956
+ throw new Error("Recovery job claim returned invalid lease or identity data.");
4957
+ }
4958
+ const recoveryKeypair = await deriveChatCompletionRecoveryKeypair(
4959
+ Buffer.from(chatKeyBytes).toString("base64url"),
4960
+ chatId,
4961
+ keyVersion
4962
+ );
4963
+ let envelope;
4964
+ try {
4965
+ envelope = JSON.parse(claim.sealed_payload);
4966
+ } catch {
4967
+ throw new Error("Recovery job contained an invalid sealed envelope.");
4968
+ }
4969
+ const plaintext = await openChatCompletionRecoveryEnvelope(
4970
+ envelope,
4971
+ {
4972
+ recoveryPrivateKey: recoveryKeypair.privateKey,
4973
+ ownerId,
4974
+ chatId,
4975
+ turnId: savedTurnId,
4976
+ jobId: recoveryJobId,
4977
+ assistantMessageId: assistantId,
4978
+ keyVersion
4979
+ }
4980
+ );
4981
+ let recovered;
4982
+ try {
4983
+ recovered = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(plaintext));
4984
+ } catch {
4985
+ throw new Error("Recovery job plaintext was not valid UTF-8 JSON.");
4986
+ }
4987
+ const expectedRecoveryFields = [
4988
+ "assistant_message_id",
4989
+ "category",
4990
+ "chat_id",
4991
+ "content",
4992
+ "job_id",
4993
+ "key_version",
4994
+ "model_name",
4995
+ "turn_id"
4996
+ ];
4997
+ 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) {
4998
+ throw new Error("Recovery job plaintext did not match the terminal completion identity.");
4999
+ }
4467
5000
  const completedAt = Math.floor(Date.now() / 1e3);
4468
5001
  const encryptedAssistantContent = await encryptWithAesGcmCombined(
4469
- assistant,
5002
+ recovered.content,
4470
5003
  chatKeyBytes
4471
5004
  );
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,
5005
+ const encryptedSenderName = await encryptWithAesGcmCombined("Assistant", chatKeyBytes);
5006
+ const encryptedCategory = recovered.category ? await encryptWithAesGcmCombined(recovered.category, chatKeyBytes) : void 0;
5007
+ const encryptedModelName = recovered.model_name ? await encryptWithAesGcmCombined(recovered.model_name, chatKeyBytes) : void 0;
5008
+ const persistedPromise = ws.waitForMessage(
5009
+ "recovery_job_persisted",
5010
+ (payload) => payload.job_id === recoveryJobId,
5011
+ 2e4
5012
+ );
5013
+ await ws.sendAsync("recovery_job_persist", {
5014
+ protocol_version: 1,
5015
+ job_id: recoveryJobId,
5016
+ lease_token: leaseToken,
5017
+ lease_generation: leaseGeneration,
5018
+ expected_messages_v: terminalExpectedMessagesV,
5019
+ encrypted_assistant_message: {
5020
+ client_message_id: assistantId,
4479
5021
  chat_id: chatId,
4480
- role: "assistant",
4481
- created_at: completedAt,
4482
- status: "synced",
4483
- user_message_id: messageId,
4484
5022
  encrypted_content: encryptedAssistantContent,
5023
+ encrypted_sender_name: encryptedSenderName,
4485
5024
  encrypted_category: encryptedCategory,
4486
- encrypted_model_name: encryptedModelName
4487
- },
4488
- versions: {
4489
- messages_v: baselineMessagesV + 2,
4490
- last_edited_overall_timestamp: completedAt
5025
+ encrypted_model_name: encryptedModelName,
5026
+ role: "assistant",
5027
+ user_message_id: messageId,
5028
+ created_at: completedAt,
5029
+ updated_at: completedAt
4491
5030
  }
4492
5031
  });
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
- );
5032
+ await persistedPromise;
5033
+ assistantMessageId = assistantId;
4501
5034
  await this.persistStreamedEmbeds({
4502
5035
  ws,
4503
5036
  embeds: resp.embeds,
4504
5037
  chatId,
4505
5038
  chatKeyBytes,
4506
- fallbackMessageId: persistedAssistantMessageId
5039
+ fallbackMessageId: assistantId
4507
5040
  });
4508
5041
  await this.persistPostProcessingMetadata({
4509
5042
  ws,
@@ -4529,6 +5062,8 @@ var OpenMatesClient = class _OpenMatesClient {
4529
5062
  modelName,
4530
5063
  mateName,
4531
5064
  followUpSuggestions,
5065
+ taskProposals,
5066
+ taskUpdateProposals,
4532
5067
  subChatEvents,
4533
5068
  appSettingsMemoryRequests
4534
5069
  };
@@ -5041,6 +5576,30 @@ var OpenMatesClient = class _OpenMatesClient {
5041
5576
  }
5042
5577
  return response.data.workflow;
5043
5578
  }
5579
+ async validateWorkflowYaml(source) {
5580
+ this.requireSession();
5581
+ const response = await this.http.post("/v1/workflows/validate", { source }, this.getCliRequestHeaders());
5582
+ if (!response.ok || !response.data.validation) {
5583
+ throw new Error(`Workflow YAML validation failed with HTTP ${response.status}`);
5584
+ }
5585
+ return response.data.validation;
5586
+ }
5587
+ async createWorkflowYaml(source) {
5588
+ this.requireSession();
5589
+ const response = await this.http.post("/v1/workflows/yaml", { source }, this.getCliRequestHeaders());
5590
+ if (!response.ok || !response.data.workflow || !response.data.validation) {
5591
+ throw new Error(`Workflow YAML create failed with HTTP ${response.status}`);
5592
+ }
5593
+ return { workflow: response.data.workflow, validation: response.data.validation };
5594
+ }
5595
+ async updateWorkflowYaml(workflowId, source) {
5596
+ this.requireSession();
5597
+ const createLike = await this.http.post(`/v1/workflows/${encodeURIComponent(workflowId)}/yaml`, { source }, this.getCliRequestHeaders());
5598
+ if (!createLike.ok || !createLike.data.workflow || !createLike.data.validation) {
5599
+ throw new Error(`Workflow YAML update failed with HTTP ${createLike.status}`);
5600
+ }
5601
+ return { workflow: createLike.data.workflow, validation: createLike.data.validation };
5602
+ }
5044
5603
  async getWorkflow(workflowId) {
5045
5604
  this.requireSession();
5046
5605
  const response = await this.http.get(
@@ -5099,12 +5658,15 @@ var OpenMatesClient = class _OpenMatesClient {
5099
5658
  async disableWorkflow(workflowId) {
5100
5659
  return this.setWorkflowEnabled(workflowId, false);
5101
5660
  }
5102
- async runWorkflow(workflowId, params = {}) {
5661
+ async runWorkflow(workflowId, params) {
5103
5662
  this.requireSession();
5663
+ if (!params.idempotencyKey.trim()) {
5664
+ throw new Error("Workflow run requires a stable idempotencyKey");
5665
+ }
5104
5666
  const response = await this.http.post(
5105
5667
  `/v1/workflows/${encodeURIComponent(workflowId)}/run`,
5106
5668
  { mode: params.mode ?? "manual", input: params.input ?? {} },
5107
- this.getCliRequestHeaders()
5669
+ { ...this.getCliRequestHeaders(), "Idempotency-Key": params.idempotencyKey }
5108
5670
  );
5109
5671
  if (!response.ok || !response.data.run) {
5110
5672
  throw new Error(`Workflow run failed with HTTP ${response.status}`);
@@ -5133,6 +5695,42 @@ var OpenMatesClient = class _OpenMatesClient {
5133
5695
  }
5134
5696
  return response.data.run;
5135
5697
  }
5698
+ async cancelWorkflowRun(workflowId, runId) {
5699
+ this.requireSession();
5700
+ const response = await this.http.post(
5701
+ `/v1/workflows/${encodeURIComponent(workflowId)}/runs/${encodeURIComponent(runId)}/cancel`,
5702
+ {},
5703
+ this.getCliRequestHeaders()
5704
+ );
5705
+ if (!response.ok || response.data.status !== "cancellation_requested" && response.data.status !== "cancelled") {
5706
+ throw new Error(`Workflow run cancellation failed with HTTP ${response.status}`);
5707
+ }
5708
+ return response.data;
5709
+ }
5710
+ async testWorkflowStep(workflowId, stepId, params = {}) {
5711
+ this.requireSession();
5712
+ const response = await this.http.post(
5713
+ `/v1/workflows/${encodeURIComponent(workflowId)}/steps/${encodeURIComponent(stepId)}/test`,
5714
+ { input: params.input ?? {}, confirmed: params.confirmed === true },
5715
+ this.getCliRequestHeaders()
5716
+ );
5717
+ if (!response.ok || !response.data.run) {
5718
+ throw new Error(`Workflow step test failed with HTTP ${response.status}`);
5719
+ }
5720
+ return response.data.run;
5721
+ }
5722
+ async respondToWorkflowRun(workflowId, runId, stepId, input) {
5723
+ this.requireSession();
5724
+ const response = await this.http.post(
5725
+ `/v1/workflows/${encodeURIComponent(workflowId)}/runs/${encodeURIComponent(runId)}/respond`,
5726
+ { step_id: stepId, input },
5727
+ this.getCliRequestHeaders()
5728
+ );
5729
+ if (!response.ok || !response.data.run) {
5730
+ throw new Error(`Workflow response failed with HTTP ${response.status}`);
5731
+ }
5732
+ return response.data.run;
5733
+ }
5136
5734
  async listWorkflowCapabilities() {
5137
5735
  this.requireSession();
5138
5736
  const response = await this.http.get(
@@ -5144,6 +5742,114 @@ var OpenMatesClient = class _OpenMatesClient {
5144
5742
  }
5145
5743
  return response.data.capabilities ?? [];
5146
5744
  }
5745
+ async upsertWorkflowTemplateProjection(workflowId, params) {
5746
+ this.requireSession();
5747
+ const response = await this.http.put(
5748
+ `/v1/workflows/${encodeURIComponent(workflowId)}/template-projection`,
5749
+ {
5750
+ template_id: params.templateId,
5751
+ source_version: params.sourceVersion,
5752
+ ciphertext: params.ciphertext,
5753
+ ciphertext_checksum: params.ciphertextChecksum,
5754
+ owner_wrapped_key: params.ownerWrappedKey,
5755
+ projection_schema_version: params.projectionSchemaVersion
5756
+ },
5757
+ this.getCliRequestHeaders()
5758
+ );
5759
+ if (!response.ok) {
5760
+ throw new Error(`Workflow template projection upsert failed with HTTP ${response.status}`);
5761
+ }
5762
+ return response.data;
5763
+ }
5764
+ async getPublicWorkflowTemplateProjection(templateId) {
5765
+ const response = await this.http.get(
5766
+ `/v1/workflows/template-projections/${encodeURIComponent(templateId)}`,
5767
+ this.getCliRequestHeaders()
5768
+ );
5769
+ if (!response.ok) {
5770
+ throw new Error(`Workflow template projection retrieval failed with HTTP ${response.status}`);
5771
+ }
5772
+ return response.data;
5773
+ }
5774
+ async revokeWorkflowTemplateProjection(workflowId) {
5775
+ this.requireSession();
5776
+ const response = await this.http.post(
5777
+ `/v1/workflows/${encodeURIComponent(workflowId)}/template-projection/revoke`,
5778
+ {},
5779
+ this.getCliRequestHeaders()
5780
+ );
5781
+ if (!response.ok) {
5782
+ throw new Error(`Workflow template projection revoke failed with HTTP ${response.status}`);
5783
+ }
5784
+ return response.data;
5785
+ }
5786
+ async unrevokeWorkflowTemplateProjection(workflowId) {
5787
+ this.requireSession();
5788
+ const response = await this.http.post(
5789
+ `/v1/workflows/${encodeURIComponent(workflowId)}/template-projection/unrevoke`,
5790
+ {},
5791
+ this.getCliRequestHeaders()
5792
+ );
5793
+ if (!response.ok) {
5794
+ throw new Error(`Workflow template projection unrevoke failed with HTTP ${response.status}`);
5795
+ }
5796
+ return response.data;
5797
+ }
5798
+ async completeImportedWorkflowBinding(workflowId, params) {
5799
+ this.requireSession();
5800
+ const response = await this.http.post(
5801
+ `/v1/workflows/${encodeURIComponent(workflowId)}/binding-requirements/complete`,
5802
+ { type: params.type, node_id: params.nodeId },
5803
+ this.getCliRequestHeaders()
5804
+ );
5805
+ if (!response.ok) {
5806
+ throw new Error(`Workflow imported binding completion failed with HTTP ${response.status}`);
5807
+ }
5808
+ return response.data;
5809
+ }
5810
+ async createWorkflowTemplateShortUrl(params) {
5811
+ this.requireSession();
5812
+ const response = await this.http.post(
5813
+ "/v1/share/short-url",
5814
+ {
5815
+ token: params.token,
5816
+ encrypted_url: params.encryptedUrl,
5817
+ content_type: "workflow_template",
5818
+ content_id: params.templateId,
5819
+ password_protected: params.passwordProtected ?? false,
5820
+ ...params.ttlSeconds !== void 0 ? { ttl_seconds: params.ttlSeconds } : {}
5821
+ },
5822
+ this.getCliRequestHeaders()
5823
+ );
5824
+ if (!response.ok) {
5825
+ throw new Error(`Workflow template short URL create failed with HTTP ${response.status}`);
5826
+ }
5827
+ return response.data;
5828
+ }
5829
+ async revokeShortUrl(token) {
5830
+ this.requireSession();
5831
+ const response = await this.http.delete(
5832
+ `/v1/share/short-url/${encodeURIComponent(token)}`,
5833
+ void 0,
5834
+ this.getCliRequestHeaders()
5835
+ );
5836
+ if (!response.ok) {
5837
+ throw new Error(`Short URL revoke failed with HTTP ${response.status}`);
5838
+ }
5839
+ return response.data;
5840
+ }
5841
+ async importWorkflowTemplate(payload) {
5842
+ this.requireSession();
5843
+ const response = await this.http.post(
5844
+ "/v1/workflows/template-import",
5845
+ payload,
5846
+ this.getCliRequestHeaders()
5847
+ );
5848
+ if (!response.ok || !response.data.workflow) {
5849
+ throw new Error(`Workflow template import failed with HTTP ${response.status}`);
5850
+ }
5851
+ return response.data.workflow;
5852
+ }
5147
5853
  async startWorkflowInput(params) {
5148
5854
  this.requireSession();
5149
5855
  const response = await this.http.post(
@@ -5290,6 +5996,22 @@ var OpenMatesClient = class _OpenMatesClient {
5290
5996
  }
5291
5997
  return response.data.task;
5292
5998
  }
5999
+ async extractUserTaskProposals(input) {
6000
+ const response = await this.http.post(
6001
+ "/v1/user-tasks/extract",
6002
+ {
6003
+ corrected_text: input.correctedText,
6004
+ mode: input.mode ?? "create",
6005
+ context_chat_id: input.contextChatId ?? null,
6006
+ project_ids: input.projectIds ?? []
6007
+ },
6008
+ this.getCliRequestHeaders()
6009
+ );
6010
+ if (!response.ok || !Array.isArray(response.data.proposed_tasks)) {
6011
+ throw new Error(`Failed to extract task proposals (HTTP ${response.status})`);
6012
+ }
6013
+ return response.data.proposed_tasks;
6014
+ }
5293
6015
  async updateUserTask(taskId, input) {
5294
6016
  this.requireSession();
5295
6017
  const response = await this.http.patch(
@@ -5314,6 +6036,54 @@ var OpenMatesClient = class _OpenMatesClient {
5314
6036
  }
5315
6037
  return response.data.task;
5316
6038
  }
6039
+ async deleteUserTask(taskId) {
6040
+ this.requireSession();
6041
+ const response = await this.http.delete(
6042
+ `/v1/user-tasks/${encodeURIComponent(taskId)}`,
6043
+ void 0,
6044
+ this.getCliRequestHeaders()
6045
+ );
6046
+ if (!response.ok) {
6047
+ throw new Error(`User task delete failed with HTTP ${response.status}`);
6048
+ }
6049
+ return response.data;
6050
+ }
6051
+ async completeUserTask(taskId, input = {}) {
6052
+ return this.postUserTaskAction(taskId, "complete", input);
6053
+ }
6054
+ async blockUserTask(taskId, input = {}) {
6055
+ return this.postUserTaskAction(taskId, "block", input);
6056
+ }
6057
+ async unblockUserTask(taskId, input = {}) {
6058
+ return this.postUserTaskAction(taskId, "unblock", input);
6059
+ }
6060
+ async skipUserTask(taskId, input = {}) {
6061
+ return this.postUserTaskAction(taskId, "skip", input);
6062
+ }
6063
+ async reorderUserTasks(input) {
6064
+ this.requireSession();
6065
+ const response = await this.http.post(
6066
+ "/v1/user-tasks/reorder",
6067
+ input,
6068
+ this.getCliRequestHeaders()
6069
+ );
6070
+ if (!response.ok) {
6071
+ throw new Error(`User task reorder failed with HTTP ${response.status}`);
6072
+ }
6073
+ return response.data.tasks ?? [];
6074
+ }
6075
+ async postUserTaskAction(taskId, action, input = {}) {
6076
+ this.requireSession();
6077
+ const response = await this.http.post(
6078
+ `/v1/user-tasks/${encodeURIComponent(taskId)}/${encodeURIComponent(action)}`,
6079
+ input,
6080
+ this.getCliRequestHeaders()
6081
+ );
6082
+ if (!response.ok || !response.data.task) {
6083
+ throw new Error(`User task ${action} failed with HTTP ${response.status}`);
6084
+ }
6085
+ return response.data.task;
6086
+ }
5317
6087
  // -------------------------------------------------------------------------
5318
6088
  // User plans
5319
6089
  // -------------------------------------------------------------------------
@@ -6194,7 +6964,7 @@ Required: ${schema.required.join(", ")}`
6194
6964
  const session = this.requireSession();
6195
6965
  const masterKey = base64ToBytes(session.masterKeyExportedB64);
6196
6966
  const cache = await this.ensureSynced();
6197
- const { createHash: createHash7 } = await import("crypto");
6967
+ const { createHash: createHash8 } = await import("crypto");
6198
6968
  const embed = cache.embeds.find(
6199
6969
  (e) => String(e.embed_id ?? "").startsWith(embedIdOrShort) || String(e.id ?? "").startsWith(embedIdOrShort)
6200
6970
  );
@@ -6202,7 +6972,7 @@ Required: ${schema.required.join(", ")}`
6202
6972
  throw new Error(`Embed '${embedIdOrShort}' not found in local cache.`);
6203
6973
  }
6204
6974
  const embedId = String(embed.embed_id ?? embed.id ?? "");
6205
- const hashedEmbedId = createHash7("sha256").update(embedId).digest("hex");
6975
+ const hashedEmbedId = createHash8("sha256").update(embedId).digest("hex");
6206
6976
  const embedKeyBytes = await this.resolveEmbedKey(
6207
6977
  cache,
6208
6978
  masterKey,
@@ -6262,8 +7032,8 @@ Required: ${schema.required.join(", ")}`
6262
7032
  if (!embed) {
6263
7033
  throw new Error(`Embed '${embedId}' not found in local cache. Run 'openmates chats list' to sync first.`);
6264
7034
  }
6265
- const { createHash: createHash7 } = await import("crypto");
6266
- const hashedEmbedId = createHash7("sha256").update(embedId).digest("hex");
7035
+ const { createHash: createHash8 } = await import("crypto");
7036
+ const hashedEmbedId = createHash8("sha256").update(embedId).digest("hex");
6267
7037
  const embedKey = await this.resolveEmbedKey(
6268
7038
  cache,
6269
7039
  masterKey,
@@ -6357,8 +7127,8 @@ Required: ${schema.required.join(", ")}`
6357
7127
  if (!embed) {
6358
7128
  throw new Error(`Embed '${embedId}' not found in local cache. Run 'openmates chats list' to sync first.`);
6359
7129
  }
6360
- const { createHash: createHash7 } = await import("crypto");
6361
- const hashedEmbedId = createHash7("sha256").update(embedId).digest("hex");
7130
+ const { createHash: createHash8 } = await import("crypto");
7131
+ const hashedEmbedId = createHash8("sha256").update(embedId).digest("hex");
6362
7132
  const embedKey = await this.resolveEmbedKey(
6363
7133
  cache,
6364
7134
  masterKey,
@@ -6550,11 +7320,11 @@ Required: ${schema.required.join(", ")}`
6550
7320
  * so every WebSocket usage gets a fresh HMAC token automatically.
6551
7321
  */
6552
7322
  async openWsClient() {
6553
- await this.refreshWsToken();
7323
+ const ownerId = await this.refreshWsToken();
6554
7324
  const session = this.requireSession();
6555
7325
  const ws = this.makeWsClient(session);
6556
7326
  await ws.open();
6557
- return { ws, session };
7327
+ return { ws, session, ownerId };
6558
7328
  }
6559
7329
  /**
6560
7330
  * Refresh the ws_token by calling /auth/session.
@@ -6571,7 +7341,9 @@ Required: ${schema.required.join(", ")}`
6571
7341
  }
6572
7342
  session.cookies = this.http.getCookieMap();
6573
7343
  saveSession(session);
7344
+ 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
7345
  } catch {
7346
+ return null;
6575
7347
  }
6576
7348
  }
6577
7349
  /**
@@ -6616,6 +7388,7 @@ Required: ${schema.required.join(", ")}`
6616
7388
  const embedKeys = [];
6617
7389
  let newChatSuggestions = [];
6618
7390
  let totalChatCount = 0;
7391
+ let reconciliation = { authoritative: false };
6619
7392
  const pendingAIResponses = [];
6620
7393
  try {
6621
7394
  ws.send("phased_sync_request", {
@@ -6642,6 +7415,11 @@ Required: ${schema.required.join(", ")}`
6642
7415
  } else if (frame.type === "phase_2_last_20_chats_ready") {
6643
7416
  const p = frame.payload;
6644
7417
  totalChatCount = p.total_chat_count ?? 0;
7418
+ reconciliation = {
7419
+ authoritative: p.authoritative === true,
7420
+ authoritative_chat_ids: p.authoritative_chat_ids,
7421
+ deleted_chat_ids: p.deleted_chat_ids
7422
+ };
6645
7423
  for (const wrapper of p.chats ?? []) {
6646
7424
  const details = wrapper.chat_details;
6647
7425
  if (!details || typeof details.id !== "string") continue;
@@ -6721,12 +7499,21 @@ Required: ${schema.required.join(", ")}`
6721
7499
  }
6722
7500
  }
6723
7501
  }
7502
+ const reconciledChats = reconcileAuthoritativeChats(chats, reconciliation);
7503
+ chats.splice(0, chats.length, ...reconciledChats);
7504
+ if (reconciliation.deleted_chat_ids?.length) {
7505
+ const { createHash: createHash8 } = await import("crypto");
7506
+ const deletedHashes = new Set(
7507
+ reconciliation.deleted_chat_ids.map((id) => createHash8("sha256").update(id).digest("hex"))
7508
+ );
7509
+ const keptEmbeds = embeds.filter((embed) => !deletedHashes.has(String(embed.hashed_chat_id ?? "")));
7510
+ const keptKeys = embedKeys.filter((key) => !deletedHashes.has(String(key.hashed_chat_id ?? "")));
7511
+ embeds.splice(0, embeds.length, ...keptEmbeds);
7512
+ embedKeys.splice(0, embedKeys.length, ...keptKeys);
7513
+ }
6724
7514
  chats.sort(
6725
7515
  (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
7516
  );
6727
- if (totalChatCount > 0 && chats.length > totalChatCount) {
6728
- chats.length = totalChatCount;
6729
- }
6730
7517
  try {
6731
7518
  await this.persistPendingAIResponsesFromSync(ws, chats, pendingAIResponses);
6732
7519
  } finally {
@@ -6979,10 +7766,10 @@ function printLogo() {
6979
7766
 
6980
7767
  // src/cli.ts
6981
7768
  import { createInterface as createInterface4 } from "readline/promises";
6982
- import { realpathSync, writeFileSync as writeFileSync6 } from "fs";
7769
+ import { readFileSync as readFileSync9, realpathSync, writeFileSync as writeFileSync6 } from "fs";
6983
7770
  import { fileURLToPath as fileURLToPath2 } from "url";
6984
7771
  import { basename as basename3, dirname as dirname3 } from "path";
6985
- import { randomUUID as randomUUID5 } from "crypto";
7772
+ import { randomUUID as randomUUID6 } from "crypto";
6986
7773
  import WebSocket2 from "ws";
6987
7774
 
6988
7775
  // ../secret-scanner/src/registry.ts
@@ -9534,6 +10321,22 @@ import { createInterface as createPromptInterface } from "readline/promises";
9534
10321
  import { homedir as homedir5 } from "os";
9535
10322
  import { dirname, join as join3, resolve as resolve3 } from "path";
9536
10323
 
10324
+ // src/branding.ts
10325
+ var OPENMATES_WORDMARK = "OPENMATES";
10326
+ var OPENMATES_ASCII_LARGE = [
10327
+ " \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",
10328
+ "\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 ",
10329
+ "\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",
10330
+ "\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",
10331
+ " \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"
10332
+ ];
10333
+ function openMatesAsciiLogo(width = 100) {
10334
+ return width >= 86 ? [...OPENMATES_ASCII_LARGE, OPENMATES_WORDMARK] : [OPENMATES_WORDMARK];
10335
+ }
10336
+ function renderOpenMatesAsciiLogo(width = 100) {
10337
+ return openMatesAsciiLogo(width).join("\n");
10338
+ }
10339
+
9537
10340
  // src/support.ts
9538
10341
  var SUPPORT_URL = "https://openmates.org/#settings/support";
9539
10342
  var SUPPORT_TITLE = "Support OpenMates development";
@@ -10446,6 +11249,38 @@ function missingRequiredEnvKeys(installPath, role) {
10446
11249
  const env = readEnvMap(installPath);
10447
11250
  return requiredRuntimeEnvKeys(role).filter((key) => !env[key]);
10448
11251
  }
11252
+ var LONG_RUNNING_SERVER_COMMANDS = /* @__PURE__ */ new Set([
11253
+ "install",
11254
+ "start",
11255
+ "stop",
11256
+ "restart",
11257
+ "update",
11258
+ "preflight",
11259
+ "caddy",
11260
+ "backup",
11261
+ "restore",
11262
+ "reset",
11263
+ "make-admin",
11264
+ "ai",
11265
+ "uninstall"
11266
+ ]);
11267
+ function shouldPrintServerCommandStart(subcommand, rest, flags) {
11268
+ if (flags.json === true || !LONG_RUNNING_SERVER_COMMANDS.has(subcommand)) return false;
11269
+ if (subcommand === "backup" && rest[0] === "list") return false;
11270
+ if (subcommand === "update" && rest[0] === "status") return false;
11271
+ if (subcommand === "ai" && rest[0] === "models" && (rest[1] === "list" || rest[1] === "remove")) return false;
11272
+ if (subcommand === "caddy" && (rest[0] === "status" || rest[0] === "diff")) return false;
11273
+ return true;
11274
+ }
11275
+ function printServerCommandStart(subcommand, rest, flags) {
11276
+ const command = ["server", subcommand, ...rest].join(" ");
11277
+ console.log(renderOpenMatesAsciiLogo());
11278
+ console.log("");
11279
+ console.log(`Running OpenMates ${command}...`);
11280
+ if (typeof flags.path === "string") console.log(`Path: ${resolve3(flags.path)}`);
11281
+ if (typeof flags.role === "string") console.log(`Role: ${flags.role}`);
11282
+ console.log("");
11283
+ }
10449
11284
  function vaultCheckContainerForRole(role) {
10450
11285
  if (role === "core") return "api";
10451
11286
  if (role === "upload") return "app-uploads";
@@ -11966,6 +12801,9 @@ async function handleServer(subcommand, rest, flags) {
11966
12801
  printServerHelp();
11967
12802
  return;
11968
12803
  }
12804
+ if (shouldPrintServerCommandStart(subcommand, rest, flags)) {
12805
+ printServerCommandStart(subcommand, rest, flags);
12806
+ }
11969
12807
  switch (subcommand) {
11970
12808
  case "status":
11971
12809
  return serverStatus(flags);
@@ -28546,6 +29384,11 @@ Only output the final Markdown table. Do NOT include explanations, notes, or any
28546
29384
  }
28547
29385
  }
28548
29386
  },
29387
+ models3d: {
29388
+ generate: {
29389
+ text: "Generate 3D model"
29390
+ }
29391
+ },
28549
29392
  music: {
28550
29393
  generate: {
28551
29394
  text: "Generate",
@@ -30885,6 +31728,27 @@ Only output the final Markdown table. Do NOT include explanations, notes, or any
30885
31728
  save: {
30886
31729
  text: "Save"
30887
31730
  },
31731
+ detail_title_hint: {
31732
+ text: "Enter to save \xB7 Escape to cancel"
31733
+ },
31734
+ detail_description_hint: {
31735
+ text: "Ctrl/Command+Enter to save \xB7 Escape to cancel"
31736
+ },
31737
+ detail_save_error: {
31738
+ text: "Your changes were not saved."
31739
+ },
31740
+ detail_loading: {
31741
+ text: "Loading {item}\u2026"
31742
+ },
31743
+ detail_load_error: {
31744
+ text: "{item} could not be loaded."
31745
+ },
31746
+ detail_untitled: {
31747
+ text: "Untitled {item}"
31748
+ },
31749
+ detail_items_count: {
31750
+ text: "{count} items"
31751
+ },
30888
31752
  settings: {
30889
31753
  text: "Settings"
30890
31754
  },
@@ -36515,6 +37379,22 @@ As of mid-2026, the severe supply shocks from the 2024\u20132025 avian flu have
36515
37379
  },
36516
37380
  privacy: {
36517
37381
  providers: {
37382
+ model_generation: {
37383
+ heading: {
37384
+ text: "3D model generation (only when you use a 3D model skill)"
37385
+ },
37386
+ description: {
37387
+ 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."
37388
+ },
37389
+ hi3d: {
37390
+ heading: {
37391
+ text: "Hi3D (3D model generation)"
37392
+ },
37393
+ description: {
37394
+ 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."
37395
+ }
37396
+ }
37397
+ },
36518
37398
  fitness: {
36519
37399
  heading: {
36520
37400
  text: "Fitness search (only when you use the fitness skill)"
@@ -40605,6 +41485,42 @@ As of mid-2026, the severe supply shocks from the 2024\u20132025 avian flu have
40605
41485
  description: {
40606
41486
  text: "Manage your credits, purchases, and what you used credits for."
40607
41487
  },
41488
+ apple_support: {
41489
+ text: "Support OpenMates"
41490
+ },
41491
+ apple_usage_details: {
41492
+ text: "View usage details"
41493
+ },
41494
+ apple_purchased_gift_cards: {
41495
+ text: "Purchased"
41496
+ },
41497
+ apple_no_gift_cards: {
41498
+ text: "No gift cards yet"
41499
+ },
41500
+ apple_gift_card_purchase_unavailable: {
41501
+ text: "Gift card purchase is unavailable until your account email is fully synchronized."
41502
+ },
41503
+ apple_support_amount: {
41504
+ text: "Amount in EUR"
41505
+ },
41506
+ apple_create_bank_transfer: {
41507
+ text: "Create bank transfer"
41508
+ },
41509
+ apple_invalid_support_amount: {
41510
+ text: "Enter a valid amount and make sure your account has an email address."
41511
+ },
41512
+ apple_fulfillment_delayed: {
41513
+ text: "Your purchase is awaiting fulfillment. It remains retryable and will not be marked complete yet."
41514
+ },
41515
+ apple_product_not_found: {
41516
+ text: "This product is not available in the App Store."
41517
+ },
41518
+ apple_credits_count: {
41519
+ text: "{credits} credits"
41520
+ },
41521
+ apple_bank_transfer_reference: {
41522
+ text: "Bank transfer reference: {reference}"
41523
+ },
40608
41524
  text: "Billing & Usage",
40609
41525
  title: {
40610
41526
  text: "Billing & Credits"
@@ -41385,6 +42301,12 @@ As of mid-2026, the severe supply shocks from the 2024\u20132025 avian flu have
41385
42301
  learning_mode_disabled: {
41386
42302
  text: "Learning Mode disabled."
41387
42303
  },
42304
+ learning_mode_attempts_remaining: {
42305
+ text: "{count} attempts remaining."
42306
+ },
42307
+ learning_mode_locked: {
42308
+ text: "Deactivation is locked for 24 hours after too many incorrect attempts."
42309
+ },
41388
42310
  learning_mode_age_group_prompt: {
41389
42311
  text: "Learner age group: under_10, 10_12, 13_15, 16_18, or adult"
41390
42312
  },
@@ -42629,6 +43551,73 @@ As of mid-2026, the severe supply shocks from the 2024\u20132025 avian flu have
42629
43551
  },
42630
43552
  share_debug_logs_admin_notice: {
42631
43553
  text: "You are an admin. Therefore your debug logs are automatically shared already. Regular users can turn on/off debug logs sharing here."
43554
+ },
43555
+ native: {
43556
+ save_error: {
43557
+ text: "Could not save this privacy setting. Please try again."
43558
+ },
43559
+ master_key_unavailable: {
43560
+ text: "Your encryption key is unavailable. Sign in again to manage this setting."
43561
+ },
43562
+ debug_session: {
43563
+ description: {
43564
+ text: "Temporarily share sanitized native diagnostics with support. Message content, encryption keys, credentials, file names, and personal data are never shared."
43565
+ },
43566
+ duration: {
43567
+ text: "Duration"
43568
+ },
43569
+ start: {
43570
+ text: "Start sharing debug logs"
43571
+ },
43572
+ activating: {
43573
+ text: "Activating..."
43574
+ },
43575
+ active: {
43576
+ text: "Active debug session"
43577
+ },
43578
+ id: {
43579
+ text: "Your Debug ID"
43580
+ },
43581
+ share_hint: {
43582
+ text: "Share this ID with the support team so they can find your sanitized logs."
43583
+ },
43584
+ stop: {
43585
+ text: "Stop sharing"
43586
+ },
43587
+ stopping: {
43588
+ text: "Stopping..."
43589
+ },
43590
+ no_expiry: {
43591
+ text: "Active until manually stopped"
43592
+ },
43593
+ error: {
43594
+ text: "Could not update the debug sharing session. Please try again."
43595
+ },
43596
+ duration_5m: {
43597
+ text: "5 minutes"
43598
+ },
43599
+ duration_1h: {
43600
+ text: "1 hour"
43601
+ },
43602
+ duration_3d: {
43603
+ text: "3 days"
43604
+ },
43605
+ duration_7d: {
43606
+ text: "7 days"
43607
+ },
43608
+ duration_none: {
43609
+ text: "No time limit"
43610
+ },
43611
+ minutes_remaining: {
43612
+ text: "{count}m remaining"
43613
+ },
43614
+ hours_remaining: {
43615
+ text: "{count}h remaining"
43616
+ },
43617
+ days_remaining: {
43618
+ text: "{count}d remaining"
43619
+ }
43620
+ }
42632
43621
  }
42633
43622
  },
42634
43623
  terms_and_conditions: {
@@ -44439,6 +45428,21 @@ As of mid-2026, the severe supply shocks from the 2024\u20132025 avian flu have
44439
45428
  pair_server_use_custom: {
44440
45429
  text: "Use custom"
44441
45430
  },
45431
+ pair_self_hosted_edition: {
45432
+ text: "Connect self-hosted edition"
45433
+ },
45434
+ pair_self_hosted_placeholder: {
45435
+ text: "Server URL"
45436
+ },
45437
+ pair_self_hosted_connect: {
45438
+ text: "Connect"
45439
+ },
45440
+ pair_self_hosted_invalid_url: {
45441
+ text: "Enter a valid HTTPS server URL."
45442
+ },
45443
+ pair_use_production: {
45444
+ text: "Use OpenMates.org"
45445
+ },
44442
45446
  pair_connect_apple_watch_title: {
44443
45447
  text: "Connect Apple Watch"
44444
45448
  },
@@ -46868,22 +47872,6 @@ var TuiTerminal = class {
46868
47872
  }
46869
47873
  };
46870
47874
 
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
47875
  // src/tuiRenderer.ts
46888
47876
  var MIN_WIDTH = 48;
46889
47877
  var CONTENT_PREVIEW_LINES = 12;
@@ -46910,6 +47898,17 @@ function createInitialTuiState() {
46910
47898
  selectedInterests: [],
46911
47899
  examples: [],
46912
47900
  activeExample: null,
47901
+ workflows: [],
47902
+ activeWorkflow: null,
47903
+ workflowRuns: [],
47904
+ tasks: [],
47905
+ activeTask: null,
47906
+ workflowTab: "graph",
47907
+ selectedWorkflowNodeIndex: 0,
47908
+ selectedWorkflowRunIndex: 0,
47909
+ expandedWorkflowNodeId: null,
47910
+ expandedWorkflowRunNodeId: null,
47911
+ workflowEdit: null,
46913
47912
  messages: [],
46914
47913
  status: null,
46915
47914
  isBusy: false
@@ -46943,6 +47942,7 @@ Files:
46943
47942
 
46944
47943
  More:
46945
47944
  openmates apps list
47945
+ openmates workflows list
46946
47946
  openmates mentions list
46947
47947
  openmates embeds show <embed-id>
46948
47948
  openmates help
@@ -46965,7 +47965,7 @@ function renderTuiFrame(state, width, height) {
46965
47965
  const contentHeight = safeHeight - 4;
46966
47966
  const body = renderBody(state, innerWidth);
46967
47967
  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)}`;
47968
+ 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
47969
  const lines = [
46970
47970
  header2,
46971
47971
  ...padLines(visible, contentHeight, innerWidth).map((line) => boxed(line, innerWidth)),
@@ -46991,6 +47991,14 @@ function renderBody(state, width) {
46991
47991
  return renderStatus(state, width);
46992
47992
  case "embed":
46993
47993
  return renderStatus(state, width);
47994
+ case "workflows":
47995
+ return renderWorkflows(state, width);
47996
+ case "workflow":
47997
+ return renderWorkflowDetail(state, width);
47998
+ case "tasks":
47999
+ return renderTasks(state, width);
48000
+ case "task":
48001
+ return renderTaskDetail(state, width);
46994
48002
  case "start":
46995
48003
  default:
46996
48004
  return renderStart(width);
@@ -47012,7 +48020,7 @@ function renderStart(width) {
47012
48020
  "Type @./notes.md, @~/Downloads/report.pdf, or @src/app.ts in your message.",
47013
48021
  "Images, PDFs, audio, and code files are attached as encrypted embeds when you are signed in.",
47014
48022
  "",
47015
- "Shortcuts: /help /login /signup /examples /exit"
48023
+ "Shortcuts: /help /login /signup /examples /tasks /exit"
47016
48024
  ].flatMap((line) => wrap(line, width));
47017
48025
  }
47018
48026
  function renderHelp(width) {
@@ -47032,6 +48040,9 @@ function renderHelp(width) {
47032
48040
  "",
47033
48041
  "Commands",
47034
48042
  " /examples Choose example chats",
48043
+ " /workflows List and run saved workflows",
48044
+ " /workflow <id> Open a workflow by ID",
48045
+ " /tasks Open your task workspace",
47035
48046
  " /login Pair-auth login",
47036
48047
  " /signup Leave TUI and run guided signup",
47037
48048
  " /embed <id> Open an embed detail view",
@@ -47040,6 +48051,150 @@ function renderHelp(width) {
47040
48051
  "Outside TUI: openmates --help, openmates chats --help, openmates apps --help"
47041
48052
  ].flatMap((line) => wrap(line, width));
47042
48053
  }
48054
+ function renderTasks(state, width) {
48055
+ const lines = ["Tasks", ""];
48056
+ if (state.tasks.length === 0) {
48057
+ lines.push("No tasks found.", "Create one outside TUI with: openmates tasks create --title <title>");
48058
+ return lines.flatMap((line) => wrap(line, width));
48059
+ }
48060
+ const visibleCount = Math.max(1, CONTENT_PREVIEW_LINES);
48061
+ const start = Math.max(0, Math.min(state.selectedIndex, state.tasks.length - visibleCount));
48062
+ for (let i = 0; i < state.tasks.slice(start, start + visibleCount).length; i += 1) {
48063
+ const absoluteIndex = start + i;
48064
+ const task = state.tasks[absoluteIndex];
48065
+ const cursor = absoluteIndex === state.selectedIndex ? ">" : " ";
48066
+ const assignee = task.assigneeType === "ai" ? "OpenMates" : task.assigneeHash ?? "user";
48067
+ lines.push(`${cursor} ${task.shortId} ${task.status} ${assignee} ${task.title}`);
48068
+ if (task.queueState !== "none") lines.push(` queue: ${task.queueState}`);
48069
+ if (task.description) lines.push(` ${task.description}`);
48070
+ lines.push("");
48071
+ }
48072
+ return lines.flatMap((line) => wrap(line, width));
48073
+ }
48074
+ function renderTaskDetail(state, width) {
48075
+ const task = state.activeTask;
48076
+ if (!task) return renderTasks(state, width);
48077
+ const assignee = task.assigneeType === "ai" ? "OpenMates" : task.assigneeHash ?? "user";
48078
+ const lines = [
48079
+ `Task: ${task.shortId}`,
48080
+ `Title: ${task.title}`,
48081
+ `Status: ${task.status}`,
48082
+ `Assignee: ${assignee}`,
48083
+ `Queue: ${task.queueState}`,
48084
+ `ID: ${task.taskId}`,
48085
+ task.description ? `Description: ${task.description}` : null,
48086
+ task.primaryChatId ? `Chat: ${task.primaryChatId}` : null,
48087
+ task.linkedProjectIds.length > 0 ? `Projects: ${task.linkedProjectIds.join(", ")}` : null,
48088
+ task.blockedReasonCode ? `Blocked reason: ${task.blockedReasonCode}` : null,
48089
+ task.aiExecutionState ? `AI state: ${task.aiExecutionState}` : null,
48090
+ "",
48091
+ "Actions: c create, e edit, x delete, r reorder, s start, d done, b block, u unblock, k skip, Esc back"
48092
+ ].filter((line) => line !== null);
48093
+ return lines.flatMap((line) => wrap(line, width));
48094
+ }
48095
+ function renderWorkflows(state, width) {
48096
+ const lines = ["Workflows", ""];
48097
+ if (state.workflows.length === 0) {
48098
+ lines.push("No workflows found.", "Create one outside TUI with: openmates workflows create --file workflow.yml");
48099
+ return lines.flatMap((line) => wrap(line, width));
48100
+ }
48101
+ const visibleCount = Math.max(1, CONTENT_PREVIEW_LINES);
48102
+ const start = Math.max(0, Math.min(state.selectedIndex, state.workflows.length - visibleCount));
48103
+ for (let i = 0; i < state.workflows.slice(start, start + visibleCount).length; i += 1) {
48104
+ const absoluteIndex = start + i;
48105
+ const workflow = state.workflows[absoluteIndex];
48106
+ const cursor = absoluteIndex === state.selectedIndex ? ">" : " ";
48107
+ const status = workflow.enabled ? "enabled" : "disabled";
48108
+ const lastRun = workflow.last_run_status ? ` last: ${workflow.last_run_status}` : "";
48109
+ lines.push(`${cursor} ${workflow.title} (${status})${lastRun}`);
48110
+ lines.push(` ${workflow.id}`);
48111
+ if (workflow.trigger_summary) lines.push(` ${workflow.trigger_summary}`);
48112
+ lines.push("");
48113
+ }
48114
+ return lines.flatMap((line) => wrap(line, width));
48115
+ }
48116
+ function renderWorkflowDetail(state, width) {
48117
+ const workflow = state.activeWorkflow;
48118
+ if (!workflow) return renderWorkflows(state, width);
48119
+ const graphNodes = workflow.graph?.nodes ?? [];
48120
+ const selectedRun = state.workflowRuns[state.selectedWorkflowRunIndex] ?? null;
48121
+ const lines = [
48122
+ `Workflow: ${workflow.title}`,
48123
+ `ID: ${workflow.id}`,
48124
+ `Status: ${workflow.enabled ? "enabled" : "disabled"}`,
48125
+ workflow.trigger_summary ? `Trigger: ${workflow.trigger_summary}` : null,
48126
+ workflow.next_run_at ? `Next run: ${formatTimestamp(workflow.next_run_at)}` : null,
48127
+ workflow.last_run_status ? `Last run: ${workflow.last_run_status}` : null,
48128
+ "",
48129
+ state.workflowTab === "graph" ? "[Graph] Runs" : "Graph [Runs]",
48130
+ ""
48131
+ ].filter((line) => line !== null);
48132
+ if (state.workflowTab === "runs") {
48133
+ lines.push(...renderRunSelector(state, width), "");
48134
+ if (selectedRun) {
48135
+ lines.push(`Run graph: ${selectedRun.id} (${selectedRun.status})`);
48136
+ lines.push(...renderWorkflowGraph({ nodes: graphNodes, state, width, run: selectedRun }));
48137
+ } else {
48138
+ lines.push("No runs yet.");
48139
+ }
48140
+ } else {
48141
+ lines.push("Graph");
48142
+ lines.push(...renderWorkflowGraph({ nodes: graphNodes, state, width, run: null }));
48143
+ if (state.workflowEdit) lines.push("", `Editing ${state.workflowEdit.field}: ${state.workflowEdit.value}`);
48144
+ }
48145
+ if (state.status) lines.push("", state.status);
48146
+ return lines.flatMap((line) => wrap(line, width));
48147
+ }
48148
+ function renderRunSelector(state, width) {
48149
+ if (state.workflowRuns.length === 0) return ["Runs", "No runs yet."];
48150
+ const lines = ["Runs"];
48151
+ const visibleRuns = state.workflowRuns.slice(0, 5);
48152
+ for (let index = 0; index < visibleRuns.length; index += 1) {
48153
+ const run = visibleRuns[index];
48154
+ const cursor = index === state.selectedWorkflowRunIndex ? ">" : " ";
48155
+ lines.push(`${cursor} ${run.id} ${run.status} ${formatTimestamp(run.started_at)}`);
48156
+ }
48157
+ return lines.map((line) => truncateVisible(line, width));
48158
+ }
48159
+ function renderWorkflowGraph(params) {
48160
+ const { nodes, state, run } = params;
48161
+ if (nodes.length === 0) return ["No graph nodes available."];
48162
+ const lines = [];
48163
+ const selectedIndex = state.workflowTab === "runs" ? selectedRunGraphNodeIndex(nodes, state) : state.selectedWorkflowNodeIndex;
48164
+ const expandedId = state.workflowTab === "runs" ? state.expandedWorkflowRunNodeId : state.expandedWorkflowNodeId;
48165
+ const nodeRunsById = new Map((run?.node_runs ?? []).map((nodeRun) => [nodeRun.node_id, nodeRun]));
48166
+ for (let index = 0; index < nodes.length; index += 1) {
48167
+ const node = nodes[index];
48168
+ const nodeRun = nodeRunsById.get(node.id) ?? null;
48169
+ const cursor = index === selectedIndex ? ">" : " ";
48170
+ const status = nodeRun ? ` [${nodeRun.status}]` : "";
48171
+ lines.push(`${cursor} [${nodeTypeLabel(node.type)}] ${node.title ?? cardSummary(node)}${status}`);
48172
+ if (nodeRun?.output_summary) lines.push(` output: ${summarizeObject(nodeRun.output_summary)}`);
48173
+ if (nodeRun?.error_summary) lines.push(` error: ${nodeRun.error_summary}`);
48174
+ if (expandedId === node.id) {
48175
+ lines.push(...renderExpandedNode(node, nodeRun));
48176
+ }
48177
+ if (index < nodes.length - 1) lines.push(" |");
48178
+ }
48179
+ return lines;
48180
+ }
48181
+ function selectedRunGraphNodeIndex(nodes, state) {
48182
+ const run = state.workflowRuns[state.selectedWorkflowRunIndex];
48183
+ const nodeRun = run?.node_runs?.find((candidate) => candidate.node_id === state.expandedWorkflowRunNodeId);
48184
+ if (!nodeRun) return Math.min(state.selectedWorkflowNodeIndex, Math.max(0, nodes.length - 1));
48185
+ return Math.max(0, nodes.findIndex((node) => node.id === nodeRun.node_id));
48186
+ }
48187
+ function renderExpandedNode(node, nodeRun) {
48188
+ const lines = [
48189
+ ` id: ${node.id}`,
48190
+ ` type: ${node.type}`
48191
+ ];
48192
+ if (node.config && Object.keys(node.config).length > 0) lines.push(` config: ${summarizeObject(node.config)}`);
48193
+ if (node.input_mapping && Object.keys(node.input_mapping).length > 0) lines.push(` input: ${summarizeObject(node.input_mapping)}`);
48194
+ if (nodeRun?.input_summary) lines.push(` run input: ${summarizeObject(nodeRun.input_summary)}`);
48195
+ if (nodeRun?.output_summary) lines.push(` run output: ${summarizeObject(nodeRun.output_summary)}`);
48196
+ return lines;
48197
+ }
47043
48198
  function renderInterests(state, width) {
47044
48199
  const lines = [
47045
48200
  "Private local personalization",
@@ -47134,13 +48289,55 @@ function renderMessageContent(content, width) {
47134
48289
  }
47135
48290
  function renderHintLine(state, width) {
47136
48291
  if (state.screen === "interests") return truncateVisible("\u2191/\u2193 move Space select Enter continue Esc back", width);
48292
+ if (state.screen === "tasks") return truncateVisible("\u2191/\u2193 choose Enter open c create Esc back", width);
48293
+ 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);
48294
+ if (state.screen === "workflows") return truncateVisible("\u2191/\u2193 choose Enter open Esc back", width);
48295
+ if (state.screen === "workflow" && state.workflowEdit) return truncateVisible("Enter save title Esc cancel edit", width);
48296
+ 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
48297
  return truncateVisible("\u2191/\u2193 choose Enter open /search filter Esc back", width);
47138
48298
  }
47139
48299
  function inputPlaceholder(state) {
47140
48300
  if (state.screen === "example") return "Continue from this example, or ask your own question...";
47141
48301
  if (state.screen === "chat") return "Ask a follow-up, use @file, or type /help";
48302
+ if (state.screen === "workflow" || state.screen === "workflows" || state.screen === "tasks" || state.screen === "task") return "Use shortcuts below, or type /help";
47142
48303
  return "Ask anything...";
47143
48304
  }
48305
+ function nodeTypeLabel(type) {
48306
+ switch (type) {
48307
+ case "manual_trigger":
48308
+ return "manual trigger";
48309
+ case "schedule_trigger":
48310
+ return "schedule";
48311
+ case "app_skill_action":
48312
+ return "app skill";
48313
+ case "send_notification":
48314
+ return "notification";
48315
+ case "ask_user":
48316
+ return "ask user";
48317
+ default:
48318
+ return type.replaceAll("_", " ");
48319
+ }
48320
+ }
48321
+ function cardSummary(node) {
48322
+ const config = node.config ?? {};
48323
+ if (node.type === "app_skill_action") {
48324
+ const app = typeof config.app === "string" ? config.app : "app";
48325
+ const skill = typeof config.skill === "string" ? config.skill : "skill";
48326
+ return `${app}.${skill}`;
48327
+ }
48328
+ if (node.type === "decision") return "If condition";
48329
+ if (node.type === "send_notification") return "Send notification";
48330
+ if (node.type === "ask_user") return "Ask for user input";
48331
+ return node.id;
48332
+ }
48333
+ function formatTimestamp(value) {
48334
+ if (!value) return "-";
48335
+ return new Date(value * 1e3).toISOString().replace("T", " ").slice(0, 16);
48336
+ }
48337
+ function summarizeObject(value) {
48338
+ const entries = Object.entries(value).slice(0, 3).map(([key, item]) => `${key}=${String(item)}`);
48339
+ return entries.join(", ") || "object";
48340
+ }
47144
48341
  function interestScore(haystack, interest) {
47145
48342
  let score = haystack.includes(interest) ? 10 : 0;
47146
48343
  for (const token of interest.split(/\s+|&/).filter((part) => part.length > 2)) {
@@ -47191,6 +48388,221 @@ function boxed(line, width) {
47191
48388
  return `\u2502 ${line.padEnd(width)} \u2502`;
47192
48389
  }
47193
48390
 
48391
+ // src/tasksCli.ts
48392
+ import { createHash as createHash7, randomBytes as randomBytes3, randomUUID as randomUUID5 } from "crypto";
48393
+ var TASK_STATUSES2 = ["backlog", "todo", "in_progress", "blocked", "done"];
48394
+ var DEFAULT_STANDALONE_PREFIX = "TASK";
48395
+ function normalizeTaskStatus(value) {
48396
+ if (value === void 0) return void 0;
48397
+ if (TASK_STATUSES2.includes(value)) return value;
48398
+ throw new Error(`Unknown task status '${value}'. Expected one of: ${TASK_STATUSES2.join(", ")}`);
48399
+ }
48400
+ function parseAssignee(value) {
48401
+ if (!value || value === "user") return { assigneeType: "user", assigneeHash: null };
48402
+ if (["ai", "openmates", "OpenMates"].includes(value)) return { assigneeType: "ai", assigneeHash: null };
48403
+ return { assigneeType: "user", assigneeHash: value };
48404
+ }
48405
+ function splitCsvFlag(value) {
48406
+ if (typeof value !== "string") return [];
48407
+ return value.split(/[,\n]/).map((item) => item.trim()).filter(Boolean);
48408
+ }
48409
+ function parseDueAt(value) {
48410
+ if (value === void 0) return void 0;
48411
+ if (value === false || value === true) throw new Error("--due requires a timestamp or date value.");
48412
+ const numeric = Number(value);
48413
+ if (Number.isFinite(numeric) && numeric > 0) return Math.floor(numeric);
48414
+ const parsed = Date.parse(value);
48415
+ if (Number.isNaN(parsed)) throw new Error(`Invalid --due value '${value}'.`);
48416
+ return Math.floor(parsed / 1e3);
48417
+ }
48418
+ async function buildCreateUserTaskInput(masterKey, input) {
48419
+ const taskKey = randomBytes3(32);
48420
+ const encryptedTaskKey = await encryptBytesWithAesGcm(taskKey, masterKey);
48421
+ const timestamp = nowSeconds();
48422
+ const assignee = parseAssignee(input.assign);
48423
+ const linkedProjectIds = input.projectIds ?? [];
48424
+ const status = input.status ?? (assignee.assigneeType === "ai" && !input.dueAt ? "in_progress" : "todo");
48425
+ return {
48426
+ task_id: randomUUIDCompat(),
48427
+ short_id: void 0,
48428
+ encrypted_task_key: encryptedTaskKey,
48429
+ encrypted_title: await encryptWithAesGcmCombined(input.title, taskKey),
48430
+ encrypted_description: await encryptWithAesGcmCombined(input.description ?? "", taskKey),
48431
+ encrypted_tags: await encryptWithAesGcmCombined("[]", taskKey),
48432
+ encrypted_linked_project_ids: await encryptWithAesGcmCombined(JSON.stringify(linkedProjectIds), taskKey),
48433
+ status,
48434
+ assignee_type: assignee.assigneeType,
48435
+ assignee_hash: assignee.assigneeHash,
48436
+ primary_chat_id: input.chatId ?? null,
48437
+ linked_project_ids: linkedProjectIds,
48438
+ plan_id: input.planId ?? null,
48439
+ due_at: input.dueAt ?? null,
48440
+ priority: 0,
48441
+ position: timestamp,
48442
+ created_at: timestamp,
48443
+ updated_at: timestamp
48444
+ };
48445
+ }
48446
+ async function buildUpdateUserTaskInput(task, masterKey, input) {
48447
+ const taskKey = await taskKeyFromRecord(task.encrypted, masterKey);
48448
+ const patch = { version: task.version, updated_at: nowSeconds() };
48449
+ if (input.title !== void 0) patch.encrypted_title = await encryptWithAesGcmCombined(input.title, taskKey);
48450
+ if (input.description !== void 0) patch.encrypted_description = await encryptWithAesGcmCombined(input.description, taskKey);
48451
+ if (input.status !== void 0) patch.status = input.status;
48452
+ if (input.assign !== void 0) {
48453
+ const assignee = parseAssignee(input.assign);
48454
+ patch.assignee_type = assignee.assigneeType;
48455
+ patch.assignee_hash = assignee.assigneeHash;
48456
+ }
48457
+ if (input.chatId !== void 0) patch.primary_chat_id = input.chatId;
48458
+ if (input.projectIds !== void 0) {
48459
+ patch.linked_project_ids = input.projectIds;
48460
+ patch.encrypted_linked_project_ids = await encryptWithAesGcmCombined(JSON.stringify(input.projectIds), taskKey);
48461
+ }
48462
+ if (input.planId !== void 0) patch.plan_id = input.planId;
48463
+ return patch;
48464
+ }
48465
+ async function decryptUserTask(record, masterKey) {
48466
+ const taskKey = await taskKeyFromRecord(record, masterKey);
48467
+ const tags = parseStringArray(await decryptOptional(record.encrypted_tags, taskKey));
48468
+ const linkedProjectIds = parseStringArray(await decryptOptional(record.encrypted_linked_project_ids, taskKey));
48469
+ return {
48470
+ taskId: record.task_id,
48471
+ shortId: record.short_id || deriveShortId(record),
48472
+ title: await decryptOptional(record.encrypted_title, taskKey) || "(untitled task)",
48473
+ description: await decryptOptional(record.encrypted_description, taskKey),
48474
+ tags,
48475
+ latestInstruction: await decryptOptional(record.encrypted_latest_instruction, taskKey),
48476
+ status: record.status,
48477
+ assigneeType: record.assignee_type,
48478
+ assigneeHash: record.assignee_hash ?? null,
48479
+ primaryChatId: record.primary_chat_id ?? null,
48480
+ linkedProjectIds: linkedProjectIds.length > 0 ? linkedProjectIds : record.linked_project_ids ?? [],
48481
+ planId: record.plan_id ?? null,
48482
+ dueAt: record.due_at ?? null,
48483
+ priority: record.priority ?? 0,
48484
+ position: record.position ?? 0,
48485
+ queueState: record.queue_state ?? "none",
48486
+ blockedReasonCode: record.blocked_reason_code ?? null,
48487
+ aiExecutionState: record.ai_execution_state ?? null,
48488
+ version: record.version ?? 1,
48489
+ encrypted: record
48490
+ };
48491
+ }
48492
+ async function decryptUserTasks(records, masterKey) {
48493
+ const output = [];
48494
+ for (const record of records) output.push(await decryptUserTask(record, masterKey));
48495
+ return output;
48496
+ }
48497
+ function findTask(tasks, id) {
48498
+ const task = tasks.find((candidate) => candidate.taskId === id || candidate.shortId === id);
48499
+ if (!task) throw new Error(`Task '${id}' was not found in the current task list.`);
48500
+ return task;
48501
+ }
48502
+ function renderTaskList(tasks) {
48503
+ if (tasks.length === 0) return "No tasks found.";
48504
+ const lines = ["Tasks", "ID Status Assignee Queue Title"];
48505
+ for (const task of tasks) {
48506
+ lines.push(`${pad(task.shortId, 9)} ${pad(task.status, 12)} ${pad(assigneeLabel(task), 11)} ${pad(task.queueState, 11)} ${task.title}`);
48507
+ }
48508
+ return lines.join("\n");
48509
+ }
48510
+ function renderTaskDetail2(task) {
48511
+ const lines = [
48512
+ `Task ${task.shortId}`,
48513
+ `Title: ${task.title}`,
48514
+ `Status: ${task.status}`,
48515
+ `Assignee: ${assigneeLabel(task)}`,
48516
+ `Queue: ${task.queueState}`,
48517
+ `Task ID: ${task.taskId}`
48518
+ ];
48519
+ if (task.description) lines.push(`Description: ${task.description}`);
48520
+ if (task.primaryChatId) lines.push(`Chat: ${task.primaryChatId}`);
48521
+ if (task.linkedProjectIds.length > 0) lines.push(`Projects: ${task.linkedProjectIds.join(", ")}`);
48522
+ if (task.planId) lines.push(`Plan: ${task.planId}`);
48523
+ if (task.blockedReasonCode) lines.push(`Blocked reason: ${task.blockedReasonCode}`);
48524
+ if (task.aiExecutionState) lines.push(`AI state: ${task.aiExecutionState}`);
48525
+ return lines.join("\n");
48526
+ }
48527
+ function renderTaskBoard(tasks, width = process.stdout.columns || 100) {
48528
+ const columns = TASK_STATUSES2.map((status) => ({ status, tasks: tasks.filter((task) => task.status === status).sort(compareTasks) }));
48529
+ if (width < 96) {
48530
+ const lines2 = ["OpenMates Tasks Board"];
48531
+ for (const column of columns) {
48532
+ lines2.push("", `${columnTitle(column.status)} (${column.tasks.length})`, "-".repeat(24));
48533
+ lines2.push(...boardColumnLines(column.tasks, 8));
48534
+ }
48535
+ return lines2.join("\n");
48536
+ }
48537
+ const perColumn = 6;
48538
+ const columnWidth = 22;
48539
+ const lines = ["OpenMates Tasks Board", ""];
48540
+ lines.push(columns.map((column) => pad(`${columnTitle(column.status)} (${column.tasks.length})`, columnWidth)).join(" "));
48541
+ lines.push(columns.map(() => "-".repeat(columnWidth)).join(" "));
48542
+ const renderedColumns = columns.map((column) => boardColumnLines(column.tasks, perColumn).map((line) => truncate(line, columnWidth)));
48543
+ const maxRows = Math.max(...renderedColumns.map((column) => column.length));
48544
+ for (let row = 0; row < maxRows; row += 1) {
48545
+ lines.push(renderedColumns.map((column) => pad(column[row] ?? "", columnWidth)).join(" "));
48546
+ }
48547
+ return lines.join("\n");
48548
+ }
48549
+ function boardColumnLines(tasks, limit) {
48550
+ if (tasks.length === 0) return ["No tasks here."];
48551
+ const visible = tasks.slice(0, limit).flatMap((task) => [
48552
+ `[${task.shortId}] ${task.title}`,
48553
+ ` ${assigneeLabel(task)} ${task.queueState === "none" ? "" : task.queueState}`.trimEnd()
48554
+ ]);
48555
+ if (tasks.length > limit) visible.push(`... ${tasks.length - limit} more`);
48556
+ return visible;
48557
+ }
48558
+ function compareTasks(a, b) {
48559
+ return a.position - b.position || a.title.localeCompare(b.title);
48560
+ }
48561
+ function columnTitle(status) {
48562
+ if (status === "in_progress") return "In progress";
48563
+ return status.slice(0, 1).toUpperCase() + status.slice(1).replace("_", " ");
48564
+ }
48565
+ function assigneeLabel(task) {
48566
+ return task.assigneeType === "ai" ? "OpenMates" : task.assigneeHash ?? "user";
48567
+ }
48568
+ async function taskKeyFromRecord(record, masterKey) {
48569
+ if (!record.encrypted_task_key) throw new Error(`Task ${record.task_id} is missing encrypted task key.`);
48570
+ const taskKey = await decryptBytesWithAesGcm(record.encrypted_task_key, masterKey);
48571
+ if (!taskKey) throw new Error(`Failed to decrypt task key for ${record.task_id}.`);
48572
+ return taskKey;
48573
+ }
48574
+ async function decryptOptional(value, key) {
48575
+ if (!value) return "";
48576
+ return await decryptWithAesGcmCombined(value, key) ?? "";
48577
+ }
48578
+ function parseStringArray(value) {
48579
+ if (!value) return [];
48580
+ try {
48581
+ const parsed = JSON.parse(value);
48582
+ return Array.isArray(parsed) ? parsed.filter((item) => typeof item === "string") : [];
48583
+ } catch {
48584
+ return [];
48585
+ }
48586
+ }
48587
+ function deriveShortId(record) {
48588
+ const prefix = record.short_id_prefix || DEFAULT_STANDALONE_PREFIX;
48589
+ const source = record.task_id || `${record.created_at ?? ""}-${record.position ?? ""}`;
48590
+ const digest = createHash7("sha256").update(source).digest("hex").slice(0, 4).toUpperCase();
48591
+ return `${prefix}-${parseInt(digest, 16) % 1e4}`;
48592
+ }
48593
+ function nowSeconds() {
48594
+ return Math.floor(Date.now() / 1e3);
48595
+ }
48596
+ function randomUUIDCompat() {
48597
+ return randomUUID5();
48598
+ }
48599
+ function pad(value, length) {
48600
+ return truncate(value, length).padEnd(length);
48601
+ }
48602
+ function truncate(value, length) {
48603
+ return value.length <= length ? value : `${value.slice(0, Math.max(0, length - 3))}...`;
48604
+ }
48605
+
47194
48606
  // src/tui.ts
47195
48607
  function defaultModeForStreams(input, output) {
47196
48608
  return input.isTTY === true && output.isTTY === true ? "tui" : "quickstart";
@@ -47233,11 +48645,78 @@ async function handleKey(params) {
47233
48645
  return;
47234
48646
  }
47235
48647
  if (key.name === "escape") {
47236
- state.screen = "start";
48648
+ if (state.workflowEdit) {
48649
+ state.workflowEdit = null;
48650
+ render();
48651
+ return;
48652
+ }
48653
+ state.screen = state.screen === "workflow" ? "workflows" : state.screen === "task" ? "tasks" : "start";
47237
48654
  state.input = "";
47238
48655
  render();
47239
48656
  return;
47240
48657
  }
48658
+ if (state.screen === "workflow" && state.workflowEdit) {
48659
+ if (key.name === "return") {
48660
+ await saveWorkflowNodeTitle({ state, client, render });
48661
+ return;
48662
+ }
48663
+ if (key.name === "backspace") {
48664
+ state.workflowEdit.value = state.workflowEdit.value.slice(0, -1);
48665
+ render();
48666
+ return;
48667
+ }
48668
+ if (!key.ctrl && !key.meta && chunk && chunk >= " ") {
48669
+ state.workflowEdit.value += chunk;
48670
+ render();
48671
+ }
48672
+ return;
48673
+ }
48674
+ if (state.screen === "workflow" && !state.input && !key.ctrl && !key.meta) {
48675
+ if (chunk === "g") {
48676
+ state.workflowTab = "graph";
48677
+ render();
48678
+ return;
48679
+ }
48680
+ if (chunk === "r") {
48681
+ state.workflowTab = "runs";
48682
+ render();
48683
+ return;
48684
+ }
48685
+ if (chunk === "x") {
48686
+ await runActiveWorkflow({ state, client, render });
48687
+ return;
48688
+ }
48689
+ if (chunk === "u") {
48690
+ await refreshActiveWorkflowRuns({ state, client, render });
48691
+ return;
48692
+ }
48693
+ if (chunk === "c") {
48694
+ await cancelLatestWorkflowRun({ state, client, render });
48695
+ return;
48696
+ }
48697
+ if (chunk === "e") {
48698
+ startWorkflowNodeEdit(state, "title");
48699
+ render();
48700
+ return;
48701
+ }
48702
+ if (chunk === "E") {
48703
+ startWorkflowNodeEdit(state, "config");
48704
+ render();
48705
+ return;
48706
+ }
48707
+ }
48708
+ if ((state.screen === "tasks" || state.screen === "task") && !state.input && !key.ctrl && !key.meta) {
48709
+ if (chunk === "c") {
48710
+ await createTaskFromTui({ state, client, terminal, render });
48711
+ return;
48712
+ }
48713
+ }
48714
+ if (state.screen === "task" && !state.input && !key.ctrl && !key.meta) {
48715
+ if (["s", "d", "b", "u", "k", "e", "x", "r"].includes(chunk)) {
48716
+ await handleActiveTaskAction({ state, client, terminal, actionKey: chunk, render });
48717
+ return;
48718
+ }
48719
+ }
47241
48720
  if (key.name === "up") {
47242
48721
  moveSelectionOrScroll(state, -1);
47243
48722
  render();
@@ -47282,6 +48761,9 @@ async function handleKey(params) {
47282
48761
  await handleEnter({ state, client, terminal, render, finish });
47283
48762
  return;
47284
48763
  }
48764
+ if (state.screen === "workflow" || state.screen === "workflows" || state.screen === "tasks" || state.screen === "task") {
48765
+ return;
48766
+ }
47285
48767
  if (!key.ctrl && !key.meta && chunk && chunk >= " ") {
47286
48768
  state.input += chunk;
47287
48769
  render();
@@ -47300,6 +48782,23 @@ async function handleEnter(params) {
47300
48782
  render();
47301
48783
  return;
47302
48784
  }
48785
+ if (state.screen === "workflows") {
48786
+ const selected = state.workflows[state.selectedIndex];
48787
+ if (selected) await openWorkflowDetail({ state, client, workflow: selected, render });
48788
+ render();
48789
+ return;
48790
+ }
48791
+ if (state.screen === "tasks") {
48792
+ const selected = state.tasks[state.selectedIndex];
48793
+ if (selected) openTaskDetail(state, selected);
48794
+ render();
48795
+ return;
48796
+ }
48797
+ if (state.screen === "workflow") {
48798
+ toggleSelectedWorkflowNode(state);
48799
+ render();
48800
+ return;
48801
+ }
47303
48802
  const text = state.input.trim();
47304
48803
  if (!text) return;
47305
48804
  state.input = "";
@@ -47326,6 +48825,22 @@ async function handleCommand(params) {
47326
48825
  render();
47327
48826
  return;
47328
48827
  }
48828
+ if (name === "/workflows") {
48829
+ await openWorkflowList({ state, client, render });
48830
+ return;
48831
+ }
48832
+ if (name === "/tasks") {
48833
+ await openTaskList({ state, client, render });
48834
+ return;
48835
+ }
48836
+ if (name === "/workflow") {
48837
+ if (!arg) {
48838
+ await openWorkflowList({ state, client, render });
48839
+ return;
48840
+ }
48841
+ await openWorkflowById({ state, client, workflowId: arg, render });
48842
+ return;
48843
+ }
47329
48844
  if (name === "/signup") {
47330
48845
  finish({ action: "signup" });
47331
48846
  return;
@@ -47429,8 +48944,368 @@ function moveSelectionOrScroll(state, direction) {
47429
48944
  state.selectedIndex = clamp(state.selectedIndex + direction, 0, Math.max(0, state.examples.length - 1));
47430
48945
  return;
47431
48946
  }
48947
+ if (state.screen === "workflows") {
48948
+ state.selectedIndex = clamp(state.selectedIndex + direction, 0, Math.max(0, state.workflows.length - 1));
48949
+ return;
48950
+ }
48951
+ if (state.screen === "tasks") {
48952
+ state.selectedIndex = clamp(state.selectedIndex + direction, 0, Math.max(0, state.tasks.length - 1));
48953
+ return;
48954
+ }
48955
+ if (state.screen === "workflow") {
48956
+ if (state.workflowTab === "runs") {
48957
+ state.selectedWorkflowRunIndex = clamp(state.selectedWorkflowRunIndex + direction, 0, Math.max(0, state.workflowRuns.length - 1));
48958
+ const selectedRun = state.workflowRuns[state.selectedWorkflowRunIndex];
48959
+ state.selectedWorkflowNodeIndex = firstRunNodeIndex(state, selectedRun);
48960
+ return;
48961
+ }
48962
+ const nodeCount = state.activeWorkflow?.graph.nodes.length ?? 0;
48963
+ state.selectedWorkflowNodeIndex = clamp(state.selectedWorkflowNodeIndex + direction, 0, Math.max(0, nodeCount - 1));
48964
+ return;
48965
+ }
47432
48966
  state.scrollOffset = Math.max(0, state.scrollOffset - direction);
47433
48967
  }
48968
+ function toggleSelectedWorkflowNode(state) {
48969
+ const workflow = state.activeWorkflow;
48970
+ if (!workflow) return;
48971
+ const node = workflow.graph.nodes[state.selectedWorkflowNodeIndex];
48972
+ if (!node) return;
48973
+ if (state.workflowTab === "runs") {
48974
+ state.expandedWorkflowRunNodeId = state.expandedWorkflowRunNodeId === node.id ? null : node.id;
48975
+ } else {
48976
+ state.expandedWorkflowNodeId = state.expandedWorkflowNodeId === node.id ? null : node.id;
48977
+ }
48978
+ }
48979
+ function firstRunNodeIndex(state, run) {
48980
+ const workflow = state.activeWorkflow;
48981
+ const firstNodeRun = run?.node_runs?.[0];
48982
+ if (!workflow || !firstNodeRun) return 0;
48983
+ return Math.max(0, workflow.graph.nodes.findIndex((node) => node.id === firstNodeRun.node_id));
48984
+ }
48985
+ async function openTaskList(params) {
48986
+ const { state, client, render } = params;
48987
+ state.screen = "status";
48988
+ state.status = "Loading tasks...";
48989
+ render();
48990
+ try {
48991
+ state.tasks = await decryptUserTasks(await client.listUserTasks(), client.getMasterKeyBytes());
48992
+ state.selectedIndex = 0;
48993
+ state.scrollOffset = 0;
48994
+ state.activeTask = null;
48995
+ state.status = null;
48996
+ state.screen = "tasks";
48997
+ } catch (error) {
48998
+ state.status = taskError(error, "Could not load tasks. Use /login first if you are not signed in.");
48999
+ state.screen = "status";
49000
+ }
49001
+ render();
49002
+ }
49003
+ function openTaskDetail(state, task) {
49004
+ state.activeTask = task;
49005
+ state.screen = "task";
49006
+ state.scrollOffset = 0;
49007
+ }
49008
+ async function handleActiveTaskAction(params) {
49009
+ const { state, client, terminal, actionKey, render } = params;
49010
+ const task = state.activeTask;
49011
+ if (!task) return;
49012
+ state.status = "Updating task...";
49013
+ render();
49014
+ try {
49015
+ if (actionKey === "e") {
49016
+ const title = await promptLine(terminal, `New title for ${task.shortId}: `);
49017
+ if (!title.trim()) {
49018
+ state.status = "Edit cancelled.";
49019
+ render();
49020
+ return;
49021
+ }
49022
+ const patch = await buildUpdateUserTaskInput(task, client.getMasterKeyBytes(), { title: title.trim() });
49023
+ const updated2 = await client.updateUserTask(task.taskId, patch);
49024
+ const decrypted2 = await decryptUserTask(updated2, client.getMasterKeyBytes());
49025
+ replaceTask(state, decrypted2);
49026
+ state.activeTask = decrypted2;
49027
+ state.status = null;
49028
+ state.screen = "task";
49029
+ render();
49030
+ return;
49031
+ }
49032
+ if (actionKey === "x") {
49033
+ const confirmation = await promptLine(terminal, `Type DELETE to delete ${task.shortId}: `);
49034
+ if (confirmation !== "DELETE") {
49035
+ state.status = "Delete cancelled.";
49036
+ render();
49037
+ return;
49038
+ }
49039
+ await client.deleteUserTask(task.taskId);
49040
+ state.tasks = state.tasks.filter((candidate) => candidate.taskId !== task.taskId);
49041
+ state.activeTask = null;
49042
+ state.selectedIndex = Math.min(state.selectedIndex, Math.max(0, state.tasks.length - 1));
49043
+ state.status = `Deleted ${task.shortId}.`;
49044
+ state.screen = "tasks";
49045
+ render();
49046
+ return;
49047
+ }
49048
+ if (actionKey === "r") {
49049
+ const positionText = await promptLine(terminal, `New numeric position for ${task.shortId}: `);
49050
+ const position = Number(positionText.trim());
49051
+ if (!Number.isFinite(position)) {
49052
+ state.status = "Reorder cancelled: position must be a number.";
49053
+ render();
49054
+ return;
49055
+ }
49056
+ const updated2 = await client.reorderUserTasks({ moves: [{ task_id: task.taskId, position }] });
49057
+ const [decrypted2] = await decryptUserTasks(updated2, client.getMasterKeyBytes());
49058
+ if (decrypted2) {
49059
+ replaceTask(state, decrypted2);
49060
+ state.activeTask = decrypted2;
49061
+ }
49062
+ state.status = null;
49063
+ state.screen = "task";
49064
+ render();
49065
+ return;
49066
+ }
49067
+ const payload = actionKey === "b" ? { version: task.version, blocked_reason_code: "needs_user_input" } : { version: task.version };
49068
+ const updated = actionKey === "s" ? await client.startUserTaskWithAI(task.taskId, {
49069
+ version: task.version,
49070
+ primary_chat_id: task.primaryChatId ?? void 0,
49071
+ linked_project_ids: task.linkedProjectIds,
49072
+ plaintext_title: task.title,
49073
+ plaintext_description: task.description,
49074
+ plaintext_latest_instruction: task.latestInstruction
49075
+ }) : 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);
49076
+ const decrypted = await decryptUserTask(updated, client.getMasterKeyBytes());
49077
+ replaceTask(state, decrypted);
49078
+ state.activeTask = decrypted;
49079
+ state.status = null;
49080
+ state.screen = "task";
49081
+ } catch (error) {
49082
+ state.status = taskError(error, "Could not update task.");
49083
+ state.screen = "status";
49084
+ }
49085
+ render();
49086
+ }
49087
+ async function createTaskFromTui(params) {
49088
+ const { state, client, terminal, render } = params;
49089
+ state.status = "Creating task...";
49090
+ render();
49091
+ try {
49092
+ const title = await promptLine(terminal, "Task title: ");
49093
+ if (!title.trim()) {
49094
+ state.status = "Create cancelled.";
49095
+ render();
49096
+ return;
49097
+ }
49098
+ const description = await promptLine(terminal, "Description (optional): ");
49099
+ const input = await buildCreateUserTaskInput(client.getMasterKeyBytes(), {
49100
+ title: title.trim(),
49101
+ description: description.trim(),
49102
+ assign: "user"
49103
+ });
49104
+ const created = await client.createUserTask(input);
49105
+ const decrypted = await decryptUserTask(created, client.getMasterKeyBytes());
49106
+ replaceTask(state, decrypted);
49107
+ state.activeTask = decrypted;
49108
+ state.selectedIndex = Math.max(0, state.tasks.findIndex((task) => task.taskId === decrypted.taskId));
49109
+ state.status = null;
49110
+ state.screen = "task";
49111
+ } catch (error) {
49112
+ state.status = taskError(error, "Could not create task.");
49113
+ state.screen = "status";
49114
+ }
49115
+ render();
49116
+ }
49117
+ async function promptLine(terminal, prompt) {
49118
+ return terminal.suspend(async () => {
49119
+ const rl = createInterface3({ input: nodeStdin, output: nodeStdout });
49120
+ try {
49121
+ return await rl.question(prompt);
49122
+ } finally {
49123
+ rl.close();
49124
+ }
49125
+ });
49126
+ }
49127
+ function replaceTask(state, task) {
49128
+ const index = state.tasks.findIndex((candidate) => candidate.taskId === task.taskId);
49129
+ if (index >= 0) state.tasks[index] = task;
49130
+ else state.tasks.unshift(task);
49131
+ }
49132
+ function taskError(error, fallback) {
49133
+ return error instanceof Error ? `${fallback} ${error.message}` : fallback;
49134
+ }
49135
+ async function openWorkflowList(params) {
49136
+ const { state, client, render } = params;
49137
+ state.screen = "status";
49138
+ state.status = "Loading workflows...";
49139
+ render();
49140
+ try {
49141
+ state.workflows = await client.listWorkflows();
49142
+ state.selectedIndex = 0;
49143
+ state.scrollOffset = 0;
49144
+ state.status = null;
49145
+ state.screen = "workflows";
49146
+ } catch (error) {
49147
+ state.status = workflowError(error, "Could not load workflows. Use /login first if you are not signed in.");
49148
+ state.screen = "status";
49149
+ }
49150
+ render();
49151
+ }
49152
+ async function openWorkflowById(params) {
49153
+ const { state, client, workflowId, render } = params;
49154
+ state.screen = "status";
49155
+ state.status = `Loading workflow ${workflowId}...`;
49156
+ render();
49157
+ try {
49158
+ const workflow = await client.getWorkflow(workflowId);
49159
+ await openWorkflowDetail({ state, client, workflow, render });
49160
+ } catch (error) {
49161
+ state.status = workflowError(error, `Could not load workflow ${workflowId}.`);
49162
+ state.screen = "status";
49163
+ render();
49164
+ }
49165
+ }
49166
+ async function openWorkflowDetail(params) {
49167
+ const { state, client, workflow, render } = params;
49168
+ state.status = `Loading workflow ${workflow.id}...`;
49169
+ render();
49170
+ let detail;
49171
+ try {
49172
+ detail = await client.getWorkflow(workflow.id);
49173
+ } catch (error) {
49174
+ state.status = workflowError(error, `Could not load workflow ${workflow.id}.`);
49175
+ state.screen = "status";
49176
+ render();
49177
+ return;
49178
+ }
49179
+ state.activeWorkflow = detail;
49180
+ state.workflowRuns = [];
49181
+ state.workflowTab = "graph";
49182
+ state.selectedWorkflowNodeIndex = 0;
49183
+ state.selectedWorkflowRunIndex = 0;
49184
+ state.expandedWorkflowNodeId = null;
49185
+ state.expandedWorkflowRunNodeId = null;
49186
+ state.workflowEdit = null;
49187
+ state.screen = "workflow";
49188
+ state.scrollOffset = 0;
49189
+ state.status = null;
49190
+ render();
49191
+ await refreshActiveWorkflowRuns({ state, client, render });
49192
+ }
49193
+ async function refreshActiveWorkflowRuns(params) {
49194
+ const { state, client, render } = params;
49195
+ const workflow = state.activeWorkflow;
49196
+ if (!workflow) return;
49197
+ state.status = "Refreshing workflow runs...";
49198
+ render();
49199
+ try {
49200
+ state.workflowRuns = await client.listWorkflowRuns(workflow.id);
49201
+ state.selectedWorkflowRunIndex = clamp(state.selectedWorkflowRunIndex, 0, Math.max(0, state.workflowRuns.length - 1));
49202
+ state.status = null;
49203
+ state.screen = "workflow";
49204
+ } catch (error) {
49205
+ state.status = workflowError(error, "Could not refresh workflow runs.");
49206
+ }
49207
+ render();
49208
+ }
49209
+ async function saveWorkflowNodeTitle(params) {
49210
+ const { state, client, render } = params;
49211
+ const workflow = state.activeWorkflow;
49212
+ const edit = state.workflowEdit;
49213
+ if (!workflow || !edit) return;
49214
+ let parsedConfig = null;
49215
+ if (edit.field === "config") {
49216
+ try {
49217
+ const parsed = JSON.parse(edit.value || "{}");
49218
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("Config must be a JSON object");
49219
+ parsedConfig = parsed;
49220
+ } catch (error) {
49221
+ state.status = workflowError(error, "Invalid config JSON.");
49222
+ render();
49223
+ return;
49224
+ }
49225
+ }
49226
+ const graph = {
49227
+ ...workflow.graph,
49228
+ nodes: workflow.graph.nodes.map((node) => {
49229
+ if (node.id !== edit.nodeId) return node;
49230
+ if (edit.field === "config") return { ...node, config: parsedConfig ?? {} };
49231
+ return { ...node, title: edit.value.trim() || null };
49232
+ })
49233
+ };
49234
+ state.status = `Saving node ${edit.field}...`;
49235
+ render();
49236
+ try {
49237
+ state.activeWorkflow = await client.updateWorkflow(workflow.id, { graph });
49238
+ state.workflowEdit = null;
49239
+ state.status = `Saved node ${edit.field}.`;
49240
+ } catch (error) {
49241
+ state.status = workflowError(error, "Could not save node title.");
49242
+ }
49243
+ render();
49244
+ }
49245
+ function startWorkflowNodeEdit(state, field) {
49246
+ const workflow = state.activeWorkflow;
49247
+ if (!workflow || state.workflowTab !== "graph") {
49248
+ state.status = "Switch to the Graph tab before editing node details.";
49249
+ return;
49250
+ }
49251
+ const node = workflow.graph.nodes[state.selectedWorkflowNodeIndex];
49252
+ if (!node) return;
49253
+ state.workflowEdit = {
49254
+ nodeId: node.id,
49255
+ field,
49256
+ value: field === "config" ? JSON.stringify(node.config ?? {}) : node.title ?? ""
49257
+ };
49258
+ state.expandedWorkflowNodeId = node.id;
49259
+ state.status = `Editing ${field} for ${node.id}.`;
49260
+ }
49261
+ async function runActiveWorkflow(params) {
49262
+ const { state, client, render } = params;
49263
+ const workflow = state.activeWorkflow;
49264
+ if (!workflow) return;
49265
+ if (!workflow.enabled) {
49266
+ state.status = "Enable this workflow outside TUI before running it.";
49267
+ render();
49268
+ return;
49269
+ }
49270
+ state.status = "Starting workflow run...";
49271
+ render();
49272
+ try {
49273
+ const run = await client.runWorkflow(workflow.id, {
49274
+ idempotencyKey: `tui-${workflow.id}-${Date.now()}`,
49275
+ mode: "manual",
49276
+ input: {}
49277
+ });
49278
+ state.workflowRuns = [run, ...state.workflowRuns.filter((candidate) => candidate.id !== run.id)];
49279
+ state.status = `Started run ${run.id}. Press u to refresh.`;
49280
+ } catch (error) {
49281
+ state.status = workflowError(error, "Could not start workflow run.");
49282
+ }
49283
+ render();
49284
+ }
49285
+ async function cancelLatestWorkflowRun(params) {
49286
+ const { state, client, render } = params;
49287
+ const workflow = state.activeWorkflow;
49288
+ const run = state.workflowRuns.find((candidate) => ["queued", "running", "waiting"].includes(candidate.status));
49289
+ if (!workflow || !run) {
49290
+ state.status = "No active workflow run to cancel.";
49291
+ render();
49292
+ return;
49293
+ }
49294
+ state.status = `Cancelling run ${run.id}...`;
49295
+ render();
49296
+ try {
49297
+ const result = await client.cancelWorkflowRun(workflow.id, run.id);
49298
+ state.status = `Run ${result.run_id} ${result.status}.`;
49299
+ await refreshActiveWorkflowRuns({ state, client, render });
49300
+ } catch (error) {
49301
+ state.status = workflowError(error, "Could not cancel workflow run.");
49302
+ render();
49303
+ }
49304
+ }
49305
+ function workflowError(error, fallback) {
49306
+ const details = error instanceof Error ? error.message : String(error);
49307
+ return `${fallback} ${details}`;
49308
+ }
47434
49309
  function toggleInterest(state) {
47435
49310
  const interest = TUI_INTERESTS[state.selectedIndex];
47436
49311
  if (!interest) return;
@@ -47928,6 +49803,10 @@ async function main() {
47928
49803
  printChatsHelp();
47929
49804
  return;
47930
49805
  }
49806
+ if (command === "drafts") {
49807
+ printDraftsHelp();
49808
+ return;
49809
+ }
47931
49810
  if (command === "apps") {
47932
49811
  printAppsHelp();
47933
49812
  return;
@@ -47940,6 +49819,10 @@ async function main() {
47940
49819
  printWorkflowsHelp();
47941
49820
  return;
47942
49821
  }
49822
+ if (command === "tasks") {
49823
+ printTasksHelp();
49824
+ return;
49825
+ }
47943
49826
  if (command === "connected-accounts") {
47944
49827
  printConnectedAccountsHelp();
47945
49828
  return;
@@ -48050,6 +49933,14 @@ async function main() {
48050
49933
  await handleChats(client, subcommand, rest, parsed.flags, redactor);
48051
49934
  return;
48052
49935
  }
49936
+ if (command === "tasks") {
49937
+ await handleTasks(client, subcommand, rest, parsed.flags);
49938
+ return;
49939
+ }
49940
+ if (command === "drafts") {
49941
+ await handleDrafts(client, subcommand, rest, parsed.flags);
49942
+ return;
49943
+ }
48053
49944
  if (command === "apps") {
48054
49945
  await handleApps(client, subcommand, rest, parsed.flags);
48055
49946
  return;
@@ -48187,6 +50078,172 @@ function handleCliVersion(flags) {
48187
50078
  }
48188
50079
  console.log("OpenMates CLI is up to date.");
48189
50080
  }
50081
+ async function handleTasks(client, subcommand, rest, flags) {
50082
+ if (!subcommand || subcommand === "help" || flags.help === true) {
50083
+ printTasksHelp();
50084
+ return;
50085
+ }
50086
+ const masterKey = client.getMasterKeyBytes();
50087
+ const scope = taskScopeFromFlags(flags);
50088
+ if (subcommand === "list" || subcommand === "status") {
50089
+ if (subcommand === "status" && rest[0]) {
50090
+ const task = await resolveTask(client, masterKey, rest[0], scope);
50091
+ printTaskOutput(task, flags);
50092
+ return;
50093
+ }
50094
+ const tasks = await loadTasks(client, masterKey, scope);
50095
+ if (flags.json === true) printJson2({ tasks: tasks.map(taskToJson) });
50096
+ else console.log(renderTaskList(tasks));
50097
+ return;
50098
+ }
50099
+ if (subcommand === "board") {
50100
+ const tasks = await loadTasks(client, masterKey, scope);
50101
+ if (flags.json === true) printJson2({ tasks: tasks.map(taskToJson) });
50102
+ else console.log(renderTaskBoard(tasks));
50103
+ return;
50104
+ }
50105
+ if (subcommand === "show") {
50106
+ const id = rest[0];
50107
+ if (!id) throw new Error("Missing task ID. Usage: openmates tasks show <task-id>");
50108
+ const task = await resolveTask(client, masterKey, id, scope);
50109
+ printTaskOutput(task, flags);
50110
+ return;
50111
+ }
50112
+ if (subcommand === "create") {
50113
+ const title = taskTitleFromFlagsOrRest(flags, rest);
50114
+ const input = await buildCreateUserTaskInput(masterKey, {
50115
+ title,
50116
+ description: typeof flags.description === "string" ? flags.description : "",
50117
+ status: normalizeTaskStatus(typeof flags.status === "string" ? flags.status : void 0),
50118
+ assign: taskAssignFlag(flags),
50119
+ chatId: typeof flags.chat === "string" ? flags.chat : null,
50120
+ projectIds: splitCsvFlag(flags.project ?? flags.projects),
50121
+ planId: typeof flags.plan === "string" ? flags.plan : null,
50122
+ dueAt: parseDueAt(flags.due)
50123
+ });
50124
+ const created = await client.createUserTask(input);
50125
+ printTaskOutput(await decryptUserTask(created, masterKey), flags);
50126
+ return;
50127
+ }
50128
+ if (subcommand === "edit") {
50129
+ const id = rest[0];
50130
+ if (!id) throw new Error("Missing task ID. Usage: openmates tasks edit <task-id> [--title ...]");
50131
+ const task = await resolveTask(client, masterKey, id, scope);
50132
+ const patch = await buildUpdateUserTaskInput(task, masterKey, {
50133
+ title: typeof flags.title === "string" ? flags.title : void 0,
50134
+ description: typeof flags.description === "string" ? flags.description : void 0,
50135
+ status: normalizeTaskStatus(typeof flags.status === "string" ? flags.status : void 0),
50136
+ assign: taskAssignFlag(flags),
50137
+ chatId: flags.chat === true ? null : typeof flags.chat === "string" ? flags.chat : void 0,
50138
+ projectIds: flags.project || flags.projects ? splitCsvFlag(flags.project ?? flags.projects) : void 0,
50139
+ planId: flags.plan === true ? null : typeof flags.plan === "string" ? flags.plan : void 0
50140
+ });
50141
+ const updated = await client.updateUserTask(task.taskId, patch);
50142
+ printTaskOutput(await decryptUserTask(updated, masterKey), flags);
50143
+ return;
50144
+ }
50145
+ if (subcommand === "delete") {
50146
+ const task = await requiredResolvedTask(client, masterKey, rest[0], scope, "delete");
50147
+ if (flags.confirm !== true) throw new Error("Deleting a task requires --confirm.");
50148
+ const result = await client.deleteUserTask(task.taskId);
50149
+ if (flags.json === true) printJson2(result);
50150
+ else console.log(`Task deleted: ${task.shortId}`);
50151
+ return;
50152
+ }
50153
+ if (subcommand === "start") {
50154
+ const task = await requiredResolvedTask(client, masterKey, rest[0], scope, "start");
50155
+ const started = await client.startUserTaskWithAI(task.taskId, {
50156
+ version: task.version,
50157
+ primary_chat_id: task.primaryChatId ?? void 0,
50158
+ linked_project_ids: task.linkedProjectIds,
50159
+ plaintext_title: task.title,
50160
+ plaintext_description: task.description,
50161
+ plaintext_latest_instruction: task.latestInstruction
50162
+ });
50163
+ printTaskOutput(await decryptUserTask(started, masterKey), flags);
50164
+ return;
50165
+ }
50166
+ if (["block", "unblock", "skip", "done"].includes(subcommand)) {
50167
+ const task = await requiredResolvedTask(client, masterKey, rest[0], scope, subcommand);
50168
+ const payload = { version: task.version };
50169
+ if (subcommand === "block") {
50170
+ if (typeof flags.reason !== "string") throw new Error("Blocking a task requires --reason <code>.");
50171
+ payload.blocked_reason_code = flags.reason;
50172
+ }
50173
+ 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);
50174
+ printTaskOutput(await decryptUserTask(updated, masterKey), flags);
50175
+ return;
50176
+ }
50177
+ if (subcommand === "reorder") {
50178
+ const task = await requiredResolvedTask(client, masterKey, rest[0], scope, "reorder");
50179
+ const move = { task_id: task.taskId };
50180
+ if (typeof flags.before === "string") move.before_task_id = (await resolveTask(client, masterKey, flags.before, scope)).taskId;
50181
+ if (typeof flags.after === "string") move.after_task_id = (await resolveTask(client, masterKey, flags.after, scope)).taskId;
50182
+ if (typeof flags.status === "string") move.status = normalizeTaskStatus(flags.status);
50183
+ if (typeof flags.position === "string") move.position = Number(flags.position);
50184
+ const updated = await client.reorderUserTasks({ moves: [move] });
50185
+ const decrypted = await decryptUserTasks(updated, masterKey);
50186
+ if (flags.json === true) printJson2({ tasks: decrypted.map(taskToJson) });
50187
+ else console.log(`Task reordered: ${task.shortId}`);
50188
+ return;
50189
+ }
50190
+ throw new Error(`Unknown tasks command '${subcommand}'. Run 'openmates tasks --help'.`);
50191
+ }
50192
+ function taskScopeFromFlags(flags) {
50193
+ return {
50194
+ status: normalizeTaskStatus(typeof flags.status === "string" ? flags.status : void 0),
50195
+ chatId: typeof flags.chat === "string" ? flags.chat : void 0,
50196
+ projectId: typeof flags.project === "string" ? flags.project : void 0,
50197
+ planId: typeof flags.plan === "string" ? flags.plan : void 0
50198
+ };
50199
+ }
50200
+ async function loadTasks(client, masterKey, scope) {
50201
+ const records = await client.listUserTasks({ status: scope.status, chatId: scope.chatId, projectId: scope.projectId });
50202
+ const tasks = await decryptUserTasks(records, masterKey);
50203
+ return scope.planId ? tasks.filter((task) => task.planId === scope.planId) : tasks;
50204
+ }
50205
+ async function resolveTask(client, masterKey, id, scope) {
50206
+ return findTask(await loadTasks(client, masterKey, { ...scope, status: void 0 }), id);
50207
+ }
50208
+ async function requiredResolvedTask(client, masterKey, id, scope, action) {
50209
+ if (!id) throw new Error(`Missing task ID. Usage: openmates tasks ${action} <task-id>`);
50210
+ return resolveTask(client, masterKey, id, scope);
50211
+ }
50212
+ function printTaskOutput(task, flags) {
50213
+ if (flags.json === true) printJson2({ task: taskToJson(task) });
50214
+ else console.log(renderTaskDetail2(task));
50215
+ }
50216
+ function taskToJson(task) {
50217
+ return {
50218
+ task_id: task.taskId,
50219
+ short_id: task.shortId,
50220
+ title: task.title,
50221
+ description: task.description,
50222
+ tags: task.tags,
50223
+ latest_instruction: task.latestInstruction,
50224
+ status: task.status,
50225
+ assignee_type: task.assigneeType,
50226
+ assignee_hash: task.assigneeHash,
50227
+ primary_chat_id: task.primaryChatId,
50228
+ linked_project_ids: task.linkedProjectIds,
50229
+ plan_id: task.planId,
50230
+ due_at: task.dueAt,
50231
+ priority: task.priority,
50232
+ position: task.position,
50233
+ queue_state: task.queueState,
50234
+ blocked_reason_code: task.blockedReasonCode,
50235
+ ai_execution_state: task.aiExecutionState,
50236
+ version: task.version
50237
+ };
50238
+ }
50239
+ function taskTitleFromFlagsOrRest(flags, rest) {
50240
+ const title = typeof flags.title === "string" ? flags.title : rest.join(" ").trim();
50241
+ if (!title) throw new Error("Missing task title. Usage: openmates tasks create --title <title>");
50242
+ return title;
50243
+ }
50244
+ function taskAssignFlag(flags) {
50245
+ return typeof flags.assign === "string" ? flags.assign : typeof flags.assignee === "string" ? flags.assignee : void 0;
50246
+ }
48190
50247
  async function handleRemoteAccess(client, subcommand, rest, flags) {
48191
50248
  if (!subcommand || subcommand === "help" || flags.help === true) {
48192
50249
  printRemoteAccessHelp();
@@ -48197,7 +50254,7 @@ async function handleRemoteAccess(client, subcommand, rest, flags) {
48197
50254
  if (!rootPath) {
48198
50255
  throw new Error("Missing source path. Usage: openmates remote-access start --path <folder>");
48199
50256
  }
48200
- const sourceId = typeof flags["source-id"] === "string" ? flags["source-id"] : randomUUID5();
50257
+ const sourceId = typeof flags["source-id"] === "string" ? flags["source-id"] : randomUUID6();
48201
50258
  const projectId = typeof flags.project === "string" ? flags.project : void 0;
48202
50259
  validateRemoteSourceRegistrationFlags(projectId, flags);
48203
50260
  const sourceType = parseRemoteAccessSourceType(flags.type);
@@ -48303,11 +50360,57 @@ function parseJsonFlag(value, flagName) {
48303
50360
  throw new Error(`Invalid JSON for ${flagName}: ${message}`);
48304
50361
  }
48305
50362
  }
50363
+ async function handleDrafts(client, subcommand, rest, flags) {
50364
+ if (!subcommand || subcommand === "help" || flags.help === true) {
50365
+ printDraftsHelp();
50366
+ return;
50367
+ }
50368
+ if (subcommand === "create" || subcommand === "update") {
50369
+ const chatId = subcommand === "update" ? rest[0] : typeof flags.chat === "string" ? flags.chat : void 0;
50370
+ const markdown = subcommand === "update" ? rest.slice(1).join(" ").trim() : rest.join(" ").trim();
50371
+ if (subcommand === "update" && !chatId) throw new Error("Missing chat ID for draft update.");
50372
+ if (!markdown) throw new Error(`Missing draft text for draft ${subcommand}.`);
50373
+ const draft = await client.saveDraft({
50374
+ chatId,
50375
+ markdown,
50376
+ preview: typeof flags.preview === "string" ? flags.preview : markdown.slice(0, 160)
50377
+ });
50378
+ printJson2(draft);
50379
+ return;
50380
+ }
50381
+ if (subcommand === "list") {
50382
+ const drafts = await client.listDrafts(flags.refresh === true);
50383
+ printJson2({ drafts });
50384
+ return;
50385
+ }
50386
+ if (subcommand === "get") {
50387
+ const chatId = rest[0];
50388
+ if (!chatId) throw new Error("Missing chat ID for draft get.");
50389
+ printJson2({ draft: await client.getDraft(chatId, flags.refresh === true) });
50390
+ return;
50391
+ }
50392
+ if (subcommand === "clear") {
50393
+ const chatId = rest[0];
50394
+ if (!chatId) throw new Error("Missing chat ID for draft clear.");
50395
+ await client.clearDraft(chatId);
50396
+ printJson2({ success: true, chat_id: chatId });
50397
+ return;
50398
+ }
50399
+ if (subcommand === "sync") {
50400
+ printJson2({ versions: await client.reconcileDraftVersions() });
50401
+ return;
50402
+ }
50403
+ throw new Error(`Unknown drafts subcommand '${subcommand}'.`);
50404
+ }
48306
50405
  async function handleChats(client, subcommand, rest, flags, redactor) {
48307
50406
  if (!subcommand || subcommand === "help" || flags.help === true) {
48308
50407
  printChatsHelp();
48309
50408
  return;
48310
50409
  }
50410
+ if (rest[0] === "tasks") {
50411
+ await handleTasks(client, rest[1], rest.slice(2), { ...flags, chat: subcommand });
50412
+ return;
50413
+ }
48311
50414
  if (subcommand === "list") {
48312
50415
  const limit = typeof flags.limit === "string" ? parseInt(flags.limit, 10) : 10;
48313
50416
  const page = typeof flags.page === "string" ? parseInt(flags.page, 10) : 1;
@@ -48354,6 +50457,7 @@ async function handleChats(client, subcommand, rest, flags, redactor) {
48354
50457
  json: flags.json === true,
48355
50458
  autoApproveSubChats: flags["auto-approve"] === true,
48356
50459
  autoApproveMemories: flags["auto-approve-memories"] === true,
50460
+ acceptTaskProposals: flags["accept-task-proposals"] === true,
48357
50461
  piiDetection: flags["no-pii-detection"] !== true,
48358
50462
  anonymousLearningMode: client.hasSession() ? void 0 : parseAnonymousLearningModeFlags(flags)
48359
50463
  },
@@ -48429,6 +50533,7 @@ Run 'openmates chats show ` + chatId + "' to check if suggestions have been save
48429
50533
  json: flags.json === true,
48430
50534
  autoApproveSubChats: flags["auto-approve"] === true,
48431
50535
  autoApproveMemories: flags["auto-approve-memories"] === true,
50536
+ acceptTaskProposals: flags["accept-task-proposals"] === true,
48432
50537
  piiDetection: flags["no-pii-detection"] !== true
48433
50538
  },
48434
50539
  redactor
@@ -48457,6 +50562,7 @@ Run 'openmates chats show ` + chatId + "' to check if suggestions have been save
48457
50562
  json: flags.json === true,
48458
50563
  autoApproveSubChats: flags["auto-approve"] === true,
48459
50564
  autoApproveMemories: flags["auto-approve-memories"] === true,
50565
+ acceptTaskProposals: flags["accept-task-proposals"] === true,
48460
50566
  piiDetection: flags["no-pii-detection"] !== true
48461
50567
  },
48462
50568
  redactor
@@ -48990,7 +51096,37 @@ async function handleWorkflows(client, subcommand, rest, flags) {
48990
51096
  }
48991
51097
  return;
48992
51098
  }
51099
+ if (subcommand === "validate") {
51100
+ const file = typeof flags.file === "string" ? flags.file : "";
51101
+ if (!file) throw new Error("Missing --file. Example: openmates workflows validate --file workflow.yml");
51102
+ const validation = await client.validateWorkflowYaml(readFileSync9(file, "utf8"));
51103
+ if (flags.json === true) {
51104
+ printJson2(validation);
51105
+ } else {
51106
+ kv("Draft valid", validation.draft_valid ? "yes" : "no");
51107
+ kv("Ready to enable", validation.enable_ready ? "yes" : "no");
51108
+ for (const diagnostic of validation.diagnostics) {
51109
+ console.log(` - ${String(diagnostic.path ?? "$")}: ${String(diagnostic.message ?? diagnostic.code ?? "invalid")}`);
51110
+ }
51111
+ }
51112
+ if (!validation.draft_valid) process.exitCode = 1;
51113
+ return;
51114
+ }
48993
51115
  if (subcommand === "create") {
51116
+ const yamlFile = typeof flags.file === "string" ? flags.file : "";
51117
+ if (yamlFile) {
51118
+ const result = await client.createWorkflowYaml(readFileSync9(yamlFile, "utf8"));
51119
+ if (flags.json === true) {
51120
+ printJson2(result);
51121
+ } else {
51122
+ printWorkflowDetail(result.workflow);
51123
+ kv("Ready to enable", result.validation.enable_ready ? "yes" : "no");
51124
+ for (const diagnostic of result.validation.diagnostics) {
51125
+ console.log(` - ${String(diagnostic.path ?? "$")}: ${String(diagnostic.message ?? diagnostic.code ?? "input required")}`);
51126
+ }
51127
+ }
51128
+ return;
51129
+ }
48994
51130
  const title = typeof flags.title === "string" ? flags.title.trim() : "";
48995
51131
  const graphJson = typeof flags.graph === "string" ? flags.graph : "";
48996
51132
  if (!title) throw new Error("Missing --title for workflow create.");
@@ -49008,6 +51144,22 @@ async function handleWorkflows(client, subcommand, rest, flags) {
49008
51144
  }
49009
51145
  return;
49010
51146
  }
51147
+ if (subcommand === "update") {
51148
+ const workflowId = rest[0];
51149
+ const yamlFile = typeof flags.file === "string" ? flags.file : "";
51150
+ if (!workflowId || !yamlFile) throw new Error("Missing workflow ID or --file. Example: openmates workflows update <id> --file workflow.yml");
51151
+ const result = await client.updateWorkflowYaml(workflowId, readFileSync9(yamlFile, "utf8"));
51152
+ if (flags.json === true) {
51153
+ printJson2(result);
51154
+ } else {
51155
+ printWorkflowDetail(result.workflow);
51156
+ kv("Ready to enable", result.validation.enable_ready ? "yes" : "no");
51157
+ for (const diagnostic of result.validation.diagnostics) {
51158
+ console.log(` - ${String(diagnostic.path ?? "$")}: ${String(diagnostic.message ?? diagnostic.code ?? "input required")}`);
51159
+ }
51160
+ }
51161
+ return;
51162
+ }
49011
51163
  if (subcommand === "input") {
49012
51164
  const text = typeof flags.text === "string" ? flags.text : rest.join(" ").trim();
49013
51165
  if (!text) throw new Error('Missing workflow input text. Example: openmates workflows input "alert me if it rains"');
@@ -49114,9 +51266,11 @@ async function handleWorkflows(client, subcommand, rest, flags) {
49114
51266
  if (subcommand === "run") {
49115
51267
  const workflowId = rest[0];
49116
51268
  if (!workflowId) throw new Error("Missing workflow ID. Example: openmates workflows run <id>");
51269
+ const idempotencyKey = typeof flags["idempotency-key"] === "string" ? flags["idempotency-key"] : "";
51270
+ if (!idempotencyKey) throw new Error("Missing --idempotency-key. Reuse this stable key when retrying the same workflow run.");
49117
51271
  const mode = flags.mode === "test" ? "test" : "manual";
49118
51272
  const input = typeof flags.input === "string" ? parseJsonFlag(flags.input, "--input") : {};
49119
- const run = await client.runWorkflow(workflowId, { mode, input });
51273
+ const run = await client.runWorkflow(workflowId, { idempotencyKey, mode, input });
49120
51274
  if (flags.json === true) {
49121
51275
  printJson2(run);
49122
51276
  } else {
@@ -49147,6 +51301,58 @@ async function handleWorkflows(client, subcommand, rest, flags) {
49147
51301
  }
49148
51302
  return;
49149
51303
  }
51304
+ if (subcommand === "run-cancel") {
51305
+ const workflowId = rest[0];
51306
+ const runId = rest[1];
51307
+ if (!workflowId || !runId) throw new Error("Missing workflow/run ID. Example: openmates workflows run-cancel <workflow-id> <run-id>");
51308
+ const result = await client.cancelWorkflowRun(workflowId, runId);
51309
+ if (flags.json === true) {
51310
+ printJson2(result);
51311
+ } else {
51312
+ kv("Status", result.status);
51313
+ }
51314
+ return;
51315
+ }
51316
+ if (subcommand === "step-test") {
51317
+ const workflowId = rest[0];
51318
+ const stepId = rest[1];
51319
+ if (!workflowId || !stepId) throw new Error("Missing workflow/step ID. Example: openmates workflows step-test <workflow-id> <step-id> --yes");
51320
+ const input = typeof flags.input === "string" ? parseJsonFlag(flags.input, "--input") : {};
51321
+ const run = await client.testWorkflowStep(workflowId, stepId, { input, confirmed: flags.yes === true });
51322
+ if (flags.json === true) {
51323
+ printJson2(run);
51324
+ } else {
51325
+ printWorkflowRun(run);
51326
+ }
51327
+ return;
51328
+ }
51329
+ if (subcommand === "respond") {
51330
+ const workflowId = rest[0];
51331
+ const runId = rest[1];
51332
+ const stepId = rest[2];
51333
+ 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"}'`);
51334
+ const input = typeof flags.input === "string" ? parseJsonFlag(flags.input, "--input") : {};
51335
+ const run = await client.respondToWorkflowRun(workflowId, runId, stepId, input);
51336
+ if (flags.json === true) {
51337
+ printJson2(run);
51338
+ } else {
51339
+ printWorkflowRun(run);
51340
+ }
51341
+ return;
51342
+ }
51343
+ if (subcommand === "help-app") {
51344
+ const capabilityId = rest[0];
51345
+ if (!capabilityId) throw new Error("Missing app skill. Example: openmates workflows help-app weather.forecast");
51346
+ const capabilities = await client.listWorkflowCapabilities();
51347
+ const capability = capabilities.find((item) => item.id === capabilityId || item.id === capabilityId.replace(":", "."));
51348
+ if (!capability) throw new Error(`Workflow capability not found: ${capabilityId}`);
51349
+ if (flags.json === true) {
51350
+ printJson2(capability);
51351
+ } else {
51352
+ printWorkflowCapabilityHelp(capability);
51353
+ }
51354
+ return;
51355
+ }
49150
51356
  throw new Error(`Unknown workflows command '${subcommand}'. Run 'openmates workflows --help'.`);
49151
51357
  }
49152
51358
  function printWorkflowList(workflows) {
@@ -49216,6 +51422,30 @@ function printWorkflowCapabilities(capabilities) {
49216
51422
  kv(capability.id, `${capability.title} \xB7 ${state}`, 24);
49217
51423
  }
49218
51424
  }
51425
+ function printWorkflowCapabilityHelp(capability) {
51426
+ header(`${capability.title}
51427
+ `);
51428
+ kv("ID", capability.id);
51429
+ kv("Type", capability.type);
51430
+ kv("Enabled", capability.enabled ? "yes" : "no");
51431
+ if (capability.reason) kv("Reason", capability.reason);
51432
+ const metadata = capability.metadata ?? {};
51433
+ const workflow = metadata.workflow;
51434
+ if (workflow) {
51435
+ kv("Effect", String(workflow.effect ?? "unknown"));
51436
+ kv("Execution", String(workflow.execution_mode ?? "unknown"));
51437
+ kv("Approval", String(workflow.approval ?? "unknown"));
51438
+ kv("Unattended", workflow.unattended === true ? "yes" : "no");
51439
+ }
51440
+ if (metadata.input_schema) {
51441
+ console.log("\nInput schema:");
51442
+ console.log(JSON.stringify(metadata.input_schema, null, 2));
51443
+ }
51444
+ if (workflow?.test_example_input) {
51445
+ console.log("\nTest example:");
51446
+ console.log(JSON.stringify(workflow.test_example_input, null, 2));
51447
+ }
51448
+ }
49219
51449
  async function handleApps(client, subcommand, rest, flags) {
49220
51450
  const apiKey = resolveApiKey(flags) ?? void 0;
49221
51451
  if (!subcommand || subcommand === "help") {
@@ -49452,7 +51682,7 @@ Run with --help for full parameter details:
49452
51682
  }
49453
51683
  }
49454
51684
  try {
49455
- const inputData = buildSkillInput(flags, inlineTokens, schemaParams);
51685
+ const inputData = app === "models3d" && skill === "generate" ? await buildModels3DGenerateInput(client, flags, inlineTokens, schemaParams) : buildSkillInput(flags, inlineTokens, schemaParams);
49456
51686
  const data = await client.runSkill({ app, skill, inputData, apiKey });
49457
51687
  if (flags.json === true) {
49458
51688
  printJson2(data);
@@ -49657,6 +51887,27 @@ function buildSkillInput(flags, inlineTokens, schemaParams) {
49657
51887
  }
49658
51888
  return {};
49659
51889
  }
51890
+ async function buildModels3DGenerateInput(client, flags, inlineTokens, schemaParams) {
51891
+ const imagePath = typeof flags.image === "string" ? flags.image : void 0;
51892
+ if (!imagePath) return buildSkillInput(flags, inlineTokens, schemaParams);
51893
+ if (typeof flags.input === "string" || inlineTokens.length > 0) {
51894
+ throw new Error("Use either a text prompt or --image <path> for 3D generation, not both.");
51895
+ }
51896
+ if (!client.hasSession()) {
51897
+ throw new Error("Image-based 3D generation requires `openmates login` to upload the reference image.");
51898
+ }
51899
+ const processed = processFiles([imagePath], null);
51900
+ if (processed.blocked.length > 0 || processed.errors.length > 0 || processed.embeds.length !== 1) {
51901
+ const reason = [...processed.blocked, ...processed.errors].map((entry) => entry.error).join("; ") || "no image embed produced";
51902
+ throw new Error(`Failed to prepare 3D reference image: ${reason}`);
51903
+ }
51904
+ const fileEmbed = processed.embeds[0];
51905
+ if (!fileEmbed.requiresUpload || !fileEmbed.localPath || fileEmbed.embed.type !== "image") {
51906
+ throw new Error("3D generation requires a supported image file.");
51907
+ }
51908
+ const uploadResult = await uploadFile(fileEmbed.localPath, client.getSession());
51909
+ return { image_embed_refs: [uploadResult.embed_id] };
51910
+ }
49660
51911
  function getEffectiveRequiredParams(schemaParams) {
49661
51912
  const required = schemaParams.filter((p) => p.required);
49662
51913
  if (required.length > 0) return required;
@@ -50201,7 +52452,7 @@ async function confirmOrExit(question) {
50201
52452
  process.exit(0);
50202
52453
  }
50203
52454
  }
50204
- async function promptLine(question) {
52455
+ async function promptLine2(question) {
50205
52456
  const rl = await import("readline");
50206
52457
  const iface = rl.createInterface({ input: process.stdin, output: process.stdout });
50207
52458
  const answer = await new Promise((resolve7) => iface.question(question, resolve7));
@@ -50210,7 +52461,7 @@ async function promptLine(question) {
50210
52461
  }
50211
52462
  async function promptSecret(question) {
50212
52463
  if (!process.stdin.isTTY) {
50213
- return promptLine(question);
52464
+ return promptLine2(question);
50214
52465
  }
50215
52466
  return new Promise((resolve7) => {
50216
52467
  const stdin2 = process.stdin;
@@ -50297,8 +52548,8 @@ async function writeSecretFile(filePath, content, force = false) {
50297
52548
  return filePath;
50298
52549
  }
50299
52550
  async function generateProvisioningPassword() {
50300
- const { randomBytes: randomBytes3 } = await import("crypto");
50301
- return `OM-${randomBytes3(18).toString("base64url")}-aA2#`;
52551
+ const { randomBytes: randomBytes4 } = await import("crypto");
52552
+ return `OM-${randomBytes4(18).toString("base64url")}-aA2#`;
50302
52553
  }
50303
52554
  function decodeBase32(input) {
50304
52555
  const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
@@ -50333,7 +52584,7 @@ async function runSecuritySetup(client, flags, options = {}) {
50333
52584
  result.otpSecret = setup.secret ?? null;
50334
52585
  if (setup.otpauth_url) client.renderTotpQrCode(setup.otpauth_url);
50335
52586
  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: ");
52587
+ 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
52588
  await client.verifyTotpSetup(code);
50338
52589
  const provider = typeof flags.provider === "string" ? flags.provider : "Authenticator app";
50339
52590
  await client.setTotpProvider(provider);
@@ -50373,8 +52624,8 @@ async function handleSignup(client, flags, options = {}) {
50373
52624
  if (flags.password !== void 0) {
50374
52625
  throw new Error("Passwords must be entered through hidden prompts, not command-line flags.");
50375
52626
  }
50376
- const email = typeof flags.email === "string" ? flags.email : await promptLine("Email: ");
50377
- const username = typeof flags.username === "string" ? flags.username : await promptLine("Username: ");
52627
+ const email = typeof flags.email === "string" ? flags.email : await promptLine2("Email: ");
52628
+ const username = typeof flags.username === "string" ? flags.username : await promptLine2("Username: ");
50378
52629
  const inviteCode = typeof flags["invite-code"] === "string" ? flags["invite-code"] : "";
50379
52630
  const language = typeof flags.language === "string" ? flags.language : "en";
50380
52631
  const password = process.env.OPENMATES_CLI_SIGNUP_PASSWORD ?? await promptSecret("Password: ");
@@ -50383,7 +52634,7 @@ async function handleSignup(client, flags, options = {}) {
50383
52634
  if (password !== confirmPassword) throw new Error("Passwords do not match.");
50384
52635
  }
50385
52636
  await client.requestSignupEmailCode({ email, inviteCode, language });
50386
- const emailCode = process.env.OPENMATES_CLI_SIGNUP_EMAIL_CODE ?? await promptLine("Email verification code: ");
52637
+ const emailCode = process.env.OPENMATES_CLI_SIGNUP_EMAIL_CODE ?? await promptLine2("Email verification code: ");
50387
52638
  await client.verifySignupEmailCode({ email, username, inviteCode, code: emailCode, language });
50388
52639
  const signup = await client.setupPasswordAccount({ email, username, password, inviteCode, language });
50389
52640
  const security = await runSecuritySetup(client, flags, options);
@@ -50425,13 +52676,15 @@ async function handleE2E(client, subcommand, rest, flags) {
50425
52676
  throw new Error("E2E provisioning refuses production API URLs.");
50426
52677
  }
50427
52678
  if (flags.password !== void 0) throw new Error("Use generated passwords or OPENMATES_CLI_SIGNUP_PASSWORD, not --password.");
52679
+ const inviteCode = process.env.OPENMATES_CLI_SIGNUP_INVITE_CODE;
52680
+ if (!inviteCode) throw new Error("OPENMATES_CLI_SIGNUP_INVITE_CODE is required for E2E provisioning.");
50428
52681
  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.");
52682
+ if (![14, 15, 16, 17, 18, 19, 20].includes(slot)) {
52683
+ throw new Error("Only reserved slots 14-20 are supported.");
50431
52684
  }
50432
52685
  const artifact = typeof flags.artifact === "string" ? flags.artifact : `test-results/credential-updates/slot-${slot}.env`;
50433
52686
  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: ");
52687
+ const email = typeof flags.email === "string" ? flags.email : domain ? `cli-e2e-slot-${slot}-${Date.now()}@${domain}` : await promptLine2("E2E account email: ");
50435
52688
  const username = typeof flags.username === "string" ? flags.username : `cli_e2e_slot_${slot}_${Date.now()}`;
50436
52689
  const generatedPassword = process.env.OPENMATES_CLI_SIGNUP_PASSWORD ?? await generateProvisioningPassword();
50437
52690
  const originalSignupPassword = process.env.OPENMATES_CLI_SIGNUP_PASSWORD;
@@ -50442,6 +52695,7 @@ async function handleE2E(client, subcommand, rest, flags) {
50442
52695
  ...flags,
50443
52696
  email,
50444
52697
  username,
52698
+ "invite-code": inviteCode,
50445
52699
  yes: true,
50446
52700
  json: false,
50447
52701
  "backup-codes-output": typeof flags["backup-codes-output"] === "string" ? flags["backup-codes-output"] : `${artifact}.backup-codes`,
@@ -50636,10 +52890,10 @@ async function handleSettings(client, subcommand, rest, flags) {
50636
52890
  await confirmOrExit("Delete this account and all associated data? [y/N] ");
50637
52891
  }
50638
52892
  await client.requestDeleteAccountEmailCode();
50639
- const emailCode = await promptLine("Enter email verification code: ");
52893
+ const emailCode = await promptLine2("Enter email verification code: ");
50640
52894
  await client.verifyDeleteAccountEmailCode(emailCode);
50641
52895
  const authMethods = await client.getAuthMethodsStatus();
50642
- const totpCode = authMethods.has_2fa ? await promptLine("Enter 2FA code: ") : void 0;
52896
+ const totpCode = authMethods.has_2fa ? await promptLine2("Enter 2FA code: ") : void 0;
50643
52897
  const result = await client.deleteAccountWithCliVerification(totpCode);
50644
52898
  if (flags.json === true) printJson2(result);
50645
52899
  else console.log("\x1B[32m\u2713\x1B[0m Account deletion requested and local CLI session cleared");
@@ -51358,11 +53612,11 @@ function ansiMateBlock(category, mateName) {
51358
53612
  const [r, g, b] = CATEGORY_ANSI_COLORS[category];
51359
53613
  return `\x1B[48;2;${r};${g};${b}m\x1B[97m ${label} \x1B[0m`;
51360
53614
  }
51361
- function formatTimestamp(ts) {
53615
+ function formatTimestamp2(ts) {
51362
53616
  if (!ts) return "\u2014";
51363
53617
  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())}`;
53618
+ const pad2 = (n) => String(n).padStart(2, "0");
53619
+ return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())} ${pad2(d.getHours())}:${pad2(d.getMinutes())}`;
51366
53620
  }
51367
53621
  function printChatsTable(result) {
51368
53622
  const { chats, total, page, limit, hasMore } = result;
@@ -51381,7 +53635,7 @@ function printChatsTable(result) {
51381
53635
  );
51382
53636
  for (const chat of chats) {
51383
53637
  const block = ansiColorBlock(chat.category);
51384
- const time = formatTimestamp(chat.updatedAt);
53638
+ const time = formatTimestamp2(chat.updatedAt);
51385
53639
  const title = chat.title ?? "(no title)";
51386
53640
  const idStr = chat.shortId;
51387
53641
  const sourceLabel = chat.source === "example" ? " \x1B[33mEXAMPLE CHAT\x1B[0m" : "";
@@ -51717,7 +53971,7 @@ async function sendMessageStreaming(client, params, redactor) {
51717
53971
  process.stdout.write(`${result2.assistant}
51718
53972
  `);
51719
53973
  }
51720
- return result2;
53974
+ return { ...result2, acceptedTaskProposals: [] };
51721
53975
  }
51722
53976
  const urlResult = prepareUrlEmbeds(finalMessage);
51723
53977
  finalMessage = urlResult.message;
@@ -51741,6 +53995,9 @@ async function sendMessageStreaming(client, params, redactor) {
51741
53995
  }))
51742
53996
  });
51743
53997
  clearTyping();
53998
+ const acceptedTaskProposals = params.acceptTaskProposals === true && result.status === "completed" ? await acceptChatTaskProposals(client, result.chatId, result.taskProposals, `${finalMessage}
53999
+
54000
+ ${result.assistant}`) : [];
51744
54001
  if (result.status === "waiting_for_user") {
51745
54002
  const question = parseInteractiveQuestionBlock(result.assistant);
51746
54003
  if (params.json && question) {
@@ -51756,7 +54013,7 @@ async function sendMessageStreaming(client, params, redactor) {
51756
54013
  "\x1B[33mOpenMates needs more input to continue.\x1B[0m\nContinue in the web app, or rerun with --json to inspect the structured waiting state.\n"
51757
54014
  );
51758
54015
  }
51759
- return result;
54016
+ return { ...result, acceptedTaskProposals: [] };
51760
54017
  }
51761
54018
  if (!params.json) {
51762
54019
  if (!headerPrinted) {
@@ -51826,7 +54083,43 @@ async function sendMessageStreaming(client, params, redactor) {
51826
54083
  `
51827
54084
  );
51828
54085
  }
51829
- return result;
54086
+ return { ...result, acceptedTaskProposals };
54087
+ }
54088
+ async function acceptChatTaskProposals(client, chatId, proposals, fallbackText) {
54089
+ let proposalsToAccept = proposals;
54090
+ if (proposalsToAccept.length === 0 && fallbackText.trim()) {
54091
+ const extractionText = buildTaskProposalFallbackText(fallbackText);
54092
+ proposalsToAccept = await client.extractUserTaskProposals({
54093
+ correctedText: extractionText,
54094
+ contextChatId: chatId
54095
+ });
54096
+ }
54097
+ if (proposalsToAccept.length === 0) return [];
54098
+ const masterKey = client.getMasterKeyBytes();
54099
+ const accepted = [];
54100
+ for (const proposal of proposalsToAccept) {
54101
+ const input = await buildCreateUserTaskInput(masterKey, {
54102
+ title: proposal.title,
54103
+ description: proposal.description ?? "",
54104
+ status: proposal.status,
54105
+ assign: proposal.assignee_type ?? "user",
54106
+ chatId
54107
+ });
54108
+ const created = await client.createUserTask(input);
54109
+ const decrypted = await decryptUserTask(created, masterKey);
54110
+ accepted.push(taskToJson(decrypted));
54111
+ }
54112
+ return accepted;
54113
+ }
54114
+ function buildTaskProposalFallbackText(text) {
54115
+ const seen = /* @__PURE__ */ new Set();
54116
+ const bulletLines = text.split("\n").map((line) => line.trim()).filter((line) => /^[-*•]\s+/.test(line)).map((line) => line.replace(/^([-*•]\s*)\[[ xX]\]\s*/, "$1")).filter((line) => {
54117
+ const key = line.replace(/^[-*•]\s+/, "").trim().toLowerCase();
54118
+ if (seen.has(key)) return false;
54119
+ seen.add(key);
54120
+ return true;
54121
+ });
54122
+ return bulletLines.length > 0 ? bulletLines.join("\n") : text;
51830
54123
  }
51831
54124
  function printIncognitoNoHistoryNotice(json) {
51832
54125
  const message = "Incognito chats are not stored. There is no incognito history to show or clear.";
@@ -51942,7 +54235,7 @@ async function renderMessageText(text, embedRefIndex, client) {
51942
54235
  async function printChatConversation(client, chat, messages, followUpSuggestions, options = {}) {
51943
54236
  const block = ansiColorBlock(chat.category);
51944
54237
  const title = chat.title ?? "(no title)";
51945
- const ts = formatTimestamp(chat.updatedAt);
54238
+ const ts = formatTimestamp2(chat.updatedAt);
51946
54239
  if (options.example) {
51947
54240
  process.stdout.write("\x1B[33mEXAMPLE CHAT\x1B[0m\n");
51948
54241
  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 +54265,7 @@ async function printChatConversation(client, chat, messages, followUpSuggestions
51972
54265
  const embedRefIndex = hasEmbedRefs ? await client.buildEmbedRefIndex(parentEmbedIds) : /* @__PURE__ */ new Map();
51973
54266
  for (let i = 0; i < messages.length; i++) {
51974
54267
  const msg = messages[i];
51975
- const msgTs = formatTimestamp(msg.createdAt);
54268
+ const msgTs = formatTimestamp2(msg.createdAt);
51976
54269
  process.stdout.write(`${SEP}
51977
54270
  `);
51978
54271
  if (msg.role === "user") {
@@ -52033,7 +54326,7 @@ async function printChatConversation(client, chat, messages, followUpSuggestions
52033
54326
  }
52034
54327
  async function printChatConversationRaw(chat, messages, options = {}) {
52035
54328
  const title = chat.title ?? "(no title)";
52036
- const ts = formatTimestamp(chat.updatedAt);
54329
+ const ts = formatTimestamp2(chat.updatedAt);
52037
54330
  if (options.example) {
52038
54331
  process.stdout.write("EXAMPLE CHAT\n");
52039
54332
  process.stdout.write("This is a public example chat. Log in to create and sync your own private chats.\n\n");
@@ -52048,7 +54341,7 @@ async function printChatConversationRaw(chat, messages, options = {}) {
52048
54341
  return;
52049
54342
  }
52050
54343
  for (const msg of messages) {
52051
- const msgTs = formatTimestamp(msg.createdAt);
54344
+ const msgTs = formatTimestamp2(msg.createdAt);
52052
54345
  process.stdout.write(`${SEP}
52053
54346
  `);
52054
54347
  const role = msg.role === "user" ? "You" : msg.senderName ?? msg.role;
@@ -52551,18 +54844,18 @@ function printWhoAmI(user) {
52551
54844
  }
52552
54845
  }
52553
54846
  function printGenericObject(value, indent = 0) {
52554
- const pad = " ".repeat(indent);
54847
+ const pad2 = " ".repeat(indent);
52555
54848
  if (value === null || value === void 0) {
52556
- console.log(`${pad}(empty)`);
54849
+ console.log(`${pad2}(empty)`);
52557
54850
  return;
52558
54851
  }
52559
54852
  if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
52560
- console.log(`${pad}${value}`);
54853
+ console.log(`${pad2}${value}`);
52561
54854
  return;
52562
54855
  }
52563
54856
  if (Array.isArray(value)) {
52564
54857
  if (value.length === 0) {
52565
- console.log(`${pad}(empty list)`);
54858
+ console.log(`${pad2}(empty list)`);
52566
54859
  return;
52567
54860
  }
52568
54861
  for (let i = 0; i < value.length; i++) {
@@ -52570,11 +54863,11 @@ function printGenericObject(value, indent = 0) {
52570
54863
  if (item && typeof item === "object") {
52571
54864
  printGenericObject(item, indent);
52572
54865
  if (i < value.length - 1) {
52573
- process.stdout.write(`${pad}\x1B[2m${"\u2500".repeat(40)}\x1B[0m
54866
+ process.stdout.write(`${pad2}\x1B[2m${"\u2500".repeat(40)}\x1B[0m
52574
54867
  `);
52575
54868
  }
52576
54869
  } else {
52577
- console.log(`${pad}${String(item)}`);
54870
+ console.log(`${pad2}${String(item)}`);
52578
54871
  }
52579
54872
  }
52580
54873
  return;
@@ -52594,34 +54887,34 @@ function printGenericObject(value, indent = 0) {
52594
54887
  if (Array.isArray(v)) {
52595
54888
  if (v.length === 0) {
52596
54889
  process.stdout.write(
52597
- `${pad} \x1B[2m${k.padEnd(20)}\x1B[0m (none)
54890
+ `${pad2} \x1B[2m${k.padEnd(20)}\x1B[0m (none)
52598
54891
  `
52599
54892
  );
52600
54893
  } else if (v.length > 0 && typeof v[0] === "object") {
52601
54894
  process.stdout.write(
52602
54895
  `
52603
- ${pad} \x1B[1m${k}\x1B[0m \x1B[2m(${v.length})\x1B[0m
54896
+ ${pad2} \x1B[1m${k}\x1B[0m \x1B[2m(${v.length})\x1B[0m
52604
54897
  `
52605
54898
  );
52606
54899
  printGenericObject(v, indent + 1);
52607
54900
  } else {
52608
54901
  process.stdout.write(
52609
- `${pad} \x1B[2m${k.padEnd(20)}\x1B[0m ${v.join(", ")}
54902
+ `${pad2} \x1B[2m${k.padEnd(20)}\x1B[0m ${v.join(", ")}
52610
54903
  `
52611
54904
  );
52612
54905
  }
52613
54906
  } else if (typeof v === "object") {
52614
- process.stdout.write(`${pad} \x1B[1m${k}\x1B[0m
54907
+ process.stdout.write(`${pad2} \x1B[1m${k}\x1B[0m
52615
54908
  `);
52616
54909
  printGenericObject(v, indent + 1);
52617
54910
  } else {
52618
- process.stdout.write(`${pad} \x1B[2m${k.padEnd(20)}\x1B[0m ${v}
54911
+ process.stdout.write(`${pad2} \x1B[2m${k.padEnd(20)}\x1B[0m ${v}
52619
54912
  `);
52620
54913
  }
52621
54914
  }
52622
54915
  return;
52623
54916
  }
52624
- console.log(`${pad}${String(value)}`);
54917
+ console.log(`${pad2}${String(value)}`);
52625
54918
  }
52626
54919
  function printBillingResponse(obj) {
52627
54920
  header("Billing\n");
@@ -52697,7 +54990,7 @@ function printMemoriesList(memories) {
52697
54990
  header(`Memories (${memories.length})
52698
54991
  `);
52699
54992
  for (const mem of memories) {
52700
- const ts = formatTimestamp(mem.updated_at);
54993
+ const ts = formatTimestamp2(mem.updated_at);
52701
54994
  process.stdout.write(
52702
54995
  `\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
54996
  `
@@ -52947,46 +55240,46 @@ async function handleMentions(client, subcommand, _rest, flags) {
52947
55240
  process.exit(1);
52948
55241
  }
52949
55242
  function serializeToYaml(data, indent = 0) {
52950
- const pad = " ".repeat(indent);
55243
+ const pad2 = " ".repeat(indent);
52951
55244
  let out = "";
52952
55245
  for (const [key, val] of Object.entries(data)) {
52953
55246
  if (val === null || val === void 0) {
52954
- out += `${pad}${key}: null
55247
+ out += `${pad2}${key}: null
52955
55248
  `;
52956
55249
  } else if (typeof val === "boolean" || typeof val === "number") {
52957
- out += `${pad}${key}: ${val}
55250
+ out += `${pad2}${key}: ${val}
52958
55251
  `;
52959
55252
  } else if (typeof val === "string") {
52960
55253
  if (val.includes("\n")) {
52961
- out += `${pad}${key}: |
55254
+ out += `${pad2}${key}: |
52962
55255
  `;
52963
55256
  for (const line of val.split("\n")) {
52964
- out += `${pad} ${line}
55257
+ out += `${pad2} ${line}
52965
55258
  `;
52966
55259
  }
52967
55260
  } else {
52968
55261
  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}
55262
+ out += `${pad2}${key}: ${needsQuote ? `"${val.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"` : val}
52970
55263
  `;
52971
55264
  }
52972
55265
  } else if (Array.isArray(val)) {
52973
- out += `${pad}${key}:
55266
+ out += `${pad2}${key}:
52974
55267
  `;
52975
55268
  for (const item of val) {
52976
55269
  if (typeof item === "object" && item !== null) {
52977
- out += `${pad}-
55270
+ out += `${pad2}-
52978
55271
  `;
52979
55272
  out += serializeToYaml(
52980
55273
  item,
52981
55274
  indent + 2
52982
55275
  );
52983
55276
  } else {
52984
- out += `${pad}- ${item}
55277
+ out += `${pad2}- ${item}
52985
55278
  `;
52986
55279
  }
52987
55280
  }
52988
55281
  } else if (typeof val === "object") {
52989
- out += `${pad}${key}:
55282
+ out += `${pad2}${key}:
52990
55283
  `;
52991
55284
  out += serializeToYaml(val, indent + 1);
52992
55285
  }
@@ -53074,6 +55367,8 @@ Commands:
53074
55367
  openmates logout Log out and clear session
53075
55368
  openmates whoami [--json] Show account info
53076
55369
  openmates chats [--help] Chat commands (list, search, show, ...)
55370
+ openmates tasks [--help] Task commands (list, create, board, ...)
55371
+ openmates drafts [--help] Encrypted draft lifecycle commands
53077
55372
  openmates apps [--help] App skill commands (list, run, ...)
53078
55373
  openmates workflows [--help] Server-side workflow commands
53079
55374
  openmates mentions [--help] List available @mentions
@@ -53212,7 +55507,7 @@ Creates local ignored credential artifacts for reserved E2E auth accounts. The
53212
55507
  command refuses production API URLs and does not upload GitHub secrets.
53213
55508
 
53214
55509
  Options:
53215
- --slot <15-20> Reserved auth-account slot
55510
+ --slot <14-20> Reserved auth-account slot
53216
55511
  --artifact <path> Output .env artifact path
53217
55512
  --email <email> Test email; prompted/generated when omitted
53218
55513
  --domain <mail-domain> Generate email at this domain
@@ -53225,10 +55520,10 @@ function printChatsHelp() {
53225
55520
  openmates chats show <chat-id> [--raw] [--json]
53226
55521
  openmates chats open [<n|example-id|slug>] [--json]
53227
55522
  openmates chats search <query> [--json]
53228
- openmates chats new <message> [--json] [--learning-mode --age-group <group>] [--auto-approve] [--auto-approve-memories] [--no-pii-detection]
53229
- openmates chats send [--chat <id>] [--incognito] <message> [--json] [--auto-approve] [--auto-approve-memories] [--no-pii-detection]
55523
+ openmates chats new <message> [--json] [--learning-mode --age-group <group>] [--auto-approve] [--auto-approve-memories] [--accept-task-proposals] [--no-pii-detection]
55524
+ openmates chats send [--chat <id>] [--incognito] <message> [--json] [--auto-approve] [--auto-approve-memories] [--accept-task-proposals] [--no-pii-detection]
53230
55525
  openmates chats send --chat <id> --followup <n> [--json] [--auto-approve] [--auto-approve-memories]
53231
- openmates chats answer-interactive --chat <id> --question-json '<json>' --answer-json '<json>' [--json]
55526
+ openmates chats answer-interactive --chat <id> --question-json '<json>' --answer-json '<json>' [--json] [--accept-task-proposals]
53232
55527
  openmates chats download <chat-id> [--output <path>] [--zip] [--json]
53233
55528
  openmates chats delete <id1> [id2] [id3] ... [--yes]
53234
55529
  openmates chats share [<chat-id>] [--expires <seconds>] [--password <pwd>] [--json]
@@ -53271,6 +55566,11 @@ Options for 'new', 'send', and 'incognito':
53271
55566
  --no-pii-detection Send the message exactly as typed. By default, the CLI
53272
55567
  replaces detected PII with placeholders before send.
53273
55568
 
55569
+ Saved-chat task options for 'new', 'send', and 'answer-interactive':
55570
+ --accept-task-proposals Explicitly save assistant task proposals as encrypted
55571
+ Tasks V1 records scoped to the chat. Without this,
55572
+ proposals remain review-only, like the web app card.
55573
+
53274
55574
  Guest-only options for logged-out 'new':
53275
55575
  --learning-mode Opt anonymous chat into request-scoped Learning Mode.
53276
55576
  --age-group <group> Required with --learning-mode: under_10, 10_12,
@@ -53326,6 +55626,46 @@ Examples:
53326
55626
  openmates chats share last --expires 604800
53327
55627
  openmates chats share d262cb68 --password mypass`);
53328
55628
  }
55629
+ function printTasksHelp() {
55630
+ console.log(`Tasks commands:
55631
+ openmates tasks list [--status <status>] [--chat <id>] [--project <id>] [--json]
55632
+ openmates tasks board [--chat <id>] [--project <id>] [--json]
55633
+ openmates tasks show <task-id|short-id> [--json]
55634
+ openmates tasks create --title <title> [--description <text>] [--assign user|ai] [--chat <id>] [--project <id>] [--status <status>] [--due <date>] [--json]
55635
+ openmates tasks edit <task-id|short-id> [--title <title>] [--description <text>] [--assign user|ai] [--status <status>] [--json]
55636
+ openmates tasks delete <task-id|short-id> --confirm [--json]
55637
+ openmates tasks start <task-id|short-id> [--json]
55638
+ openmates tasks status [<task-id|short-id>] [--json]
55639
+ openmates tasks block <task-id|short-id> --reason <code> [--json]
55640
+ openmates tasks unblock <task-id|short-id> [--json]
55641
+ openmates tasks skip <task-id|short-id> [--json]
55642
+ openmates tasks done <task-id|short-id> [--json]
55643
+ openmates tasks reorder <task-id|short-id> [--before <task-id>] [--after <task-id>] [--position <n>] [--status <status>] [--json]
55644
+
55645
+ Chat-scoped aliases:
55646
+ openmates chats <chat-id> tasks list
55647
+ openmates chats <chat-id> tasks board
55648
+ openmates chats <chat-id> tasks create --title <title>
55649
+
55650
+ Statuses:
55651
+ backlog, todo, in_progress, blocked, done
55652
+
55653
+ Notes:
55654
+ Task IDs accept full task_id or human short IDs such as OM-6.
55655
+ Normal output decrypts task title and description locally; use --json for machine-readable plaintext fields.`);
55656
+ }
55657
+ function printDraftsHelp() {
55658
+ console.log(`Draft commands:
55659
+ openmates drafts create <text> [--chat <uuid>] [--preview <text>] [--json]
55660
+ openmates drafts update <chat-id> <text> [--preview <text>] [--json]
55661
+ openmates drafts list [--refresh] [--json]
55662
+ openmates drafts get <chat-id> [--refresh] [--json]
55663
+ openmates drafts clear <chat-id> [--json]
55664
+ openmates drafts sync [--json]
55665
+
55666
+ Draft plaintext is encrypted locally with the account master key. Only Format-D
55667
+ ciphertext and version metadata are sent to the server or written to CLI cache.`);
55668
+ }
53329
55669
  function printAppsHelp() {
53330
55670
  console.log(`Apps commands:
53331
55671
  openmates apps list [--json]
@@ -53334,6 +55674,7 @@ function printAppsHelp() {
53334
55674
  openmates apps skill-info <app-id> <skill-id> [--json]
53335
55675
  openmates apps <app-id> <skill-id> "<query>" [--json]
53336
55676
  openmates apps <app-id> <skill-id> --input '<json>' [--json]
55677
+ openmates apps models3d generate --image ./reference.png [--json]
53337
55678
  openmates apps code run --language python --code 'print("Hello")'
53338
55679
  openmates apps code run --entry main.py --file main.py [--file requirements.txt]
53339
55680
  openmates apps code run --entry main.py --dir ./project [--exclude node_modules]
@@ -53349,6 +55690,7 @@ Examples:
53349
55690
  openmates apps web search "latest AI news"
53350
55691
  openmates apps news search "climate change"
53351
55692
  openmates apps ai ask "Summarise this: ..."
55693
+ openmates apps models3d generate --image ./chair.png
53352
55694
  openmates apps code run --language python --filename hello.py --code 'print("Hello from CLI")'
53353
55695
  openmates apps travel search_connections --input '{"requests":[{"legs":[{"origin":"BER","destination":"LHR","date":"2026-04-15"}]}]}'
53354
55696
  openmates apps travel booking-link --token "<booking_token from search result>"
@@ -53358,6 +55700,9 @@ function printWorkflowsHelp() {
53358
55700
  console.log(`Workflows commands:
53359
55701
  openmates workflows list [--json]
53360
55702
  openmates workflows capabilities [--json]
55703
+ openmates workflows validate --file workflow.yml [--json]
55704
+ openmates workflows create --file workflow.yml [--json]
55705
+ openmates workflows update <workflow-id> --file workflow.yml [--json]
53361
55706
  openmates workflows create --title <title> --graph '<json>' [--enabled] [--run-content-retention last_5|none] [--json]
53362
55707
  openmates workflows input <text> [--workflow-id <id>] [--project-id <id>] [--json]
53363
55708
  openmates workflows input-show <session-id> [--json]
@@ -53368,9 +55713,13 @@ function printWorkflowsHelp() {
53368
55713
  openmates workflows show <workflow-id> [--json]
53369
55714
  openmates workflows enable <workflow-id> [--json]
53370
55715
  openmates workflows disable <workflow-id> [--json]
53371
- openmates workflows run <workflow-id> [--mode manual|test] [--input '<json>'] [--json]
55716
+ openmates workflows run <workflow-id> --idempotency-key <stable-key> [--mode manual|test] [--input '<json>'] [--json]
53372
55717
  openmates workflows runs <workflow-id> [--json]
53373
55718
  openmates workflows run-show <workflow-id> <run-id> [--json]
55719
+ openmates workflows run-cancel <workflow-id> <run-id> [--json]
55720
+ openmates workflows step-test <workflow-id> <step-id> [--input '<json>'] [--yes] [--json]
55721
+ openmates workflows respond <workflow-id> <run-id> <step-id> --input '<json>' [--json]
55722
+ openmates workflows help-app <app.skill> [--json]
53374
55723
  openmates workflows delete <workflow-id> --yes [--json]
53375
55724
 
53376
55725
  Workflows run on the OpenMates server, not in this terminal process. The CLI
@@ -53380,6 +55729,7 @@ and Apple clients.
53380
55729
  Examples:
53381
55730
  openmates workflows list
53382
55731
  openmates workflows capabilities --json
55732
+ openmates workflows help-app weather.forecast
53383
55733
  openmates workflows input "alert me if it rains tomorrow"
53384
55734
  openmates workflows run wf_123 --mode test --json`);
53385
55735
  }
@@ -53631,9 +55981,12 @@ if (isCliEntrypoint()) {
53631
55981
  }
53632
55982
 
53633
55983
  export {
55984
+ deriveChatCompletionRecoveryKeypair,
55985
+ openChatCompletionRecoveryEnvelope,
53634
55986
  unwrapApiKeyMasterKey,
53635
55987
  decryptWithAesGcmCombined,
53636
55988
  decryptBytesWithAesGcm,
55989
+ encryptBytesWithAesGcm,
53637
55990
  encryptWithAesGcmCombined,
53638
55991
  hashItemKey,
53639
55992
  generateChatShareBlob,
@@ -53646,6 +55999,7 @@ export {
53646
55999
  INTEREST_TAG_IDS,
53647
56000
  normalizeInterestTagIds,
53648
56001
  MEMORY_TYPE_REGISTRY,
56002
+ reconcileAuthoritativeChats,
53649
56003
  MATE_NAMES,
53650
56004
  deriveAppUrl,
53651
56005
  OpenMatesClient,