@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.
- package/README.md +47 -31
- package/dist/bin/core.js +901 -432
- package/dist/bin/core.js.map +4 -4
- package/dist/bin/preview.js +147 -12
- package/dist/bin/preview.js.map +3 -3
- package/dist/web/app.css +8 -0
- package/dist/web/app.js +7 -2
- package/dist/web/index.html +11 -3
- package/dist/web/widget-delivery.js +28 -0
- package/dist/web/widgets.css +31 -0
- package/dist/web/widgets.js +127 -0
- package/package.json +3 -3
- package/resources/agent-template/package-lock.json +4 -4
- package/resources/agent-template/package.json +1 -1
package/dist/bin/preview.js
CHANGED
|
@@ -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
|
|
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
|
-
`${
|
|
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,
|
|
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
|
-
|
|
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" && !
|
|
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:
|
|
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
|
-
|
|
1494
|
+
messages: [{ type: "text", text: input.text }],
|
|
1363
1495
|
clientMessageId
|
|
1364
1496
|
}, { signal: options.signal });
|
|
1365
1497
|
const receipt2 = object(value);
|
|
1366
|
-
|
|
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(
|
|
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(
|
|
1515
|
+
const messageId = safeText(item.messageId, "receipt.items[0].messageId", 256);
|
|
1381
1516
|
this.#active = receipt.shouldReply ? Object.freeze({
|
|
1382
1517
|
runId,
|
|
1383
|
-
messageTimestamp:
|
|
1518
|
+
messageTimestamp: item.timestamp,
|
|
1384
1519
|
observedReplying: false
|
|
1385
1520
|
}) : void 0;
|
|
1386
1521
|
this.#latest = Object.freeze({ runId, status: receipt.shouldReply ? "pending" : "completed" });
|