@my-life-buddies/cli 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -175,6 +175,22 @@ var PreviewController = class {
175
175
  }
176
176
  return this.simulatorSnapshot();
177
177
  }
178
+ /** Never allow an old conversation's delayed widget response to enter the new one. */
179
+ async simulatorWidgets(action, id = "", mediaId = "") {
180
+ if (this.#resetting) throw Object.assign(new Error("\u4F1A\u8BDD\u6B63\u5728\u91CD\u7F6E"), { status: 409 });
181
+ const client = this.#requiredSimulator();
182
+ const revision = this.#conversationRevision;
183
+ let result;
184
+ if (action === "list" && client.widgets) result = { widgets: await client.widgets(), conversationRevision: revision };
185
+ else if (action === "html" && client.widgetHtml) result = await client.widgetHtml(id);
186
+ else if (action === "image" && client.widgetImage) result = await client.widgetImage(id, mediaId);
187
+ else if ((action === "pin" || action === "unpin") && client.pinWidget) {
188
+ await client.pinWidget(id, action === "pin");
189
+ result = { ok: true };
190
+ } else throw Object.assign(new Error("\u5F53\u524D\u6A21\u62DF\u5668\u4E0D\u652F\u6301\u5C0F\u6302\u4EF6"), { status: 501 });
191
+ if (this.#resetting || revision !== this.#conversationRevision || client !== this.#simulator) throw Object.assign(new Error("\u4F1A\u8BDD\u5DF2\u5207\u6362\uFF0C\u8BF7\u91CD\u65B0\u6253\u5F00\u5C0F\u6302\u4EF6"), { status: 409 });
192
+ return result;
193
+ }
178
194
  async resetSimulator() {
179
195
  if (this.#resetting || this.#sendingMessage || this.#simulatorState.activeRun) {
180
196
  throw Object.assign(new Error("\u8BF7\u7B49\u5F85\u5F53\u524D\u56DE\u590D\u6216\u64CD\u4F5C\u5B8C\u6210\u540E\u518D\u6E05\u7A7A"), { status: 409, code: "SIMULATOR_BUSY" });
@@ -578,13 +594,14 @@ var PreviewController = class {
578
594
  };
579
595
 
580
596
  // apps/preview/src/server.ts
597
+ import { checkWidgetDelivery } from "./core.js";
581
598
  import { randomBytes, timingSafeEqual } from "node:crypto";
582
599
  import { readFileSync } from "node:fs";
583
600
  import { createServer } from "node:http";
584
601
  import { createRequire } from "node:module";
585
602
  import QRCode from "qrcode";
586
603
  var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["127.0.0.1", "::1", "localhost"]);
587
- var SESSION_COOKIE = "buddy_preview_session";
604
+ var SESSION_COOKIE_PREFIX = "buddy_preview_session";
588
605
  var require2 = createRequire(import.meta.url);
589
606
  var BRAND_ICON = Buffer.from(
590
607
  readFileSync(new URL("../web/assets/developer-platform-icon.png.base64", import.meta.url), "utf8").trim(),
@@ -607,6 +624,9 @@ var WEB_ASSETS = Object.freeze(/* @__PURE__ */ new Map([
607
624
  contentType: "text/css; charset=utf-8",
608
625
  body: readFileSync(new URL("../web/brand.css", import.meta.url))
609
626
  }],
627
+ ["/assets/widget-delivery.js", { contentType: "text/javascript; charset=utf-8", body: readFileSync(new URL("../web/widget-delivery.js", import.meta.url)) }],
628
+ ["/assets/widgets.js", { contentType: "text/javascript; charset=utf-8", body: readFileSync(new URL("../web/widgets.js", import.meta.url)) }],
629
+ ["/assets/widgets.css", { contentType: "text/css; charset=utf-8", body: readFileSync(new URL("../web/widgets.css", import.meta.url)) }],
610
630
  ["/assets/app.js", {
611
631
  contentType: "text/javascript; charset=utf-8",
612
632
  body: readFileSync(new URL("../web/app.js", import.meta.url))
@@ -771,6 +791,7 @@ data: ${JSON.stringify(value)}
771
791
  if (!allowedHost(request, Number(origin.port)) || !allowedOrigin(request, origin)) {
772
792
  return apiError(response, 403, "PREVIEW_ORIGIN_FORBIDDEN", "\u8BF7\u6C42\u6765\u6E90\u4E0D\u53D7\u4FE1\u4EFB");
773
793
  }
794
+ const sessionCookie = `${SESSION_COOKIE_PREFIX}_${origin.port}`;
774
795
  const url = requestUrl(request, origin);
775
796
  if (!url) return apiError(response, 400, "INVALID_URL", "\u8BF7\u6C42\u5730\u5740\u65E0\u6548");
776
797
  if (request.method === "GET" && bootstrapAvailable && url.pathname === `/bootstrap/${sessionToken}`) {
@@ -779,13 +800,13 @@ data: ${JSON.stringify(value)}
779
800
  writeHeaders(response);
780
801
  response.setHeader(
781
802
  "Set-Cookie",
782
- `${SESSION_COOKIE}=${sessionToken}; HttpOnly; SameSite=Strict; Path=/; Max-Age=43200`
803
+ `${sessionCookie}=${sessionToken}; HttpOnly; SameSite=Strict; Path=/; Max-Age=43200`
783
804
  );
784
805
  response.setHeader("Location", "/");
785
806
  response.end();
786
807
  return;
787
808
  }
788
- const suppliedToken = cookieValue(request, SESSION_COOKIE);
809
+ const suppliedToken = cookieValue(request, sessionCookie);
789
810
  if (!suppliedToken || !safeEqual(suppliedToken, sessionToken)) {
790
811
  return apiError(response, 401, "PREVIEW_UNAUTHORIZED", "\u9700\u8981\u4ECE CLI \u63D0\u4F9B\u7684\u4E00\u6B21\u6027\u5165\u53E3\u6253\u5F00 Preview");
791
812
  }
@@ -793,6 +814,9 @@ data: ${JSON.stringify(value)}
793
814
  const asset = WEB_ASSETS.get(url.pathname);
794
815
  if (asset) return staticAsset(response, asset);
795
816
  }
817
+ if (request.method === "GET" && url.pathname === "/api/widgets/delivery") {
818
+ return json(response, 200, await checkWidgetDelivery(options.controller.projectRoot));
819
+ }
796
820
  if (request.method === "GET" && url.pathname === "/api/project") {
797
821
  try {
798
822
  return json(response, 200, { project: publicProject(await options.controller.project()) });
@@ -833,6 +857,35 @@ data: ${JSON.stringify(value)}
833
857
  return apiError(response, failure.status, failure.code, failure.message);
834
858
  }
835
859
  }
860
+ const widgetRoute = /^\/api\/simulator\/widgets(?:\/([^/]+)(?:\/(image|pin))?)?$/u.exec(url.pathname);
861
+ if (widgetRoute) {
862
+ try {
863
+ const id = widgetRoute[1] ? decodeURIComponent(widgetRoute[1]) : "";
864
+ const suffix = widgetRoute[2];
865
+ if (request.method === "GET" && !id) return json(response, 200, await options.controller.simulatorWidgets("list"));
866
+ if (id && suffix === "pin" && (request.method === "PUT" || request.method === "DELETE")) return json(response, 200, await options.controller.simulatorWidgets(request.method === "PUT" ? "pin" : "unpin", id));
867
+ if (id && request.method === "GET" && suffix === "image") {
868
+ const image = await options.controller.simulatorWidgets("image", id, url.searchParams.get("mediaId") ?? "");
869
+ if (!/^image\/(png|jpeg|webp|gif)(;|$)/u.test(image.contentType)) throw new Error("\u56FE\u7247\u683C\u5F0F\u4E0D\u652F\u6301");
870
+ writeHeaders(response);
871
+ response.setHeader("Content-Type", image.contentType);
872
+ response.end(image.body);
873
+ return;
874
+ }
875
+ if (id && request.method === "GET" && !suffix) {
876
+ const html = await options.controller.simulatorWidgets("html", id);
877
+ writeHeaders(response);
878
+ response.setHeader("Content-Security-Policy", "sandbox allow-scripts; default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src data:; connect-src 'none'; frame-src 'none'; base-uri 'none'; form-action 'none'; frame-ancestors 'self'");
879
+ response.setHeader("Content-Type", "text/html; charset=utf-8");
880
+ response.end(html);
881
+ return;
882
+ }
883
+ return apiError(response, 405, "METHOD_NOT_ALLOWED", "\u4E0D\u652F\u6301\u8FD9\u4E2A\u5C0F\u6302\u4EF6\u64CD\u4F5C");
884
+ } catch (cause) {
885
+ const failure = requestFailure(cause);
886
+ return apiError(response, failure.status, failure.code, failure.message);
887
+ }
888
+ }
836
889
  if (request.method === "GET" && url.pathname === "/api/events") {
837
890
  response.statusCode = 200;
838
891
  writeHeaders(response);
@@ -1229,18 +1282,51 @@ function message(value, index) {
1229
1282
  const role = input.author === "buddy" ? "assistant" : input.author === "user" || input.author === "owner" ? "user" : void 0;
1230
1283
  if (!role) return null;
1231
1284
  const messageId = safeText(input.id, `messages[${index}].id`, 256);
1232
- if (typeof input.text !== "string" || Buffer.byteLength(input.text, "utf8") > 48 * 1024) {
1285
+ let text2 = input.type === "widget" && input.text === void 0 ? "" : input.text;
1286
+ if (typeof text2 !== "string" || Buffer.byteLength(text2, "utf8") > 48 * 1024) {
1233
1287
  throw new Error(`messages[${index}].text is invalid`);
1234
1288
  }
1235
- if (input.type === "tool" && !input.text.trim()) return null;
1289
+ if (input.type === "tool" && !text2.trim()) return null;
1236
1290
  if (typeof input.timestamp !== "number" || !Number.isFinite(input.timestamp)) {
1237
1291
  throw new Error(`messages[${index}].timestamp is invalid`);
1238
1292
  }
1293
+ if (role === "assistant" && input.type === "question") {
1294
+ const options = object(input.question)?.options;
1295
+ if (!Array.isArray(options) || options.length < 2 || options.length > 4 || options.some((option) => typeof option !== "string" || !option.trim() || option.length > 30)) {
1296
+ throw new Error(`messages[${index}].question is invalid`);
1297
+ }
1298
+ text2 += `
1299
+
1300
+ ${options.map((option, i) => `${i + 1}. ${option}`).join("\n")}
1301
+
1302
+ \u8BF7\u76F4\u63A5\u56DE\u590D\u4F60\u7684\u9009\u62E9\uFF0C\u4E5F\u53EF\u4EE5\u8865\u5145\u8BF4\u660E\u3002`;
1303
+ }
1304
+ if (role === "assistant" && input.type === "permissionRequest") {
1305
+ const request = object(input.permissionRequest);
1306
+ text2 += request?.status === "pending" ? "\n\n\u8BF7\u5728\u642D\u642D App \u4E2D\u5904\u7406\u8FD9\u5F20\u6388\u6743\u5361\uFF1B\u6D4F\u89C8\u5668\u9884\u89C8\u4E0D\u4EE3\u66FF\u7CFB\u7EDF\u6388\u6743\u3002" : "\n\n\u6388\u6743\u5361\u7684\u5904\u7406\u7ED3\u679C\u8BF7\u4EE5\u642D\u642D App \u4E2D\u7684\u72B6\u6001\u4E3A\u51C6\u3002";
1307
+ }
1239
1308
  return Object.freeze({
1240
1309
  messageId,
1241
1310
  role,
1242
- text: input.text,
1243
- createdAt: new Date(input.timestamp).toISOString()
1311
+ text: text2,
1312
+ createdAt: new Date(input.timestamp).toISOString(),
1313
+ ...input.type === "widget" && object(input.widget) ? { widget: parseWidget(input.widget) } : {}
1314
+ });
1315
+ }
1316
+ function widgetId(value) {
1317
+ const id = safeText(value, "widgetId", 64);
1318
+ if (/[\s/\\]/u.test(id) || id === "." || id === "..") throw new TypeError("widgetId is invalid");
1319
+ return id;
1320
+ }
1321
+ function parseWidget(value) {
1322
+ const item = object(value);
1323
+ return Object.freeze({
1324
+ id: widgetId(item?.id),
1325
+ title: typeof item?.title === "string" && item.title.trim() ? item.title : "\u5C0F\u6302\u4EF6",
1326
+ ...typeof item?.type === "string" ? { type: item.type } : {},
1327
+ ...typeof item?.summary === "string" ? { summary: item.summary } : {},
1328
+ ...typeof item?.imageMediaId === "string" && /^media_[a-zA-Z0-9_-]+$/u.test(item.imageMediaId) ? { imageMediaId: item.imageMediaId } : {},
1329
+ ...typeof item?.isPinned === "boolean" ? { isPinned: item.isPinned } : {}
1244
1330
  });
1245
1331
  }
1246
1332
  function localReceipt(prefix, value) {
@@ -1292,6 +1378,52 @@ var MlbSimulatorClient = class {
1292
1378
  this.#privateId = details.privateId ?? void 0;
1293
1379
  return details;
1294
1380
  }
1381
+ async widgets() {
1382
+ this.#assertResetComplete();
1383
+ const id = await this.#ensurePrivate();
1384
+ const body = object(await this.#request("GET", `/app/privates/${encodeURIComponent(id)}/widgets`, {}));
1385
+ if (!Array.isArray(body?.widgets)) throw httpError(502, { error: "\u65E0\u6CD5\u8BFB\u53D6\u4F1A\u8BDD\u5C0F\u6302\u4EF6" });
1386
+ return body.widgets.map(parseWidget).filter((item) => item.type !== void 0);
1387
+ }
1388
+ async widgetHtml(id) {
1389
+ this.#assertResetComplete();
1390
+ const privateId = await this.#ensurePrivate();
1391
+ const result = await this.#widgetResource(`/app/widgets/${encodeURIComponent(widgetId(id))}?conversationId=${encodeURIComponent(privateId)}`);
1392
+ return new TextDecoder("utf-8", { fatal: true }).decode(result.body);
1393
+ }
1394
+ async pinWidget(id, pinned) {
1395
+ this.#assertResetComplete();
1396
+ const privateId = await this.#ensurePrivate();
1397
+ await this.#request(pinned ? "PUT" : "DELETE", `/app/widgets/${encodeURIComponent(widgetId(id))}/pin`, { ...pinned ? { body: { sourceConversationId: privateId } } : {} });
1398
+ }
1399
+ async widgetImage(id, mediaId) {
1400
+ this.#assertResetComplete();
1401
+ if (!/^media_[a-zA-Z0-9_-]+$/u.test(mediaId)) throw new TypeError("Invalid media ID");
1402
+ const access = object(await this.#request("GET", `/app/media/${encodeURIComponent(mediaId)}?widgetId=${encodeURIComponent(widgetId(id))}`, {}));
1403
+ const url = new URL(String(access?.url));
1404
+ if (url.protocol !== "https:" || url.username || url.password || access?.mediaId !== mediaId || typeof access.expiresAt !== "number" || access.expiresAt <= Date.now()) throw new Error("\u56FE\u7247\u94FE\u63A5\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u8BD5");
1405
+ return this.#widgetResource(url.href, false);
1406
+ }
1407
+ async #widgetResource(path, authenticated = true) {
1408
+ const response = await this.#fetch(authenticated ? endpoint(this.#baseUrl, path) : new URL(path), { headers: authenticated ? { Authorization: `Bearer ${this.#accessToken}` } : {}, credentials: "omit", redirect: "error", signal: AbortSignal.timeout(15e3) });
1409
+ if (!response.ok) throw httpError(response.status, { error: response.status === 403 || response.status === 404 ? "\u5C0F\u6302\u4EF6\u5DF2\u5220\u9664\u3001\u9875\u9762\u672A\u4E0A\u4F20\uFF0C\u6216\u4F60\u5DF2\u4E0D\u5728\u8FD9\u4E2A\u4F1A\u8BDD\u4E2D\u3002" : "\u5C0F\u6302\u4EF6\u6682\u65F6\u65E0\u6CD5\u6253\u5F00\uFF0C\u8BF7\u91CD\u65B0\u52A0\u8F7D\u3002" });
1410
+ const reader = response.body?.getReader();
1411
+ if (!reader) throw new Error("\u5C0F\u6302\u4EF6\u5185\u5BB9\u4E3A\u7A7A");
1412
+ const chunks = [];
1413
+ let size = 0;
1414
+ try {
1415
+ while (true) {
1416
+ const part = await reader.read();
1417
+ if (part.done) break;
1418
+ size += part.value.byteLength;
1419
+ if (size > 4 * 1024 * 1024) throw new Error("\u5C0F\u6302\u4EF6\u5185\u5BB9\u8FC7\u5927\uFF0C\u6682\u65F6\u65E0\u6CD5\u6253\u5F00");
1420
+ chunks.push(part.value);
1421
+ }
1422
+ } finally {
1423
+ await reader.cancel();
1424
+ }
1425
+ return { body: Buffer.concat(chunks), contentType: response.headers.get("content-type") ?? "text/html" };
1426
+ }
1295
1427
  async state(options = {}) {
1296
1428
  this.#assertResetComplete();
1297
1429
  const privateId = await this.#ensurePrivate(options.signal);
@@ -1359,14 +1491,16 @@ var MlbSimulatorClient = class {
1359
1491
  value = await mlbAppCommand(url, this.#accessToken, "message.send", {
1360
1492
  kind: "private",
1361
1493
  id: privateId,
1362
- message: input.text,
1494
+ messages: [{ type: "text", text: input.text }],
1363
1495
  clientMessageId
1364
1496
  }, { signal: options.signal });
1365
1497
  const receipt2 = object(value);
1366
- if (receipt2?.ok !== true || receipt2.clientMessageId !== clientMessageId || typeof receipt2.messageId !== "string" || !receipt2.messageId.trim() || !Number.isSafeInteger(receipt2.timestamp) || receipt2.timestamp < 0 || typeof receipt2.shouldReply !== "boolean") {
1498
+ const items = Array.isArray(receipt2?.items) ? receipt2.items : [];
1499
+ const item2 = object(items[0]);
1500
+ if (receipt2?.ok !== true || receipt2.clientMessageId !== clientMessageId || items.length !== 1 || item2?.index !== 0 || typeof item2.messageId !== "string" || !item2.messageId.trim() || !Number.isSafeInteger(item2.timestamp) || item2.timestamp < 0 || typeof receipt2.shouldReply !== "boolean") {
1367
1501
  throw httpError(502, { error: "MLB \u6D88\u606F\u56DE\u6267\u65E0\u6548" });
1368
1502
  }
1369
- safeText(receipt2.messageId, "receipt.messageId", 256);
1503
+ safeText(item2.messageId, "receipt.items[0].messageId", 256);
1370
1504
  } catch (cause) {
1371
1505
  if (cause && typeof cause === "object" && "status" in cause && typeof cause.status === "number" && cause.status >= 400 && cause.status < 500) throw cause;
1372
1506
  throw Object.assign(new Error("\u53D1\u9001\u7ED3\u679C\u5C1A\u672A\u786E\u8BA4\uFF0C\u8BF7\u5237\u65B0\u804A\u5929\u8BB0\u5F55\u540E\u518D\u91CD\u8BD5"), {
@@ -1376,11 +1510,12 @@ var MlbSimulatorClient = class {
1376
1510
  });
1377
1511
  }
1378
1512
  const receipt = object(value);
1513
+ const item = object(receipt.items[0]);
1379
1514
  const runId = localReceipt("turn", idempotencyKey);
1380
- const messageId = safeText(receipt.messageId, "receipt.messageId", 256);
1515
+ const messageId = safeText(item.messageId, "receipt.items[0].messageId", 256);
1381
1516
  this.#active = receipt.shouldReply ? Object.freeze({
1382
1517
  runId,
1383
- messageTimestamp: receipt.timestamp,
1518
+ messageTimestamp: item.timestamp,
1384
1519
  observedReplying: false
1385
1520
  }) : void 0;
1386
1521
  this.#latest = Object.freeze({ runId, status: receipt.shouldReply ? "pending" : "completed" });