manualofme-bridge 0.2.1 → 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.
Files changed (2) hide show
  1. package/cli.js +1331 -544
  2. package/package.json +1 -1
package/cli.js CHANGED
@@ -34,6 +34,15 @@ import os from "node:os";
34
34
  import path from "node:path";
35
35
  import { fileURLToPath } from "node:url";
36
36
  import { z } from "zod";
37
+ function bridgeInstance(cwd, name, hostname = os.hostname()) {
38
+ const machine = hostname.trim().slice(0, 100) || "PC";
39
+ return {
40
+ id: createHash("sha256").update(`${machine.toLowerCase()}
41
+ ${sameCase(path.resolve(cwd))}`).digest("hex").slice(0, 16),
42
+ machine,
43
+ project: (name ?? path.basename(path.resolve(cwd))).slice(0, 200)
44
+ };
45
+ }
37
46
  function parseConfig(env) {
38
47
  const set = Object.fromEntries(Object.entries(env).filter(([, v]) => v !== void 0 && v !== ""));
39
48
  const r = Env.safeParse(set);
@@ -55,7 +64,8 @@ ${lines.join("\n")}`);
55
64
  expoAccessToken: e.EXPO_ACCESS_TOKEN,
56
65
  tunnel: e.BRIDGE_TUNNEL,
57
66
  cloudflaredPath: e.CLOUDFLARED_PATH,
58
- apiUrl: e.BRIDGE_API_URL.replace(/\/+$/, "")
67
+ apiUrl: e.BRIDGE_API_URL.replace(/\/+$/, ""),
68
+ instance: bridgeInstance(cwd, e.BRIDGE_NAME)
59
69
  };
60
70
  }
61
71
  function loadEnvFiles() {
@@ -106,7 +116,9 @@ var init_config = __esm({
106
116
  /** Đường dẫn cloudflared, khi không nằm trong PATH và không ở chỗ winget cài. */
107
117
  CLOUDFLARED_PATH: z.string().optional(),
108
118
  /** API server để gửi địa chỉ tunnel lên, app tự lấy về. */
109
- BRIDGE_API_URL: z.url().default(DEFAULT_API_URL)
119
+ BRIDGE_API_URL: z.url().default(DEFAULT_API_URL),
120
+ /** Tên project hiện trong app. Mặc định là tên thư mục làm việc. */
121
+ BRIDGE_NAME: z.string().trim().min(1).max(200).optional()
110
122
  });
111
123
  envFiles = () => [...APP_DIR ? [path.join(APP_DIR, ".env")] : [], USER_ENV_FILE];
112
124
  }
@@ -247,7 +259,7 @@ var init_sha256 = __esm({
247
259
 
248
260
  // ../../packages/shared/src/enums.ts
249
261
  import { z as z2 } from "zod";
250
- var SOURCES, Source, TASK_STATUSES, TaskStatus, TASK_PRIORITIES, TaskPriority, REMINDER_STATUSES, ReminderStatus, TARGET_TYPES, TargetType, TX_TYPES, TxType, CATEGORY_KINDS, CategoryKind, AGENT_AUTONOMIES, AgentAutonomy, AI_PROVIDER_MODES, AiProviderMode, AI_PROVIDERS, AiProvider, CONVERSATION_STATUSES, ConversationStatus, DEVICE_PLATFORMS, DevicePlatform, NOTICE_KINDS, NoticeKind;
262
+ var SOURCES, Source, TASK_STATUSES, TaskStatus, TASK_PRIORITIES, TaskPriority, REPEAT_FROMS, RepeatFrom, PLANNED_BYS, PlannedBy, REMINDER_STATUSES, ReminderStatus, TARGET_TYPES, TargetType, TX_TYPES, TxType, EVENT_SCOPES, EventScope, CATEGORY_KINDS, CategoryKind, AGENT_AUTONOMIES, AgentAutonomy, AI_PROVIDER_MODES, AiProviderMode, AI_PROVIDERS, AiProvider, CONVERSATION_STATUSES, ConversationStatus, DEVICE_PLATFORMS, DevicePlatform, NOTICE_KINDS, NoticeKind;
251
263
  var init_enums = __esm({
252
264
  "../../packages/shared/src/enums.ts"() {
253
265
  "use strict";
@@ -257,12 +269,27 @@ var init_enums = __esm({
257
269
  TaskStatus = z2.enum(TASK_STATUSES);
258
270
  TASK_PRIORITIES = ["low", "medium", "high", "urgent"];
259
271
  TaskPriority = z2.enum(TASK_PRIORITIES);
272
+ REPEAT_FROMS = ["due", "completion"];
273
+ RepeatFrom = z2.enum(REPEAT_FROMS);
274
+ PLANNED_BYS = ["auto", "user"];
275
+ PlannedBy = z2.enum(PLANNED_BYS);
260
276
  REMINDER_STATUSES = ["scheduled", "fired", "snoozed", "done", "cancelled"];
261
277
  ReminderStatus = z2.enum(REMINDER_STATUSES);
262
- TARGET_TYPES = ["task", "note", "transaction", "person", "none"];
278
+ TARGET_TYPES = [
279
+ "task",
280
+ "note",
281
+ "transaction",
282
+ "person",
283
+ "event",
284
+ "habit",
285
+ "bill",
286
+ "none"
287
+ ];
263
288
  TargetType = z2.enum(TARGET_TYPES);
264
289
  TX_TYPES = ["expense", "income", "transfer", "debt_out", "debt_in"];
265
290
  TxType = z2.enum(TX_TYPES);
291
+ EVENT_SCOPES = ["this", "following", "all"];
292
+ EventScope = z2.enum(EVENT_SCOPES);
266
293
  CATEGORY_KINDS = ["expense", "income"];
267
294
  CategoryKind = z2.enum(CATEGORY_KINDS);
268
295
  AGENT_AUTONOMIES = ["ask_more", "balanced", "act_more"];
@@ -279,7 +306,21 @@ var init_enums = __esm({
279
306
  "daily_briefing",
280
307
  "debt_digest",
281
308
  "budget_alert",
282
- "ai_budget_alert"
309
+ "ai_budget_alert",
310
+ "google_sync_error",
311
+ "bill_logged",
312
+ "afternoon_briefing",
313
+ "evening_review",
314
+ "meeting_reminder",
315
+ "calendar_alert",
316
+ "weekly_preview",
317
+ // Mục tiêu (docs/10 §1.4)
318
+ "goal_plan",
319
+ "goal_risk",
320
+ "goal_review",
321
+ "goal_milestone",
322
+ "goal_block"
323
+ // nhắc trước khối giờ làm việc mục tiêu do bộ xếp lịch đặt
283
324
  ];
284
325
  NoticeKind = z2.enum(NOTICE_KINDS);
285
326
  }
@@ -290,12 +331,14 @@ import { z as z3 } from "zod";
290
331
  function queryList(item) {
291
332
  return z3.string().transform((v) => v.split(",").filter(Boolean)).pipe(z3.array(item).min(1));
292
333
  }
293
- var Id, IsoDateTime, QueryBool, PageQuery;
334
+ var Id, IsoDateTime, DateKey, HhMm, QueryBool, PageQuery;
294
335
  var init_common = __esm({
295
336
  "../../packages/shared/src/schemas/common.ts"() {
296
337
  "use strict";
297
338
  Id = z3.string().min(1).max(64);
298
339
  IsoDateTime = z3.iso.datetime({ offset: true });
340
+ DateKey = z3.string().regex(/^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$/, "C\u1EA7n d\u1EA1ng YYYY-MM-DD");
341
+ HhMm = z3.string().regex(/^([01]\d|2[0-3]):[0-5]\d$/, "\u0110\u1ECBnh d\u1EA1ng HH:mm");
299
342
  QueryBool = z3.enum(["true", "false"]).transform((v) => v === "true");
300
343
  PageQuery = z3.object({
301
344
  cursor: z3.string().max(500).optional(),
@@ -304,335 +347,6 @@ var init_common = __esm({
304
347
  }
305
348
  });
306
349
 
307
- // ../../packages/shared/src/schemas/notify.ts
308
- import { z as z4 } from "zod";
309
- var ExpoToken, WebPushSubscription, WebToken, RegisterDevice, Device, Notice, ListNoticesQuery, Connector, ReadNotices;
310
- var init_notify = __esm({
311
- "../../packages/shared/src/schemas/notify.ts"() {
312
- "use strict";
313
- init_enums();
314
- init_common();
315
- ExpoToken = z4.string().regex(/^Expo(nent)?PushToken\[[^\]]+\]$/, "Kh\xF4ng ph\u1EA3i Expo push token");
316
- WebPushSubscription = z4.object({
317
- endpoint: z4.url({ protocol: /^https$/ }).max(1e3),
318
- keys: z4.object({
319
- p256dh: z4.string().min(80).max(100),
320
- // khoá công khai P-256 dạng base64url (65 byte)
321
- auth: z4.string().min(16).max(30)
322
- // 16 byte base64url
323
- })
324
- });
325
- WebToken = z4.union([z4.string().max(2e3), z4.record(z4.string(), z4.unknown())]).transform((v, ctx) => {
326
- let raw = v;
327
- if (typeof v === "string") {
328
- try {
329
- raw = JSON.parse(v);
330
- } catch {
331
- ctx.addIssue({ code: "custom", message: "pushToken kh\xF4ng ph\u1EA3i JSON" });
332
- return z4.NEVER;
333
- }
334
- }
335
- const sub = WebPushSubscription.safeParse(raw);
336
- if (!sub.success) {
337
- ctx.addIssue({ code: "custom", message: "Subscription Web Push kh\xF4ng h\u1EE3p l\u1EC7" });
338
- return z4.NEVER;
339
- }
340
- const { endpoint, keys } = sub.data;
341
- return JSON.stringify({ endpoint, keys: { p256dh: keys.p256dh, auth: keys.auth } });
342
- });
343
- RegisterDevice = z4.discriminatedUnion("platform", [
344
- z4.object({ platform: z4.literal("android"), pushToken: ExpoToken }).strict(),
345
- z4.object({ platform: z4.literal("web"), pushToken: WebToken }).strict()
346
- ]);
347
- Device = z4.object({
348
- id: z4.string(),
349
- platform: DevicePlatform,
350
- lastSeen: z4.string()
351
- });
352
- Notice = z4.object({
353
- id: z4.string(),
354
- kind: NoticeKind,
355
- title: z4.string(),
356
- body: z4.string(),
357
- data: z4.record(z4.string(), z4.unknown()).nullable(),
358
- // vd {budgetId, month} cho nút "Xem chi tiết"
359
- createdAt: z4.string(),
360
- readAt: z4.string().nullable()
361
- });
362
- ListNoticesQuery = PageQuery.extend({ unread: QueryBool.optional() });
363
- Connector = z4.object({
364
- clientId: z4.string(),
365
- name: z4.string(),
366
- uri: z4.string().nullable(),
367
- icon: z4.string().nullable(),
368
- scopes: z4.array(z4.string()),
369
- connectedAt: z4.string(),
370
- lastUsedAt: z4.string().nullable()
371
- // lần cấp access token gần nhất (khoảng mỗi giờ khi đang dùng)
372
- });
373
- ReadNotices = z4.object({ ids: z4.array(z4.string()).min(1).max(200).optional() }).strict();
374
- }
375
- });
376
-
377
- // ../../packages/shared/src/schemas/bridge.ts
378
- import { z as z5 } from "zod";
379
- function deriveBridgeKey(token) {
380
- const digest2 = sha256(new TextEncoder().encode(`manualofme-bridge-url:${token}`));
381
- return `${BRIDGE_KEY_PREFIX}${base64url(digest2)}`;
382
- }
383
- var BRIDGE_SUBPROTOCOL, BRIDGE_CLOSE_UNAUTHORIZED, BRIDGE_PERMISSION_MODES, BridgePermissionMode, BridgeModel, BRIDGE_MAX_ATTACHMENTS, BridgeAttachment, BridgeClientMessage, BridgeChatItem, BridgePermissionRequest, BridgeSessionInfo, BridgeServerMessage, BRIDGE_KEY_PREFIX, RegisterBridgeKey, BridgePublicUrl, PublishBridgeUrl, BridgeEndpoint;
384
- var init_bridge = __esm({
385
- "../../packages/shared/src/schemas/bridge.ts"() {
386
- "use strict";
387
- init_sha256();
388
- init_notify();
389
- BRIDGE_SUBPROTOCOL = "manualofme-bridge";
390
- BRIDGE_CLOSE_UNAUTHORIZED = 4401;
391
- BRIDGE_PERMISSION_MODES = [
392
- "default",
393
- "acceptEdits",
394
- "plan",
395
- "bypassPermissions"
396
- ];
397
- BridgePermissionMode = z5.enum(BRIDGE_PERMISSION_MODES);
398
- BridgeModel = z5.string().max(100).regex(/^[A-Za-z0-9._[\]-]+$/, "T\xEAn model kh\xF4ng h\u1EE3p l\u1EC7").nullable();
399
- BRIDGE_MAX_ATTACHMENTS = 4;
400
- BridgeAttachment = z5.object({
401
- name: z5.string().trim().min(1).max(200),
402
- mimeType: z5.string().max(200),
403
- data: z5.string().regex(/^[A-Za-z0-9+/]*={0,2}$/, "data ph\u1EA3i l\xE0 base64")
404
- }).strict();
405
- BridgeClientMessage = z5.discriminatedUnion("type", [
406
- /** Liệt kê các session của project (cả session mở từ VS Code). */
407
- z5.object({ type: z5.literal("list_sessions") }).strict(),
408
- /** Chuyển sang session khác. `null` = session mới (tạo khi gửi tin đầu tiên). */
409
- z5.object({ type: z5.literal("open_session"), sessionId: z5.uuid().nullable() }).strict(),
410
- z5.object({
411
- type: z5.literal("send"),
412
- /** Rỗng được khi có tệp đính kèm. */
413
- text: z5.string().trim().max(2e4),
414
- clientId: z5.string().max(64).optional(),
415
- // bridge trả lại trong item user để app khử trùng
416
- attachments: z5.array(BridgeAttachment).max(BRIDGE_MAX_ATTACHMENTS).optional()
417
- }).strict(),
418
- /**
419
- * Đổi chế độ duyệt / model, áp cho mọi app và nhớ qua lần khởi động lại. Đổi chế độ khi đang
420
- * chạy thì các yêu cầu duyệt mà chế độ mới tự cho phép được duyệt luôn; model áp từ lượt sau.
421
- */
422
- z5.object({
423
- type: z5.literal("set_options"),
424
- permissionMode: BridgePermissionMode.optional(),
425
- model: BridgeModel.optional()
426
- }).strict(),
427
- /** Dừng lượt đang chạy. */
428
- z5.object({ type: z5.literal("interrupt") }).strict(),
429
- z5.object({
430
- type: z5.literal("permission_reply"),
431
- requestId: z5.string().min(1).max(200),
432
- allow: z5.boolean(),
433
- message: z5.string().max(1e3).optional()
434
- // lý do từ chối, gửi lại cho Claude
435
- }).strict(),
436
- /** Đăng ký Expo push token để nhận thông báo khi xong việc / cần duyệt. */
437
- z5.object({ type: z5.literal("register_push"), token: ExpoToken }).strict(),
438
- z5.object({ type: z5.literal("unregister_push"), token: ExpoToken }).strict()
439
- ]);
440
- BridgeChatItem = z5.discriminatedUnion("kind", [
441
- z5.object({
442
- kind: z5.literal("user"),
443
- id: z5.string(),
444
- text: z5.string(),
445
- clientId: z5.string().optional()
446
- // chỉ có với tin vừa gửi từ app
447
- }),
448
- z5.object({ kind: z5.literal("assistant"), id: z5.string(), text: z5.string() }),
449
- /** Claude gọi tool. `summary` là phần dễ đọc của input: lệnh bash, đường dẫn file... */
450
- z5.object({ kind: z5.literal("tool"), id: z5.string(), name: z5.string(), summary: z5.string() })
451
- ]);
452
- BridgePermissionRequest = z5.object({
453
- requestId: z5.string(),
454
- toolName: z5.string(),
455
- title: z5.string(),
456
- // câu mô tả sẵn, vd "Claude wants to run: pnpm test"
457
- input: z5.record(z5.string(), z5.unknown())
458
- });
459
- BridgeSessionInfo = z5.object({
460
- sessionId: z5.string(),
461
- title: z5.string(),
462
- updatedAt: z5.string()
463
- // ISO
464
- });
465
- BridgeServerMessage = z5.discriminatedUnion("type", [
466
- /** Gửi khi vừa kết nối và mỗi khi trạng thái đổi. */
467
- z5.object({
468
- type: z5.literal("state"),
469
- sessionId: z5.string().nullable(),
470
- busy: z5.boolean(),
471
- // đang có lượt chạy
472
- pendingPermissions: z5.array(BridgePermissionRequest),
473
- // Bridge bản cũ không gửi hai trường này (chưa đổi được chế độ / model từ app)
474
- permissionMode: BridgePermissionMode.optional(),
475
- model: BridgeModel.optional()
476
- }),
477
- /** Toàn bộ lịch sử (đã cắt bớt) của session hiện tại: khi kết nối và khi đổi session. */
478
- z5.object({
479
- type: z5.literal("history"),
480
- sessionId: z5.string().nullable(),
481
- items: z5.array(BridgeChatItem)
482
- }),
483
- z5.object({ type: z5.literal("sessions"), sessions: z5.array(BridgeSessionInfo) }),
484
- /** Dòng mới trong lượt đang chạy. */
485
- z5.object({ type: z5.literal("item"), sessionId: z5.string().nullable(), item: BridgeChatItem }),
486
- z5.object({ type: z5.literal("permission_request"), request: BridgePermissionRequest }),
487
- z5.object({
488
- type: z5.literal("permission_resolved"),
489
- requestId: z5.string(),
490
- allow: z5.boolean()
491
- }),
492
- z5.object({
493
- type: z5.literal("turn_end"),
494
- sessionId: z5.string().nullable(),
495
- ok: z5.boolean(),
496
- text: z5.string(),
497
- // câu trả lời cuối, hoặc lỗi khi ok = false
498
- costUsd: z5.number(),
499
- durationMs: z5.number()
500
- }),
501
- z5.object({
502
- type: z5.literal("error"),
503
- message: z5.string(),
504
- clientId: z5.string().optional()
505
- // lỗi do tin `send` có clientId này
506
- })
507
- ]);
508
- BRIDGE_KEY_PREFIX = "mobk_";
509
- RegisterBridgeKey = z5.object({ key: z5.string().regex(/^mobk_[A-Za-z0-9_-]{43}$/, "Kho\xE1 bridge kh\xF4ng h\u1EE3p l\u1EC7") }).strict();
510
- BridgePublicUrl = z5.string().max(500).regex(/^wss:\/\/[a-z0-9.-]+(:\d+)?\/[^\s?#]*$/i, "Ph\u1EA3i c\xF3 d\u1EA1ng wss://<host>/ws");
511
- PublishBridgeUrl = z5.object({ url: BridgePublicUrl.nullable(), ifUrl: BridgePublicUrl.nullable().optional() }).strict();
512
- BridgeEndpoint = z5.object({
513
- url: z5.string().nullable(),
514
- // null = bridge chưa gửi địa chỉ hoặc đã tắt
515
- updatedAt: z5.string().nullable(),
516
- hasKey: z5.boolean()
517
- // app đã đăng ký khoá bridge chưa
518
- });
519
- }
520
- });
521
-
522
- // src/announce.ts
523
- var RETRY_MIN_MS, RETRY_MAX_MS, UNREGISTERED_RETRY_MS, UrlAnnouncer;
524
- var init_announce = __esm({
525
- "src/announce.ts"() {
526
- "use strict";
527
- RETRY_MIN_MS = 2e3;
528
- RETRY_MAX_MS = 6e4;
529
- UNREGISTERED_RETRY_MS = 1e4;
530
- UrlAnnouncer = class {
531
- constructor(opts) {
532
- this.opts = opts;
533
- this.fetchFn = opts.fetchFn ?? fetch;
534
- this.sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms).unref()));
535
- }
536
- /** Địa chỉ còn chờ gửi; `undefined` = không có gì chờ. */
537
- want = void 0;
538
- /** Địa chỉ server đang giữ do bridge này gửi; `undefined` = chưa gửi được lần nào. */
539
- published = void 0;
540
- replaced = false;
541
- running = false;
542
- /** Đã báo khoá chưa đăng ký, để không in lặp mỗi lần thử. */
543
- warned = false;
544
- fetchFn;
545
- sleep;
546
- /** Gửi địa chỉ mới (`null` = bridge tắt). Lỗi mạng thì tự thử lại, địa chỉ mới nhất thắng. */
547
- announce(url) {
548
- if (this.replaced) return Promise.resolve();
549
- this.want = url;
550
- return this.run();
551
- }
552
- /** Gọi định kỳ: hỏi server còn giữ địa chỉ của bridge này không, để biết sớm khi bị máy khác thay. */
553
- async check() {
554
- if (this.running || this.replaced || typeof this.published !== "string") return;
555
- if (await this.send(this.published, this.published) === "replaced") this.markReplaced();
556
- }
557
- /** Lúc tắt bridge: báo server bridge đã tắt, không chờ quá `timeoutMs`. */
558
- async clear(timeoutMs = 2e3) {
559
- this.want = void 0;
560
- if (this.replaced || !this.published) return;
561
- await this.send(null, this.published, AbortSignal.timeout(timeoutMs));
562
- }
563
- markReplaced() {
564
- if (this.replaced) return;
565
- this.replaced = true;
566
- this.want = void 0;
567
- console.log("Bridge \u1EDF m\xE1y kh\xE1c v\u1EEBa b\u1EADt v\xE0 \u0111\xE3 nh\u1EADn k\u1EBFt n\u1ED1i, bridge n\xE0y t\u1EF1 t\u1EAFt.");
568
- this.opts.onReplaced?.();
569
- }
570
- async run() {
571
- if (this.running) return;
572
- this.running = true;
573
- let delay = RETRY_MIN_MS;
574
- try {
575
- while (this.want !== void 0) {
576
- const url = this.want;
577
- if (url === null && this.published === void 0) {
578
- this.want = void 0;
579
- break;
580
- }
581
- const result = await this.send(url, this.published);
582
- if (result === "replaced") {
583
- this.markReplaced();
584
- break;
585
- }
586
- if (result === "unauthorized") {
587
- if (!this.warned) {
588
- this.warned = true;
589
- console.log(
590
- "Server ch\u01B0a nh\u1EADn kho\xE1 c\u1EE7a bridge: m\u1EDF web ho\u1EB7c app \u2192 Claude PC \u2192 C\xE0i \u0111\u1EB7t k\u1EBFt n\u1ED1i, d\xE1n BRIDGE_TOKEN, \u0111\u1EC3 tr\u1ED1ng URL r\u1ED3i L\u01B0u. Bridge t\u1EF1 th\u1EED l\u1EA1i."
591
- );
592
- }
593
- await this.sleep(UNREGISTERED_RETRY_MS);
594
- continue;
595
- }
596
- if (result === "ok") {
597
- this.published = url;
598
- if (this.want === url) this.want = void 0;
599
- delay = RETRY_MIN_MS;
600
- this.warned = false;
601
- if (url) console.log("\u0110\xE3 g\u1EEDi \u0111\u1ECBa ch\u1EC9 l\xEAn server, app s\u1EBD t\u1EF1 k\u1EBFt n\u1ED1i.");
602
- continue;
603
- }
604
- await this.sleep(delay);
605
- delay = Math.min(RETRY_MAX_MS, delay * 2);
606
- }
607
- } finally {
608
- this.running = false;
609
- }
610
- }
611
- /** `ifUrl` khác `undefined` thì server chỉ ghi khi địa chỉ đang lưu đúng bằng nó. */
612
- async send(url, ifUrl, signal) {
613
- try {
614
- const res = await this.fetchFn(`${this.opts.apiUrl}/v1/bridge/url`, {
615
- method: "PUT",
616
- headers: {
617
- Authorization: `Bearer ${this.opts.key}`,
618
- "Content-Type": "application/json"
619
- },
620
- body: JSON.stringify(ifUrl === void 0 ? { url } : { url, ifUrl }),
621
- signal
622
- });
623
- if (res.ok) return "ok";
624
- if (res.status === 401) return "unauthorized";
625
- if (res.status === 409) return "replaced";
626
- console.error(`G\u1EEDi \u0111\u1ECBa ch\u1EC9 l\xEAn server l\u1ED7i HTTP ${res.status}, th\u1EED l\u1EA1i sau`);
627
- return "retry";
628
- } catch {
629
- return "retry";
630
- }
631
- }
632
- };
633
- }
634
- });
635
-
636
350
  // ../../node_modules/.pnpm/rrule@2.8.1/node_modules/rrule/dist/es5/rrule.js
637
351
  var require_rrule = __commonJS({
638
352
  "../../node_modules/.pnpm/rrule@2.8.1/node_modules/rrule/dist/es5/rrule.js"(exports, module) {
@@ -722,8 +436,8 @@ var require_rrule = __commonJS({
722
436
  this.weekday = weekday;
723
437
  this.n = n;
724
438
  }
725
- Weekday2.fromStr = function(str) {
726
- return new Weekday2(ALL_WEEKDAYS.indexOf(str));
439
+ Weekday2.fromStr = function(str2) {
440
+ return new Weekday2(ALL_WEEKDAYS.indexOf(str2));
727
441
  };
728
442
  Weekday2.prototype.nth = function(n) {
729
443
  return this.n === n ? this : new Weekday2(this.weekday, n);
@@ -792,20 +506,20 @@ var require_rrule = __commonJS({
792
506
  if (padString === void 0) {
793
507
  padString = " ";
794
508
  }
795
- var str = String(item);
509
+ var str2 = String(item);
796
510
  targetLength = targetLength >> 0;
797
- if (str.length > targetLength) {
798
- return String(str);
511
+ if (str2.length > targetLength) {
512
+ return String(str2);
799
513
  }
800
- targetLength = targetLength - str.length;
514
+ targetLength = targetLength - str2.length;
801
515
  if (targetLength > padString.length) {
802
516
  padString += repeat(padString, targetLength / padString.length);
803
517
  }
804
- return padString.slice(0, targetLength) + String(str);
518
+ return padString.slice(0, targetLength) + String(str2);
805
519
  }
806
- var split = function(str, sep, num) {
807
- var splits = str.split(sep);
808
- return num ? splits.slice(0, num).concat([splits.slice(num).join(sep)]) : splits;
520
+ var split = function(str2, sep, num3) {
521
+ var splits = str2.split(sep);
522
+ return num3 ? splits.slice(0, num3).concat([splits.slice(num3).join(sep)]) : splits;
809
523
  };
810
524
  var pymod = function(a, b) {
811
525
  var r = a % b;
@@ -2545,9 +2259,9 @@ var require_rrule = __commonJS({
2545
2259
  case "BYHOUR":
2546
2260
  case "BYMINUTE":
2547
2261
  case "BYSECOND":
2548
- var num = parseNumber(value);
2262
+ var num3 = parseNumber(value);
2549
2263
  var optionKey = key.toLowerCase();
2550
- options[optionKey] = num;
2264
+ options[optionKey] = num3;
2551
2265
  break;
2552
2266
  case "BYWEEKDAY":
2553
2267
  case "BYDAY":
@@ -3359,8 +3073,8 @@ var require_rrule = __commonJS({
3359
3073
  RRule3.fromText = function(text, language) {
3360
3074
  return fromText(text, language);
3361
3075
  };
3362
- RRule3.fromString = function(str) {
3363
- return new RRule3(RRule3.parseString(str) || void 0);
3076
+ RRule3.fromString = function(str2) {
3077
+ return new RRule3(RRule3.parseString(str2) || void 0);
3364
3078
  };
3365
3079
  RRule3.prototype._iter = function(iterResult) {
3366
3080
  return iter(iterResult, this.options);
@@ -3864,6 +3578,13 @@ var require_rrule = __commonJS({
3864
3578
  }
3865
3579
  });
3866
3580
 
3581
+ // ../../packages/shared/src/lunar.ts
3582
+ var init_lunar = __esm({
3583
+ "../../packages/shared/src/lunar.ts"() {
3584
+ "use strict";
3585
+ }
3586
+ });
3587
+
3867
3588
  // ../../packages/shared/src/time.ts
3868
3589
  var init_time = __esm({
3869
3590
  "../../packages/shared/src/time.ts"() {
@@ -3889,34 +3610,55 @@ function parseUntil(value) {
3889
3610
  }
3890
3611
  return { date, dateOnly: m[4] === void 0 };
3891
3612
  }
3892
- function normalizeRRule(input) {
3893
- const body = input.trim().toUpperCase().replace(/^RRULE:/, "");
3894
- const seen = /* @__PURE__ */ new Set();
3613
+ function parseParts(body, rules, lunar) {
3614
+ const parts = /* @__PURE__ */ new Map();
3895
3615
  for (const part of body.split(";")) {
3896
3616
  const [key, value, ...rest] = part.split("=");
3897
3617
  if (!key || value === void 0 || rest.length > 0) {
3898
3618
  throw new RRuleError(`"${part}" kh\xF4ng \u0111\xFAng d\u1EA1ng KEY=VALUE`);
3899
3619
  }
3900
- const rule = RULES[key];
3901
- if (!rule) throw new RRuleError(`Kh\xF4ng h\u1ED7 tr\u1EE3 ${key}`);
3902
- if (seen.has(key)) throw new RRuleError(`${key} xu\u1EA5t hi\u1EC7n nhi\u1EC1u l\u1EA7n`);
3903
- if (!rule.test(value)) throw new RRuleError(`${key}=${value} kh\xF4ng h\u1EE3p l\u1EC7, c\u1EA7n ${rule.hint}`);
3904
- seen.add(key);
3620
+ const rule = rules[key];
3621
+ if (!rule) {
3622
+ throw new RRuleError(lunar ? `L\u1ECBch \xE2m kh\xF4ng h\u1ED7 tr\u1EE3 ${key}` : `Kh\xF4ng h\u1ED7 tr\u1EE3 ${key}`);
3623
+ }
3624
+ if (parts.has(key)) throw new RRuleError(`${key} xu\u1EA5t hi\u1EC7n nhi\u1EC1u l\u1EA7n`);
3625
+ if (!rule.test(value)) {
3626
+ throw new RRuleError(
3627
+ `${key}=${value} kh\xF4ng h\u1EE3p l\u1EC7${lunar ? " (l\u1ECBch \xE2m)" : ""}, c\u1EA7n ${rule.hint}`
3628
+ );
3629
+ }
3630
+ parts.set(key, value);
3905
3631
  }
3906
- if (!seen.has("FREQ")) throw new RRuleError("Thi\u1EBFu FREQ");
3907
- if (seen.has("COUNT") && seen.has("UNTIL")) {
3632
+ if (!parts.has("FREQ")) throw new RRuleError("Thi\u1EBFu FREQ");
3633
+ if (parts.has("COUNT") && parts.has("UNTIL")) {
3908
3634
  throw new RRuleError("Kh\xF4ng d\xF9ng COUNT v\xE0 UNTIL c\xF9ng l\xFAc");
3909
3635
  }
3636
+ if (lunar && parts.has("BYMONTH") && parts.get("FREQ") !== "YEARLY") {
3637
+ throw new RRuleError("L\u1ECBch \xE2m: BYMONTH ch\u1EC9 d\xF9ng v\u1EDBi FREQ=YEARLY");
3638
+ }
3639
+ return parts;
3640
+ }
3641
+ function normalizeRRule(input) {
3642
+ const text = input.trim().toUpperCase();
3643
+ if (text.startsWith(LUNAR_PREFIX)) {
3644
+ const body2 = text.slice(LUNAR_PREFIX.length).replace(/^RRULE:/, "");
3645
+ parseParts(body2, LUNAR_RULES, true);
3646
+ return LUNAR_PREFIX + body2;
3647
+ }
3648
+ const body = text.replace(/^RRULE:/, "");
3649
+ parseParts(body, RULES, false);
3910
3650
  return body;
3911
3651
  }
3912
- var import_rrule, RRuleError, WEEKDAY, BYDAY_ITEM, RULES;
3652
+ var import_rrule, RRuleError, LUNAR_PREFIX, WEEKDAY, BYDAY_ITEM, RULES, LUNAR_RULES;
3913
3653
  var init_recurrence = __esm({
3914
3654
  "../../packages/shared/src/recurrence.ts"() {
3915
3655
  "use strict";
3916
3656
  import_rrule = __toESM(require_rrule(), 1);
3657
+ init_lunar();
3917
3658
  init_time();
3918
3659
  RRuleError = class extends Error {
3919
3660
  };
3661
+ LUNAR_PREFIX = "LUNAR:";
3920
3662
  WEEKDAY = "(?:MO|TU|WE|TH|FR|SA|SU)";
3921
3663
  BYDAY_ITEM = new RegExp(`^(?:[+-]?(?:[1-9]|[1-4]\\d|5[0-3]))?${WEEKDAY}$`);
3922
3664
  RULES = {
@@ -3938,12 +3680,22 @@ var init_recurrence = __esm({
3938
3680
  BYSETPOS: { test: (v) => intList(v, 1, 366, true), hint: "1..366 ho\u1EB7c -366..-1" },
3939
3681
  WKST: { test: (v) => new RegExp(`^${WEEKDAY}$`).test(v), hint: "MO..SU" }
3940
3682
  };
3683
+ LUNAR_RULES = {
3684
+ FREQ: { test: (v) => /^(?:YEARLY|MONTHLY)$/.test(v), hint: "MONTHLY ho\u1EB7c YEARLY" },
3685
+ INTERVAL: RULES.INTERVAL,
3686
+ COUNT: RULES.COUNT,
3687
+ UNTIL: { test: RULES.UNTIL.test, hint: "ng\xE0y d\u01B0\u01A1ng YYYYMMDD ho\u1EB7c YYYYMMDDTHHMMSSZ" },
3688
+ BYMONTH: RULES.BYMONTH,
3689
+ BYMONTHDAY: { test: (v) => intList(v, 1, 30), hint: "ng\xE0y \xE2m 1..30" },
3690
+ BYHOUR: RULES.BYHOUR,
3691
+ BYMINUTE: RULES.BYMINUTE
3692
+ };
3941
3693
  }
3942
3694
  });
3943
3695
 
3944
3696
  // ../../packages/shared/src/schemas/reminders.ts
3945
- import { z as z6 } from "zod";
3946
- var MAX_SNOOZE_MIN, RRuleString, Reminder, ReminderFields, targetConsistent, CreateReminder, UpdateReminder, SnoozeReminder, ListRemindersQuery;
3697
+ import { z as z4 } from "zod";
3698
+ var MAX_SNOOZE_MIN, MAX_REMINDER_OFFSET_MIN, MAX_CONDITION_LENGTH, ReminderOffsetMin, RRuleString, Reminder, ReminderFields, targetConsistent, CreateReminder, UpdateReminder, SnoozeReminder, ListRemindersQuery;
3947
3699
  var init_reminders = __esm({
3948
3700
  "../../packages/shared/src/schemas/reminders.ts"() {
3949
3701
  "use strict";
@@ -3951,8 +3703,11 @@ var init_reminders = __esm({
3951
3703
  init_recurrence();
3952
3704
  init_common();
3953
3705
  MAX_SNOOZE_MIN = 24 * 60;
3954
- RRuleString = z6.string().max(500).overwrite(
3955
- (s) => s.trim().toUpperCase().replace(/^RRULE:/, "")
3706
+ MAX_REMINDER_OFFSET_MIN = 30 * 24 * 60;
3707
+ MAX_CONDITION_LENGTH = 500;
3708
+ ReminderOffsetMin = z4.number().int().min(0).max(MAX_REMINDER_OFFSET_MIN);
3709
+ RRuleString = z4.string().max(500).overwrite(
3710
+ (s) => s.trim().toUpperCase().replace(/^(LUNAR:)?RRULE:/, "$1")
3956
3711
  ).superRefine((s, ctx) => {
3957
3712
  try {
3958
3713
  normalizeRRule(s);
@@ -3961,43 +3716,61 @@ var init_reminders = __esm({
3961
3716
  ctx.addIssue({ code: "custom", message: e.message });
3962
3717
  }
3963
3718
  });
3964
- Reminder = z6.object({
3965
- id: z6.string(),
3966
- title: z6.string(),
3967
- body: z6.string().nullable(),
3719
+ Reminder = z4.object({
3720
+ id: z4.string(),
3721
+ title: z4.string(),
3722
+ body: z4.string().nullable(),
3968
3723
  targetType: TargetType,
3969
- targetId: z6.string().nullable(),
3970
- fireAt: z6.string(),
3724
+ targetId: z4.string().nullable(),
3725
+ fireAt: z4.string(),
3971
3726
  // lần nhắc kế tiếp
3972
- rrule: z6.string().nullable(),
3727
+ rrule: z4.string().nullable(),
3728
+ offsetMin: z4.number().int().nullable(),
3729
+ // có giá trị = nhắc tương đối, fireAt tự tính theo item
3730
+ // Có giá trị = trợ lý tự kiểm tra: đến giờ Gemini đọc dữ liệu, chỉ nhắc khi điều kiện đúng
3731
+ condition: z4.string().nullable(),
3973
3732
  status: ReminderStatus,
3974
3733
  source: Source,
3975
- createdAt: z6.string(),
3976
- updatedAt: z6.string()
3734
+ createdAt: z4.string(),
3735
+ updatedAt: z4.string()
3977
3736
  });
3978
- ReminderFields = z6.object({
3979
- title: z6.string().trim().min(1).max(200),
3980
- body: z6.string().max(2e3).nullable(),
3737
+ ReminderFields = z4.object({
3738
+ title: z4.string().trim().min(1).max(200),
3739
+ body: z4.string().max(2e3).nullable(),
3981
3740
  targetType: TargetType,
3982
3741
  targetId: Id.nullable(),
3983
3742
  fireAt: IsoDateTime,
3984
- rrule: RRuleString.nullable()
3743
+ rrule: RRuleString.nullable(),
3744
+ offsetMin: ReminderOffsetMin.nullable(),
3745
+ condition: z4.string().trim().min(1).max(MAX_CONDITION_LENGTH).nullable()
3985
3746
  });
3986
3747
  targetConsistent = (v) => (v.targetType ?? "none") === "none" ? v.targetId == null : v.targetId != null;
3987
3748
  CreateReminder = ReminderFields.extend({
3988
3749
  body: ReminderFields.shape.body.optional(),
3989
3750
  targetType: TargetType.default("none"),
3990
3751
  targetId: ReminderFields.shape.targetId.optional(),
3991
- rrule: ReminderFields.shape.rrule.optional()
3752
+ fireAt: ReminderFields.shape.fireAt.optional(),
3753
+ rrule: ReminderFields.shape.rrule.optional(),
3754
+ offsetMin: ReminderFields.shape.offsetMin.optional(),
3755
+ condition: ReminderFields.shape.condition.optional()
3992
3756
  }).strict().refine(targetConsistent, {
3993
3757
  message: "targetId b\u1EAFt bu\u1ED9c khi c\xF3 targetType, v\xE0 ph\u1EA3i b\u1ECF tr\u1ED1ng khi targetType l\xE0 none",
3994
3758
  path: ["targetId"]
3759
+ }).refine((v) => v.offsetMin != null || v.fireAt != null, {
3760
+ message: "C\u1EA7n fireAt (gi\u1EDD c\u1ED1 \u0111\u1ECBnh) ho\u1EB7c offsetMin (nh\u1EAFc tr\u01B0\u1EDBc gi\u1EDD c\u1EE7a item)",
3761
+ path: ["fireAt"]
3762
+ }).refine((v) => v.offsetMin == null || v.targetType !== "none", {
3763
+ message: "Nh\u1EAFc t\u01B0\u01A1ng \u0111\u1ED1i (offsetMin) ph\u1EA3i g\u1EAFn v\u1EDBi m\u1ED9t item",
3764
+ path: ["offsetMin"]
3765
+ }).refine((v) => v.condition == null || v.offsetMin == null, {
3766
+ message: "Nh\u1EAFc c\xF3 \u0111i\u1EC1u ki\u1EC7n d\xF9ng fireAt (+ rrule), kh\xF4ng d\xF9ng offsetMin",
3767
+ path: ["condition"]
3995
3768
  });
3996
3769
  UpdateReminder = ReminderFields.extend({
3997
3770
  // fired / snoozed do hệ thống đặt, người dùng chỉ bật lại, đánh dấu xong hoặc huỷ
3998
- status: z6.enum(["scheduled", "done", "cancelled"])
3771
+ status: z4.enum(["scheduled", "done", "cancelled"])
3999
3772
  }).partial().strict();
4000
- SnoozeReminder = z6.object({ minutes: z6.number().int().min(1).max(MAX_SNOOZE_MIN) }).strict();
3773
+ SnoozeReminder = z4.object({ minutes: z4.number().int().min(1).max(MAX_SNOOZE_MIN) }).strict();
4001
3774
  ListRemindersQuery = PageQuery.extend({
4002
3775
  from: IsoDateTime.optional(),
4003
3776
  // fireAt >= from
@@ -4010,22 +3783,635 @@ var init_reminders = __esm({
4010
3783
  }
4011
3784
  });
4012
3785
 
4013
- // ../../packages/shared/src/push.ts
4014
- var AGENT_PERMISSION_CATEGORY;
4015
- var init_push = __esm({
4016
- "../../packages/shared/src/push.ts"() {
3786
+ // ../../packages/shared/src/schemas/tasks.ts
3787
+ import { z as z5 } from "zod";
3788
+ var MAX_TASKS_PER_CREATE, Task, TaskTree, TaskFields, RepeatFields, CreateSubtask, CreateTask, UpdateTask, UpdateTaskResult, ListTasksQuery, PostponedTask, MAX_POSTPONE, PostponeTasks, PostponeTasksResult, UndoPostpone, ReorderTasks;
3789
+ var init_tasks = __esm({
3790
+ "../../packages/shared/src/schemas/tasks.ts"() {
4017
3791
  "use strict";
3792
+ init_enums();
3793
+ init_common();
4018
3794
  init_reminders();
4019
- AGENT_PERMISSION_CATEGORY = "agent_permission";
4020
- }
3795
+ MAX_TASKS_PER_CREATE = 100;
3796
+ Task = z5.object({
3797
+ id: z5.string(),
3798
+ parentId: z5.string().nullable(),
3799
+ title: z5.string(),
3800
+ description: z5.string().nullable(),
3801
+ status: TaskStatus,
3802
+ priority: TaskPriority,
3803
+ dueAt: z5.string().nullable(),
3804
+ allDay: z5.boolean(),
3805
+ estimateMin: z5.number().int().nullable(),
3806
+ sortOrder: z5.number().int(),
3807
+ completedAt: z5.string().nullable(),
3808
+ areaId: z5.string().nullable(),
3809
+ // subtask luôn cùng mảng với task gốc
3810
+ rrule: z5.string().nullable(),
3811
+ // chỉ ở task gốc đang mở của chuỗi lặp
3812
+ repeatFrom: RepeatFrom.nullable(),
3813
+ seriesId: z5.string().nullable(),
3814
+ // id task đầu chuỗi, lần đã xong vẫn giữ để xem lịch sử
3815
+ scheduledAt: z5.string().nullable(),
3816
+ // giờ định làm, khác hạn dueAt
3817
+ // Mục tiêu (docs/10 §1.2): task gốc có goalId là mốc, subtask là việc
3818
+ goalId: z5.string().nullable(),
3819
+ optional: z5.boolean(),
3820
+ // "nên có", thiếu giờ thì cắt trước
3821
+ deferred: z5.boolean(),
3822
+ // việc nên có đã dồn ra sau hạn, không xếp lịch
3823
+ plannedBy: PlannedBy.nullable(),
3824
+ slipCount: z5.number().int(),
3825
+ source: Source,
3826
+ createdAt: z5.string(),
3827
+ updatedAt: z5.string(),
3828
+ childrenCount: z5.number().int()
3829
+ });
3830
+ TaskTree = Task.extend({
3831
+ get children() {
3832
+ return z5.array(TaskTree);
3833
+ }
3834
+ });
3835
+ TaskFields = z5.object({
3836
+ title: z5.string().trim().min(1).max(300),
3837
+ description: z5.string().max(1e4).nullable(),
3838
+ status: TaskStatus,
3839
+ priority: TaskPriority,
3840
+ dueAt: IsoDateTime.nullable(),
3841
+ allDay: z5.boolean(),
3842
+ estimateMin: z5.number().int().min(1).max(1e5).nullable(),
3843
+ scheduledAt: IsoDateTime.nullable(),
3844
+ optional: z5.boolean()
3845
+ });
3846
+ RepeatFields = z5.object({
3847
+ rrule: RRuleString.nullable(),
3848
+ repeatFrom: RepeatFrom
3849
+ });
3850
+ CreateSubtask = TaskFields.partial().extend({
3851
+ title: TaskFields.shape.title,
3852
+ get subtasks() {
3853
+ return z5.array(CreateSubtask).max(MAX_TASKS_PER_CREATE).optional();
3854
+ }
3855
+ }).strict();
3856
+ CreateTask = TaskFields.partial().extend(RepeatFields.partial().shape).extend({
3857
+ title: TaskFields.shape.title,
3858
+ parentId: Id.nullable().optional(),
3859
+ // Như mảng: subtask theo mục tiêu của task gốc
3860
+ goalId: Id.nullable().optional(),
3861
+ // Subtask lấy mảng của task gốc; gửi khác mảng gốc thì 400 area_follows_root
3862
+ areaId: Id.nullable().optional(),
3863
+ subtasks: z5.array(CreateSubtask).max(MAX_TASKS_PER_CREATE).optional()
3864
+ }).strict();
3865
+ UpdateTask = TaskFields.extend(RepeatFields.shape).extend({
3866
+ parentId: Id.nullable(),
3867
+ // chuyển sang cha khác, null = thành task gốc
3868
+ areaId: Id.nullable(),
3869
+ // đổi ở task gốc thì cả cây đổi theo
3870
+ goalId: Id.nullable(),
3871
+ // như areaId: chỉ đổi ở task gốc
3872
+ deferred: z5.boolean(),
3873
+ sortOrder: z5.number().int()
3874
+ }).partial().strict();
3875
+ UpdateTaskResult = Task.extend({ nextTask: Task.nullable() });
3876
+ ListTasksQuery = PageQuery.extend({
3877
+ status: queryList(TaskStatus).optional(),
3878
+ dueFrom: IsoDateTime.optional(),
3879
+ dueTo: IsoDateTime.optional(),
3880
+ parentId: Id.optional(),
3881
+ q: z5.string().trim().max(200).optional(),
3882
+ areaId: queryList(Id).optional(),
3883
+ // "a,b", "none" = chưa xếp mảng
3884
+ seriesId: Id.optional()
3885
+ // lịch sử một chuỗi lặp (mọi trạng thái, mới nhất trước)
3886
+ });
3887
+ PostponedTask = z5.object({ id: Id, dueAt: IsoDateTime, scheduledAt: IsoDateTime.nullable() }).strict();
3888
+ MAX_POSTPONE = 50;
3889
+ PostponeTasks = z5.object({
3890
+ ids: z5.array(Id).min(1).max(MAX_POSTPONE).optional(),
3891
+ noticeId: Id.optional(),
3892
+ to: z5.literal("tomorrow")
3893
+ }).strict().refine((v) => v.ids || v.noticeId, { message: "C\u1EA7n ids ho\u1EB7c noticeId", path: ["ids"] });
3894
+ PostponeTasksResult = z5.object({
3895
+ postponed: z5.number().int(),
3896
+ before: z5.array(PostponedTask)
3897
+ });
3898
+ UndoPostpone = z5.object({ before: z5.array(PostponedTask).min(1).max(MAX_POSTPONE), noticeId: Id.optional() }).strict();
3899
+ ReorderTasks = z5.array(z5.object({ id: Id, sortOrder: z5.number().int(), parentId: Id.nullable() }).strict()).min(1).max(500);
3900
+ }
3901
+ });
3902
+
3903
+ // ../../packages/shared/src/schemas/notify.ts
3904
+ import { z as z6 } from "zod";
3905
+ var ExpoToken, WebPushSubscription, WebToken, RegisterDevice, Device, Notice, BriefingSuggestion, BriefingData, ListNoticesQuery, Connector, ReadNotices;
3906
+ var init_notify = __esm({
3907
+ "../../packages/shared/src/schemas/notify.ts"() {
3908
+ "use strict";
3909
+ init_enums();
3910
+ init_common();
3911
+ init_tasks();
3912
+ ExpoToken = z6.string().regex(/^Expo(nent)?PushToken\[[^\]]+\]$/, "Kh\xF4ng ph\u1EA3i Expo push token");
3913
+ WebPushSubscription = z6.object({
3914
+ endpoint: z6.url({ protocol: /^https$/ }).max(1e3),
3915
+ keys: z6.object({
3916
+ p256dh: z6.string().min(80).max(100),
3917
+ // khoá công khai P-256 dạng base64url (65 byte)
3918
+ auth: z6.string().min(16).max(30)
3919
+ // 16 byte base64url
3920
+ })
3921
+ });
3922
+ WebToken = z6.union([z6.string().max(2e3), z6.record(z6.string(), z6.unknown())]).transform((v, ctx) => {
3923
+ let raw = v;
3924
+ if (typeof v === "string") {
3925
+ try {
3926
+ raw = JSON.parse(v);
3927
+ } catch {
3928
+ ctx.addIssue({ code: "custom", message: "pushToken kh\xF4ng ph\u1EA3i JSON" });
3929
+ return z6.NEVER;
3930
+ }
3931
+ }
3932
+ const sub = WebPushSubscription.safeParse(raw);
3933
+ if (!sub.success) {
3934
+ ctx.addIssue({ code: "custom", message: "Subscription Web Push kh\xF4ng h\u1EE3p l\u1EC7" });
3935
+ return z6.NEVER;
3936
+ }
3937
+ const { endpoint, keys } = sub.data;
3938
+ return JSON.stringify({ endpoint, keys: { p256dh: keys.p256dh, auth: keys.auth } });
3939
+ });
3940
+ RegisterDevice = z6.discriminatedUnion("platform", [
3941
+ z6.object({ platform: z6.literal("android"), pushToken: ExpoToken }).strict(),
3942
+ z6.object({ platform: z6.literal("web"), pushToken: WebToken }).strict()
3943
+ ]);
3944
+ Device = z6.object({
3945
+ id: z6.string(),
3946
+ platform: DevicePlatform,
3947
+ lastSeen: z6.string()
3948
+ });
3949
+ Notice = z6.object({
3950
+ id: z6.string(),
3951
+ kind: NoticeKind,
3952
+ title: z6.string(),
3953
+ body: z6.string(),
3954
+ data: z6.record(z6.string(), z6.unknown()).nullable(),
3955
+ // vd {budgetId, month} cho nút "Xem chi tiết"
3956
+ createdAt: z6.string(),
3957
+ readAt: z6.string().nullable()
3958
+ });
3959
+ BriefingSuggestion = z6.object({
3960
+ text: z6.string().max(200),
3961
+ taskId: z6.string().max(64).optional(),
3962
+ slotStart: z6.iso.datetime({ offset: true }).optional()
3963
+ });
3964
+ BriefingData = z6.object({
3965
+ taskIds: z6.array(z6.string()).default([]),
3966
+ suggestions: z6.array(BriefingSuggestion).default([]),
3967
+ postponed: z6.object({ count: z6.number().int(), before: z6.array(PostponedTask) }).nullable().default(null)
3968
+ });
3969
+ ListNoticesQuery = PageQuery.extend({ unread: QueryBool.optional() });
3970
+ Connector = z6.object({
3971
+ clientId: z6.string(),
3972
+ name: z6.string(),
3973
+ uri: z6.string().nullable(),
3974
+ icon: z6.string().nullable(),
3975
+ scopes: z6.array(z6.string()),
3976
+ connectedAt: z6.string(),
3977
+ lastUsedAt: z6.string().nullable()
3978
+ // lần cấp access token gần nhất (khoảng mỗi giờ khi đang dùng)
3979
+ });
3980
+ ReadNotices = z6.object({ ids: z6.array(z6.string()).min(1).max(200).optional() }).strict();
3981
+ }
3982
+ });
3983
+
3984
+ // ../../packages/shared/src/schemas/bridge.ts
3985
+ import { z as z7 } from "zod";
3986
+ function deriveBridgeKey(token) {
3987
+ const digest2 = sha256(new TextEncoder().encode(`manualofme-bridge-url:${token}`));
3988
+ return `${BRIDGE_KEY_PREFIX}${base64url(digest2)}`;
3989
+ }
3990
+ var BRIDGE_SUBPROTOCOL, BRIDGE_CLOSE_UNAUTHORIZED, BRIDGE_PERMISSION_MODES, BridgePermissionMode, BridgeModel, BRIDGE_MAX_ATTACHMENTS, BridgeAttachment, BRIDGE_MAX_WINDOWS, BridgeClientMessage, BridgeFileRef, BridgeToolDetail, BridgeChatItem, BridgePermissionRequest, BridgeSessionInfo, BridgeWindow, BridgeServerMessage, BRIDGE_KEY_PREFIX, RegisterBridgeKey, BridgePublicUrl, BridgeInstance, PublishBridgeUrl, BridgeEndpointInstance, BridgeEndpoint;
3991
+ var init_bridge = __esm({
3992
+ "../../packages/shared/src/schemas/bridge.ts"() {
3993
+ "use strict";
3994
+ init_sha256();
3995
+ init_notify();
3996
+ BRIDGE_SUBPROTOCOL = "manualofme-bridge";
3997
+ BRIDGE_CLOSE_UNAUTHORIZED = 4401;
3998
+ BRIDGE_PERMISSION_MODES = [
3999
+ "default",
4000
+ "acceptEdits",
4001
+ "plan",
4002
+ "bypassPermissions"
4003
+ ];
4004
+ BridgePermissionMode = z7.enum(BRIDGE_PERMISSION_MODES);
4005
+ BridgeModel = z7.string().max(100).regex(/^[A-Za-z0-9._[\]-]+$/, "T\xEAn model kh\xF4ng h\u1EE3p l\u1EC7").nullable();
4006
+ BRIDGE_MAX_ATTACHMENTS = 4;
4007
+ BridgeAttachment = z7.object({
4008
+ name: z7.string().trim().min(1).max(200),
4009
+ mimeType: z7.string().max(200),
4010
+ data: z7.string().regex(/^[A-Za-z0-9+/]*={0,2}$/, "data ph\u1EA3i l\xE0 base64")
4011
+ }).strict();
4012
+ BRIDGE_MAX_WINDOWS = 10;
4013
+ BridgeClientMessage = z7.discriminatedUnion("type", [
4014
+ /** Liệt kê các session của project (cả session mở từ VS Code). */
4015
+ z7.object({ type: z7.literal("list_sessions") }).strict(),
4016
+ /** App bản cũ: đổi cửa sổ hiện tại sang session khác. `null` = session mới. */
4017
+ z7.object({ type: z7.literal("open_session"), sessionId: z7.uuid().nullable() }).strict(),
4018
+ /**
4019
+ * Mở cửa sổ cho một session (đã mở rồi thì thôi). `new: true` = cửa sổ trống, `sessionId` do app
4020
+ * tự sinh, thành id của session Claude khi gửi tin đầu tiên.
4021
+ */
4022
+ z7.object({ type: z7.literal("open_window"), sessionId: z7.uuid(), new: z7.boolean().optional() }).strict(),
4023
+ /** Đóng cửa sổ (session vẫn còn, mở lại từ Lịch sử). Đang chạy thì không đóng được. */
4024
+ z7.object({ type: z7.literal("close_window"), sessionId: z7.uuid() }).strict(),
4025
+ /** Lấy lịch sử một session, bridge trả `history` chỉ cho app hỏi. */
4026
+ z7.object({ type: z7.literal("get_history"), sessionId: z7.uuid() }).strict(),
4027
+ z7.object({
4028
+ type: z7.literal("send"),
4029
+ sessionId: z7.uuid().optional(),
4030
+ /** Rỗng được khi có tệp đính kèm. */
4031
+ text: z7.string().trim().max(2e4),
4032
+ clientId: z7.string().max(64).optional(),
4033
+ // bridge trả lại trong item user để app khử trùng
4034
+ attachments: z7.array(BridgeAttachment).max(BRIDGE_MAX_ATTACHMENTS).optional()
4035
+ }).strict(),
4036
+ /**
4037
+ * Đổi chế độ duyệt / model, áp cho mọi app và nhớ qua lần khởi động lại. Đổi chế độ khi đang
4038
+ * chạy thì các yêu cầu duyệt mà chế độ mới tự cho phép được duyệt luôn; model áp từ lượt sau.
4039
+ */
4040
+ z7.object({
4041
+ type: z7.literal("set_options"),
4042
+ permissionMode: BridgePermissionMode.optional(),
4043
+ model: BridgeModel.optional()
4044
+ }).strict(),
4045
+ /** Dừng lượt đang chạy. */
4046
+ z7.object({ type: z7.literal("interrupt"), sessionId: z7.uuid().optional() }).strict(),
4047
+ z7.object({
4048
+ type: z7.literal("permission_reply"),
4049
+ requestId: z7.string().min(1).max(200),
4050
+ allow: z7.boolean(),
4051
+ message: z7.string().max(1e3).optional()
4052
+ // lý do từ chối, gửi lại cho Claude
4053
+ }).strict(),
4054
+ /** Đăng ký Expo push token để nhận thông báo khi xong việc / cần duyệt. */
4055
+ z7.object({ type: z7.literal("register_push"), token: ExpoToken }).strict(),
4056
+ z7.object({ type: z7.literal("unregister_push"), token: ExpoToken }).strict(),
4057
+ /** Lấy ảnh theo `ref` trong item (`user.files`, `image`). Bridge trả `image_data`. */
4058
+ z7.object({ type: z7.literal("get_image"), ref: z7.string().min(1).max(300) }).strict()
4059
+ ]);
4060
+ BridgeFileRef = z7.object({
4061
+ ref: z7.string().max(300),
4062
+ name: z7.string().max(200),
4063
+ mimeType: z7.string().max(200)
4064
+ });
4065
+ BridgeToolDetail = z7.object({
4066
+ edits: z7.array(z7.object({ old: z7.string(), new: z7.string() })).optional(),
4067
+ content: z7.string().optional()
4068
+ });
4069
+ BridgeChatItem = z7.discriminatedUnion("kind", [
4070
+ z7.object({
4071
+ kind: z7.literal("user"),
4072
+ id: z7.string(),
4073
+ text: z7.string(),
4074
+ // không gồm danh sách tệp gửi kèm
4075
+ clientId: z7.string().optional(),
4076
+ // chỉ có với tin vừa gửi từ app
4077
+ files: z7.array(BridgeFileRef).optional()
4078
+ }),
4079
+ z7.object({ kind: z7.literal("assistant"), id: z7.string(), text: z7.string() }),
4080
+ /** Claude gọi tool. `summary` là phần dễ đọc của input: lệnh bash, đường dẫn file... */
4081
+ z7.object({
4082
+ kind: z7.literal("tool"),
4083
+ id: z7.string(),
4084
+ name: z7.string(),
4085
+ summary: z7.string(),
4086
+ detail: BridgeToolDetail.optional()
4087
+ }),
4088
+ /** Ảnh Claude nhận từ tool (đọc file ảnh, chụp màn hình...). */
4089
+ z7.object({ kind: z7.literal("image"), id: z7.string(), file: BridgeFileRef }),
4090
+ /** Mốc nén hội thoại. Bridge không đọc được số token thì để trống. */
4091
+ z7.object({
4092
+ kind: z7.literal("compact"),
4093
+ id: z7.string(),
4094
+ trigger: z7.enum(["auto", "manual"]),
4095
+ preTokens: z7.number().optional(),
4096
+ postTokens: z7.number().optional()
4097
+ })
4098
+ ]);
4099
+ BridgePermissionRequest = z7.object({
4100
+ requestId: z7.string(),
4101
+ toolName: z7.string(),
4102
+ title: z7.string(),
4103
+ // câu mô tả sẵn, vd "Claude wants to run: pnpm test"
4104
+ input: z7.record(z7.string(), z7.unknown()),
4105
+ sessionId: z7.string().nullable().optional()
4106
+ // cửa sổ hỏi; bridge bản cũ không gửi
4107
+ });
4108
+ BridgeSessionInfo = z7.object({
4109
+ sessionId: z7.string(),
4110
+ title: z7.string(),
4111
+ updatedAt: z7.string()
4112
+ // ISO
4113
+ });
4114
+ BridgeWindow = z7.object({
4115
+ sessionId: z7.string(),
4116
+ title: z7.string(),
4117
+ busy: z7.boolean()
4118
+ });
4119
+ BridgeServerMessage = z7.discriminatedUnion("type", [
4120
+ /** Gửi khi vừa kết nối và mỗi khi trạng thái đổi. */
4121
+ z7.object({
4122
+ type: z7.literal("state"),
4123
+ sessionId: z7.string().nullable(),
4124
+ busy: z7.boolean(),
4125
+ // đang có lượt chạy
4126
+ pendingPermissions: z7.array(BridgePermissionRequest),
4127
+ // Bridge bản cũ không gửi hai trường này (chưa đổi được chế độ / model từ app)
4128
+ permissionMode: BridgePermissionMode.optional(),
4129
+ model: BridgeModel.optional(),
4130
+ /** Các cửa sổ đang mở. Bridge bản cũ không gửi: chỉ có một cửa sổ là `sessionId`. */
4131
+ windows: z7.array(BridgeWindow).optional()
4132
+ }),
4133
+ /** Toàn bộ lịch sử (đã cắt bớt) của session hiện tại: khi kết nối và khi đổi session. */
4134
+ z7.object({
4135
+ type: z7.literal("history"),
4136
+ sessionId: z7.string().nullable(),
4137
+ items: z7.array(BridgeChatItem)
4138
+ }),
4139
+ z7.object({ type: z7.literal("sessions"), sessions: z7.array(BridgeSessionInfo) }),
4140
+ /** Dòng mới trong lượt đang chạy. */
4141
+ z7.object({ type: z7.literal("item"), sessionId: z7.string().nullable(), item: BridgeChatItem }),
4142
+ z7.object({ type: z7.literal("permission_request"), request: BridgePermissionRequest }),
4143
+ z7.object({
4144
+ type: z7.literal("permission_resolved"),
4145
+ requestId: z7.string(),
4146
+ allow: z7.boolean()
4147
+ }),
4148
+ z7.object({
4149
+ type: z7.literal("turn_end"),
4150
+ sessionId: z7.string().nullable(),
4151
+ ok: z7.boolean(),
4152
+ text: z7.string(),
4153
+ // câu trả lời cuối, hoặc lỗi khi ok = false
4154
+ costUsd: z7.number(),
4155
+ durationMs: z7.number()
4156
+ }),
4157
+ z7.object({
4158
+ type: z7.literal("error"),
4159
+ message: z7.string(),
4160
+ clientId: z7.string().optional()
4161
+ // lỗi do tin `send` có clientId này
4162
+ }),
4163
+ /** Trả lời `get_image`, chỉ gửi cho app hỏi. `data: null` = ảnh không còn hoặc không được đọc. */
4164
+ z7.object({
4165
+ type: z7.literal("image_data"),
4166
+ ref: z7.string(),
4167
+ mimeType: z7.string().optional(),
4168
+ data: z7.string().nullable()
4169
+ })
4170
+ ]);
4171
+ BRIDGE_KEY_PREFIX = "mobk_";
4172
+ RegisterBridgeKey = z7.object({ key: z7.string().regex(/^mobk_[A-Za-z0-9_-]{43}$/, "Kho\xE1 bridge kh\xF4ng h\u1EE3p l\u1EC7") }).strict();
4173
+ BridgePublicUrl = z7.string().max(500).regex(/^wss:\/\/[a-z0-9.-]+(:\d+)?\/[^\s?#]*$/i, "Ph\u1EA3i c\xF3 d\u1EA1ng wss://<host>/ws");
4174
+ BridgeInstance = z7.object({
4175
+ id: z7.string().regex(/^[A-Za-z0-9_-]{1,64}$/, "id bridge kh\xF4ng h\u1EE3p l\u1EC7"),
4176
+ machine: z7.string().trim().min(1).max(100),
4177
+ project: z7.string().trim().max(200)
4178
+ }).strict();
4179
+ PublishBridgeUrl = z7.object({
4180
+ url: BridgePublicUrl.nullable(),
4181
+ ifUrl: BridgePublicUrl.nullable().optional(),
4182
+ instance: BridgeInstance.optional()
4183
+ }).strict();
4184
+ BridgeEndpointInstance = BridgeInstance.extend({
4185
+ url: z7.string().nullable(),
4186
+ updatedAt: z7.string().nullable()
4187
+ });
4188
+ BridgeEndpoint = z7.object({
4189
+ /** Địa chỉ bridge bật gần nhất, cho app bản cũ chỉ nối một bridge. */
4190
+ url: z7.string().nullable(),
4191
+ // null = bridge chưa gửi địa chỉ hoặc đã tắt
4192
+ updatedAt: z7.string().nullable(),
4193
+ hasKey: z7.boolean(),
4194
+ // app đã đăng ký khoá bridge chưa
4195
+ /** Mọi bridge (máy · project), bật gần nhất trước. Server bản cũ không có. */
4196
+ bridges: z7.array(BridgeEndpointInstance).optional()
4197
+ });
4198
+ }
4199
+ });
4200
+
4201
+ // src/announce.ts
4202
+ var RETRY_MIN_MS, RETRY_MAX_MS, UNREGISTERED_RETRY_MS, UrlAnnouncer;
4203
+ var init_announce = __esm({
4204
+ "src/announce.ts"() {
4205
+ "use strict";
4206
+ RETRY_MIN_MS = 2e3;
4207
+ RETRY_MAX_MS = 6e4;
4208
+ UNREGISTERED_RETRY_MS = 1e4;
4209
+ UrlAnnouncer = class {
4210
+ constructor(opts) {
4211
+ this.opts = opts;
4212
+ this.fetchFn = opts.fetchFn ?? fetch;
4213
+ this.sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms).unref()));
4214
+ }
4215
+ /** Địa chỉ còn chờ gửi; `undefined` = không có gì chờ. */
4216
+ want = void 0;
4217
+ /** Địa chỉ server đang giữ do bridge này gửi; `undefined` = chưa gửi được lần nào. */
4218
+ published = void 0;
4219
+ replaced = false;
4220
+ running = false;
4221
+ /** Đã báo khoá chưa đăng ký, để không in lặp mỗi lần thử. */
4222
+ warned = false;
4223
+ fetchFn;
4224
+ sleep;
4225
+ /** Gửi địa chỉ mới (`null` = bridge tắt). Lỗi mạng thì tự thử lại, địa chỉ mới nhất thắng. */
4226
+ announce(url) {
4227
+ if (this.replaced) return Promise.resolve();
4228
+ this.want = url;
4229
+ return this.run();
4230
+ }
4231
+ /** Gọi định kỳ: hỏi server còn giữ địa chỉ của bridge này không, để biết sớm khi bị máy khác thay. */
4232
+ async check() {
4233
+ if (this.running || this.replaced || typeof this.published !== "string") return;
4234
+ if (await this.send(this.published, this.published) === "replaced") this.markReplaced();
4235
+ }
4236
+ /** Lúc tắt bridge: báo server bridge đã tắt, không chờ quá `timeoutMs`. */
4237
+ async clear(timeoutMs = 2e3) {
4238
+ this.want = void 0;
4239
+ if (this.replaced || !this.published) return;
4240
+ await this.send(null, this.published, AbortSignal.timeout(timeoutMs));
4241
+ }
4242
+ markReplaced() {
4243
+ if (this.replaced) return;
4244
+ this.replaced = true;
4245
+ this.want = void 0;
4246
+ console.log("Project n\xE0y v\u1EEBa \u0111\u01B0\u1EE3c b\u1EADt bridge m\u1EDBi v\xE0 \u0111\xE3 nh\u1EADn k\u1EBFt n\u1ED1i, bridge n\xE0y t\u1EF1 t\u1EAFt.");
4247
+ this.opts.onReplaced?.();
4248
+ }
4249
+ async run() {
4250
+ if (this.running) return;
4251
+ this.running = true;
4252
+ let delay = RETRY_MIN_MS;
4253
+ try {
4254
+ while (this.want !== void 0) {
4255
+ const url = this.want;
4256
+ if (url === null && this.published === void 0) {
4257
+ this.want = void 0;
4258
+ break;
4259
+ }
4260
+ const result = await this.send(url, this.published);
4261
+ if (result === "replaced") {
4262
+ this.markReplaced();
4263
+ break;
4264
+ }
4265
+ if (result === "unauthorized") {
4266
+ if (!this.warned) {
4267
+ this.warned = true;
4268
+ console.log(
4269
+ "Server ch\u01B0a nh\u1EADn kho\xE1 c\u1EE7a bridge: m\u1EDF web ho\u1EB7c app \u2192 Claude PC \u2192 C\xE0i \u0111\u1EB7t k\u1EBFt n\u1ED1i, d\xE1n BRIDGE_TOKEN, \u0111\u1EC3 tr\u1ED1ng URL r\u1ED3i L\u01B0u. Bridge t\u1EF1 th\u1EED l\u1EA1i."
4270
+ );
4271
+ }
4272
+ await this.sleep(UNREGISTERED_RETRY_MS);
4273
+ continue;
4274
+ }
4275
+ if (result === "ok") {
4276
+ this.published = url;
4277
+ if (this.want === url) this.want = void 0;
4278
+ delay = RETRY_MIN_MS;
4279
+ this.warned = false;
4280
+ if (url) console.log("\u0110\xE3 g\u1EEDi \u0111\u1ECBa ch\u1EC9 l\xEAn server, app s\u1EBD t\u1EF1 k\u1EBFt n\u1ED1i.");
4281
+ continue;
4282
+ }
4283
+ await this.sleep(delay);
4284
+ delay = Math.min(RETRY_MAX_MS, delay * 2);
4285
+ }
4286
+ } finally {
4287
+ this.running = false;
4288
+ }
4289
+ }
4290
+ /** `ifUrl` khác `undefined` thì server chỉ ghi khi địa chỉ đang lưu đúng bằng nó. */
4291
+ async send(url, ifUrl, signal) {
4292
+ try {
4293
+ const res = await this.fetchFn(`${this.opts.apiUrl}/v1/bridge/url`, {
4294
+ method: "PUT",
4295
+ headers: {
4296
+ Authorization: `Bearer ${this.opts.key}`,
4297
+ "Content-Type": "application/json"
4298
+ },
4299
+ body: JSON.stringify({
4300
+ url,
4301
+ ...ifUrl === void 0 ? {} : { ifUrl },
4302
+ ...this.opts.instance ? { instance: this.opts.instance } : {}
4303
+ }),
4304
+ signal
4305
+ });
4306
+ if (res.ok) return "ok";
4307
+ if (res.status === 401) return "unauthorized";
4308
+ if (res.status === 409) return "replaced";
4309
+ console.error(`G\u1EEDi \u0111\u1ECBa ch\u1EC9 l\xEAn server l\u1ED7i HTTP ${res.status}, th\u1EED l\u1EA1i sau`);
4310
+ return "retry";
4311
+ } catch {
4312
+ return "retry";
4313
+ }
4314
+ }
4315
+ };
4316
+ }
4317
+ });
4318
+
4319
+ // ../../packages/shared/src/push.ts
4320
+ var SNOOZE_CUSTOM, HABIT_DONE, HABIT_SKIP, BILL_PAID, TASK_DONE, PLAN_DAY, POSTPONE_ALL, OPEN_MEET, PLAN_WEEK, GOAL_DONE, GOAL_SKIP, REMINDER_ACTIONS, PUSH_ACTIONS, AGENT_PERMISSION_CATEGORY;
4321
+ var init_push = __esm({
4322
+ "../../packages/shared/src/push.ts"() {
4323
+ "use strict";
4324
+ init_reminders();
4325
+ SNOOZE_CUSTOM = "snooze_custom";
4326
+ HABIT_DONE = "habit_done";
4327
+ HABIT_SKIP = "habit_skip";
4328
+ BILL_PAID = "bill_paid";
4329
+ TASK_DONE = "task_done";
4330
+ PLAN_DAY = "plan_day";
4331
+ POSTPONE_ALL = "postpone_all";
4332
+ OPEN_MEET = "open_meet";
4333
+ PLAN_WEEK = "plan_week";
4334
+ GOAL_DONE = "goal_done";
4335
+ GOAL_SKIP = "goal_skip";
4336
+ REMINDER_ACTIONS = [
4337
+ { action: "snooze_10", title: "10 ph\xFAt" },
4338
+ {
4339
+ action: SNOOZE_CUSTOM,
4340
+ title: "Nh\u1EADp s\u1ED1 ph\xFAt",
4341
+ input: { placeholder: "S\u1ED1 ph\xFAt, vd 45", submit: "Ho\xE3n" }
4342
+ },
4343
+ // Nhắc một lần thì thôi nhắc, nhắc lặp lại thì chờ lần sau (completeReminder)
4344
+ { action: "done", title: "B\u1ECF qua" }
4345
+ ];
4346
+ PUSH_ACTIONS = {
4347
+ // Android tối đa 3 nút: "Xong việc" thay "Bỏ qua" (bỏ qua thì gạt thông báo đi)
4348
+ task_reminder: [{ action: TASK_DONE, title: "Xong vi\u1EC7c" }, ...REMINDER_ACTIONS.slice(0, 2)],
4349
+ // Nhắc trước giờ họp / hẹn: thường chỉ cần biết, hoặc nhắc lại sát giờ
4350
+ event_reminder: [
4351
+ { action: "done", title: "OK" },
4352
+ { action: "snooze_5", title: "Ho\xE3n 5'" }
4353
+ ],
4354
+ reminder: REMINDER_ACTIONS,
4355
+ // Xong = log {delta: 1}; nhắc lặp nên không gọi done (done sẽ bỏ luôn lần kế)
4356
+ habit_reminder: [
4357
+ { action: HABIT_DONE, title: "Xong" },
4358
+ { action: HABIT_SKIP, title: "B\u1ECF qua h\xF4m nay" }
4359
+ ],
4360
+ bill_reminder: [
4361
+ { action: BILL_PAID, title: "\u0110\xE3 \u0111\xF3ng" },
4362
+ { action: "snooze_1440", title: "Ho\xE3n 1 ng\xE0y" }
4363
+ ],
4364
+ // Hoá đơn mỗi kỳ một khác: gõ số tiền ngay trên thông báo (parseVnd), web mở màn hoá đơn
4365
+ bill_amount_reminder: [
4366
+ {
4367
+ action: BILL_PAID,
4368
+ title: "Nh\u1EADp s\u1ED1 ti\u1EC1n",
4369
+ input: { placeholder: "S\u1ED1 ti\u1EC1n, vd 650k", submit: "\u0110\xE3 \u0111\xF3ng" }
4370
+ },
4371
+ { action: "snooze_1440", title: "Ho\xE3n 1 ng\xE0y" }
4372
+ ],
4373
+ budget_alert: [{ action: "open", title: "Xem chi ti\u1EBFt" }],
4374
+ // Lên kế hoạch: mở app, tạo hội thoại với tin nhắn dựng sẵn (POST /v1/notices/:id/plan)
4375
+ daily_briefing: [{ action: PLAN_DAY, title: "L\xEAn k\u1EBF ho\u1EA1ch" }],
4376
+ afternoon_briefing: [
4377
+ { action: PLAN_DAY, title: "L\xEAn k\u1EBF ho\u1EA1ch" },
4378
+ { action: "open", title: "M\u1EDF" }
4379
+ ],
4380
+ // Dời hết sang mai: POST /v1/tasks/postpone {noticeId}, chạy nền không mở app
4381
+ evening_review: [
4382
+ { action: POSTPONE_ALL, title: "D\u1EDDi h\u1EBFt sang mai" },
4383
+ { action: "open", title: "M\u1EDF" }
4384
+ ],
4385
+ // Lên kế hoạch tuần: như PLAN_DAY, tin nhắn dựng sẵn cho cả tuần
4386
+ weekly_preview: [
4387
+ { action: PLAN_WEEK, title: "L\xEAn k\u1EBF ho\u1EA1ch tu\u1EA7n" },
4388
+ { action: "open", title: "M\u1EDF" }
4389
+ ],
4390
+ // Họp không có meetLink thì server gửi bằng category `calendar_alert` (chỉ nút Xem lịch)
4391
+ meeting_reminder: [
4392
+ { action: OPEN_MEET, title: "V\xE0o Meet" },
4393
+ { action: "open", title: "M\u1EDF" }
4394
+ ],
4395
+ // Mở màn Lịch ở ngày bị đổi (data.date)
4396
+ calendar_alert: [{ action: "open", title: "Xem l\u1ECBch" }],
4397
+ // Xong: PATCH /v1/tasks/:taskId done; Dời: POST /v1/planner/skip/:taskId (data.taskId)
4398
+ goal_block: [
4399
+ { action: GOAL_DONE, title: "Xong" },
4400
+ { action: GOAL_SKIP, title: "D\u1EDDi" }
4401
+ ],
4402
+ notice: []
4403
+ };
4404
+ AGENT_PERMISSION_CATEGORY = "agent_permission";
4405
+ }
4021
4406
  });
4022
4407
 
4023
4408
  // src/push.ts
4024
- function createPusher(state2, accessToken, fetchFn = fetch) {
4409
+ function createPusher(state2, accessToken, fetchFn = fetch, origin) {
4025
4410
  return async (notice) => {
4026
4411
  const tokens = [...state2.pushTokens];
4027
4412
  if (tokens.length === 0) return;
4028
4413
  const body = notice.body.replace(/\s+/g, " ").trim();
4414
+ const title = origin?.project ? `${origin.project} \xB7 ${notice.title}` : notice.title;
4029
4415
  const messages = tokens.map((to) => ({
4030
4416
  to,
4031
4417
  priority: "high",
@@ -4034,8 +4420,9 @@ function createPusher(state2, accessToken, fetchFn = fetch) {
4034
4420
  event: notice.event,
4035
4421
  sessionId: notice.sessionId,
4036
4422
  ...notice.requestId ? { requestId: notice.requestId } : {},
4423
+ ...origin ? { bridgeId: origin.bridgeId, project: origin.project } : {},
4037
4424
  push: {
4038
- title: notice.title,
4425
+ title,
4039
4426
  body: body.length > MAX_BODY ? `${body.slice(0, MAX_BODY)}\u2026` : body,
4040
4427
  category: notice.event === "permission_request" ? AGENT_PERMISSION_CATEGORY : "notice",
4041
4428
  channelId: "alerts"
@@ -4105,6 +4492,198 @@ var init_auth = __esm({
4105
4492
  }
4106
4493
  });
4107
4494
 
4495
+ // src/uploads.ts
4496
+ import { mkdirSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
4497
+ import os2 from "node:os";
4498
+ import path2 from "node:path";
4499
+ function safeFileName(name) {
4500
+ const base = path2.basename(name.replace(/\\/g, "/"));
4501
+ const clean = base.replace(/[<>:"/\\|?*\x00-\x1f]/g, "_").replace(/^\.+/, "").trim().slice(0, 120);
4502
+ return clean || "tep";
4503
+ }
4504
+ function saveAttachments(files, root = UPLOAD_DIR) {
4505
+ if (!files.length) return [];
4506
+ cleanOld(root);
4507
+ const dir = path2.join(root, `${Date.now().toString(36)}-${crypto.randomUUID().slice(0, 8)}`);
4508
+ mkdirSync(dir, { recursive: true });
4509
+ const used = /* @__PURE__ */ new Set();
4510
+ return files.map((f) => {
4511
+ let name = safeFileName(f.name);
4512
+ for (let i = 2; used.has(name.toLowerCase()); i++) {
4513
+ const ext = path2.extname(name);
4514
+ name = `${path2.basename(safeFileName(f.name), ext)} (${i})${ext}`;
4515
+ }
4516
+ used.add(name.toLowerCase());
4517
+ const file = path2.join(dir, name);
4518
+ writeFileSync(file, Buffer.from(f.data, "base64"));
4519
+ return file;
4520
+ });
4521
+ }
4522
+ function promptWithFiles(text, paths) {
4523
+ if (!paths.length) return text;
4524
+ const list = paths.map((p) => `- ${p}`).join("\n");
4525
+ return text ? `${text}
4526
+
4527
+ ${FILES_HEAD}
4528
+ ${list}` : `${FILES_HEAD}
4529
+ ${list}`;
4530
+ }
4531
+ function splitFiles(prompt) {
4532
+ const at = prompt.lastIndexOf(FILES_HEAD);
4533
+ if (at < 0 || at > 0 && !prompt.slice(0, at).endsWith("\n\n")) return { text: prompt, paths: [] };
4534
+ const lines = prompt.slice(at + FILES_HEAD.length).split("\n").filter(Boolean);
4535
+ if (!lines.length || !lines.every((l) => l.startsWith("- "))) return { text: prompt, paths: [] };
4536
+ return { text: prompt.slice(0, at).trim(), paths: lines.map((l) => l.slice(2).trim()) };
4537
+ }
4538
+ function fileRef(file, root = UPLOAD_DIR) {
4539
+ const rel = path2.relative(root, file);
4540
+ if (!rel || rel.startsWith("..") || path2.isAbsolute(rel)) return null;
4541
+ const name = path2.basename(file);
4542
+ return { ref: `u/${rel.split(path2.sep).join("/")}`, name, mimeType: mimeOf(name) };
4543
+ }
4544
+ function cleanOld(root, keepMs = KEEP_MS) {
4545
+ let names;
4546
+ try {
4547
+ names = readdirSync(root);
4548
+ } catch {
4549
+ return;
4550
+ }
4551
+ const cutoff = Date.now() - keepMs;
4552
+ for (const name of names) {
4553
+ const p = path2.join(root, name);
4554
+ try {
4555
+ if (statSync(p).mtimeMs < cutoff) rmSync(p, { recursive: true, force: true });
4556
+ } catch {
4557
+ }
4558
+ }
4559
+ }
4560
+ var UPLOAD_DIR, KEEP_MS, FILES_HEAD, MIME, mimeOf;
4561
+ var init_uploads = __esm({
4562
+ "src/uploads.ts"() {
4563
+ "use strict";
4564
+ UPLOAD_DIR = path2.join(os2.tmpdir(), "manualofme-bridge-uploads");
4565
+ KEEP_MS = 3 * 24 * 60 * 60 * 1e3;
4566
+ FILES_HEAD = "T\u1EC7p ng\u01B0\u1EDDi d\xF9ng g\u1EEDi k\xE8m (\u0111\u1ECDc b\u1EB1ng tool Read):";
4567
+ MIME = {
4568
+ ".png": "image/png",
4569
+ ".jpg": "image/jpeg",
4570
+ ".jpeg": "image/jpeg",
4571
+ ".gif": "image/gif",
4572
+ ".webp": "image/webp",
4573
+ ".pdf": "application/pdf"
4574
+ };
4575
+ mimeOf = (name) => MIME[path2.extname(name).toLowerCase()] ?? "application/octet-stream";
4576
+ }
4577
+ });
4578
+
4579
+ // src/images.ts
4580
+ import { existsSync, mkdirSync as mkdirSync2, readFileSync, statSync as statSync2, writeFileSync as writeFileSync2 } from "node:fs";
4581
+ import os3 from "node:os";
4582
+ import path3 from "node:path";
4583
+ function saveToolImage(id, mimeType, data, root = IMAGE_DIR) {
4584
+ const ext = EXT[mimeType];
4585
+ if (!ext) return null;
4586
+ const name = `${id.replace(/[^A-Za-z0-9_-]/g, "_").slice(0, 120)}${ext}`;
4587
+ const file = path3.join(root, name);
4588
+ try {
4589
+ if (!existsSync(file)) {
4590
+ mkdirSync2(root, { recursive: true });
4591
+ cleanOld(root, KEEP_MS2);
4592
+ writeFileSync2(file, Buffer.from(data, "base64"));
4593
+ }
4594
+ } catch (e) {
4595
+ console.error("[bridge] l\u01B0u \u1EA3nh", e);
4596
+ return null;
4597
+ }
4598
+ return { ref: `i/${name}`, name, mimeType };
4599
+ }
4600
+ function readImage(ref, roots = { u: UPLOAD_DIR, i: IMAGE_DIR }) {
4601
+ const m = /^([ui])\/(.+)$/.exec(ref);
4602
+ if (!m) return null;
4603
+ const root = roots[m[1]];
4604
+ const parts = m[2].split("/");
4605
+ if (parts.some((p) => !p || p === "." || p === ".." || p.includes("\\") || p.includes(":")))
4606
+ return null;
4607
+ const file = path3.resolve(root, ...parts);
4608
+ const rel = path3.relative(root, file);
4609
+ if (rel.startsWith("..") || path3.isAbsolute(rel)) return null;
4610
+ if (!IMAGE_EXTS.has(path3.extname(file).toLowerCase())) return null;
4611
+ try {
4612
+ const st = statSync2(file);
4613
+ if (!st.isFile() || st.size > MAX_IMAGE_BYTES) return null;
4614
+ return { mimeType: mimeOf(file), data: readFileSync(file).toString("base64") };
4615
+ } catch {
4616
+ return null;
4617
+ }
4618
+ }
4619
+ var IMAGE_DIR, KEEP_MS2, MAX_IMAGE_BYTES, EXT, IMAGE_EXTS;
4620
+ var init_images = __esm({
4621
+ "src/images.ts"() {
4622
+ "use strict";
4623
+ init_uploads();
4624
+ IMAGE_DIR = path3.join(os3.tmpdir(), "manualofme-bridge-images");
4625
+ KEEP_MS2 = 15 * 24 * 60 * 60 * 1e3;
4626
+ MAX_IMAGE_BYTES = 5 * 1024 * 1024;
4627
+ EXT = {
4628
+ "image/png": ".png",
4629
+ "image/jpeg": ".jpg",
4630
+ "image/gif": ".gif",
4631
+ "image/webp": ".webp"
4632
+ };
4633
+ IMAGE_EXTS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".gif", ".webp"]);
4634
+ }
4635
+ });
4636
+
4637
+ // src/compact.ts
4638
+ import { existsSync as existsSync2, readdirSync as readdirSync2 } from "node:fs";
4639
+ import { readFile } from "node:fs/promises";
4640
+ import os4 from "node:os";
4641
+ import path4 from "node:path";
4642
+ function transcriptPath(cwd, sessionId, home = claudeHome()) {
4643
+ return path4.join(home, "projects", cwd.replace(/[^a-zA-Z0-9]/g, "-"), `${sessionId}.jsonl`);
4644
+ }
4645
+ function findTranscript(cwd, sessionId, home = claudeHome()) {
4646
+ if (!/^[A-Za-z0-9-]+$/.test(sessionId)) return null;
4647
+ const direct = transcriptPath(cwd, sessionId, home);
4648
+ if (existsSync2(direct)) return direct;
4649
+ const projects = path4.join(home, "projects");
4650
+ try {
4651
+ for (const dir of readdirSync2(projects)) {
4652
+ const file = path4.join(projects, dir, `${sessionId}.jsonl`);
4653
+ if (existsSync2(file)) return file;
4654
+ }
4655
+ } catch {
4656
+ }
4657
+ return null;
4658
+ }
4659
+ async function readCompactMeta(file) {
4660
+ const out = /* @__PURE__ */ new Map();
4661
+ const text = await readFile(file, "utf8");
4662
+ for (const line of text.split("\n")) {
4663
+ if (!line.includes('"compact_boundary"')) continue;
4664
+ try {
4665
+ const row = JSON.parse(line);
4666
+ if (row.subtype !== "compact_boundary" || typeof row.uuid !== "string") continue;
4667
+ const m = row.compactMetadata;
4668
+ out.set(row.uuid, {
4669
+ trigger: m?.trigger === "manual" ? "manual" : "auto",
4670
+ preTokens: num(m?.preTokens),
4671
+ postTokens: num(m?.postTokens)
4672
+ });
4673
+ } catch {
4674
+ }
4675
+ }
4676
+ return out;
4677
+ }
4678
+ var claudeHome, num;
4679
+ var init_compact = __esm({
4680
+ "src/compact.ts"() {
4681
+ "use strict";
4682
+ claudeHome = () => process.env.CLAUDE_CONFIG_DIR || path4.join(os4.homedir(), ".claude");
4683
+ num = (v) => typeof v === "number" && Number.isFinite(v) ? v : void 0;
4684
+ }
4685
+ });
4686
+
4108
4687
  // src/transcript.ts
4109
4688
  function toolSummary(input) {
4110
4689
  if (!input || typeof input !== "object") return "";
@@ -4120,14 +4699,79 @@ function blocksOf(message) {
4120
4699
  if (typeof content === "string") return [{ type: "text", text: content }];
4121
4700
  return Array.isArray(content) ? content : [];
4122
4701
  }
4123
- function toChatItems(msg) {
4702
+ function toolDetail(name, input) {
4703
+ if (!input || typeof input !== "object") return void 0;
4704
+ const obj = input;
4705
+ if (name === "Write") {
4706
+ const content = str(obj.content);
4707
+ return content ? { content: clip(content, MAX_DETAIL) } : void 0;
4708
+ }
4709
+ const raw = name === "Edit" ? [obj] : name === "MultiEdit" && Array.isArray(obj.edits) ? obj.edits : [];
4710
+ const edits = raw.filter((e) => e && typeof e === "object").slice(0, MAX_EDITS);
4711
+ if (!edits.length) return void 0;
4712
+ const each = Math.floor(MAX_DETAIL / (2 * edits.length));
4713
+ return {
4714
+ edits: edits.map((e) => ({ old: clip(str(e.old_string), each), new: clip(str(e.new_string), each) }))
4715
+ };
4716
+ }
4717
+ function compactItem(id, meta) {
4718
+ return {
4719
+ kind: "compact",
4720
+ id,
4721
+ trigger: meta.trigger,
4722
+ ...meta.preTokens !== void 0 ? { preTokens: meta.preTokens } : {},
4723
+ ...meta.postTokens !== void 0 ? { postTokens: meta.postTokens } : {}
4724
+ };
4725
+ }
4726
+ function toolImages(blocks, save, skip) {
4727
+ return blocks.flatMap((b) => {
4728
+ if (b.type !== "tool_result" || typeof b.tool_use_id !== "string" || !Array.isArray(b.content))
4729
+ return [];
4730
+ const toolId = b.tool_use_id;
4731
+ if (skip?.has(toolId)) return [];
4732
+ return b.content.flatMap((c, n) => {
4733
+ const src = c.source;
4734
+ if (c.type !== "image" || src?.type !== "base64") return [];
4735
+ if (typeof src.media_type !== "string" || typeof src.data !== "string") return [];
4736
+ const id = `${toolId}:img${n}`;
4737
+ const file = save(id, src.media_type, src.data);
4738
+ return file ? [{ kind: "image", id, file }] : [];
4739
+ });
4740
+ });
4741
+ }
4742
+ function toChatItems(msg, opts = {}) {
4124
4743
  if (msg.parent_tool_use_id) return [];
4125
4744
  const id = msg.uuid ?? crypto.randomUUID();
4745
+ if (msg.type === "system") {
4746
+ if (msg.subtype === "compact_boundary") {
4747
+ const m = msg.compact_metadata;
4748
+ return [
4749
+ compactItem(id, {
4750
+ trigger: m?.trigger === "manual" ? "manual" : "auto",
4751
+ preTokens: num2(m?.pre_tokens),
4752
+ postTokens: num2(m?.post_tokens)
4753
+ })
4754
+ ];
4755
+ }
4756
+ const meta = msg.uuid ? opts.compacts?.get(msg.uuid) : void 0;
4757
+ return meta ? [compactItem(id, meta)] : [];
4758
+ }
4126
4759
  const blocks = blocksOf(msg.message);
4127
4760
  if (msg.type === "user") {
4128
4761
  if (msg.isSynthetic) return [];
4129
- const text = blocks.flatMap((b) => b.type === "text" && typeof b.text === "string" ? [b.text.trim()] : []).filter((t) => t && !NOISE.test(t)).join("\n\n");
4130
- return text ? [{ kind: "user", id, text: clip(text, MAX_TEXT) }] : [];
4762
+ const images = opts.saveImage ? toolImages(blocks, opts.saveImage, opts.sentReads) : [];
4763
+ const raw = blocks.flatMap((b) => b.type === "text" && typeof b.text === "string" ? [b.text.trim()] : []).filter((t) => t && !NOISE.test(t)).join("\n\n");
4764
+ if (raw.startsWith(COMPACT_SUMMARY_PREFIX)) return images;
4765
+ const { text, paths } = splitFiles(raw);
4766
+ const files = paths.flatMap((p) => fileRef(p) ?? []);
4767
+ if (!text && !files.length) return images;
4768
+ const item = {
4769
+ kind: "user",
4770
+ id,
4771
+ text: clip(text, MAX_TEXT),
4772
+ ...files.length ? { files } : {}
4773
+ };
4774
+ return [item, ...images];
4131
4775
  }
4132
4776
  if (msg.type === "assistant") {
4133
4777
  return blocks.flatMap((b, i) => {
@@ -4136,27 +4780,44 @@ function toChatItems(msg) {
4136
4780
  return [{ kind: "assistant", id: itemId, text: clip(b.text.trim(), MAX_TEXT) }];
4137
4781
  }
4138
4782
  if (b.type === "tool_use" && typeof b.name === "string") {
4139
- return [{ kind: "tool", id: itemId, name: b.name, summary: toolSummary(b.input) }];
4783
+ const detail = toolDetail(b.name, b.input);
4784
+ const file = str(b.input?.file_path);
4785
+ if (b.name === "Read" && typeof b.id === "string" && file && fileRef(file)) {
4786
+ opts.sentReads?.add(b.id);
4787
+ }
4788
+ return [
4789
+ {
4790
+ kind: "tool",
4791
+ id: itemId,
4792
+ name: b.name,
4793
+ summary: toolSummary(b.input),
4794
+ ...detail ? { detail } : {}
4795
+ }
4796
+ ];
4140
4797
  }
4141
4798
  return [];
4142
4799
  });
4143
4800
  }
4144
4801
  return [];
4145
4802
  }
4146
- function toHistory(messages, limit = HISTORY_LIMIT) {
4147
- return messages.flatMap(toChatItems).slice(-limit);
4803
+ function toHistory(messages, opts = {}, limit = HISTORY_LIMIT) {
4804
+ const all = { sentReads: /* @__PURE__ */ new Set(), ...opts };
4805
+ return messages.flatMap((m) => toChatItems(m, all)).slice(-limit);
4148
4806
  }
4149
4807
  function clipInput(input, max = 2e3) {
4150
4808
  return Object.fromEntries(
4151
4809
  Object.entries(input).map(([k, v]) => [k, typeof v === "string" ? clip(v, max) : v])
4152
4810
  );
4153
4811
  }
4154
- var MAX_TEXT, MAX_SUMMARY, HISTORY_LIMIT, SUMMARY_KEYS, NOISE, clip;
4812
+ var MAX_TEXT, MAX_SUMMARY, MAX_DETAIL, MAX_EDITS, HISTORY_LIMIT, SUMMARY_KEYS, NOISE, COMPACT_SUMMARY_PREFIX, clip, str, num2;
4155
4813
  var init_transcript = __esm({
4156
4814
  "src/transcript.ts"() {
4157
4815
  "use strict";
4816
+ init_uploads();
4158
4817
  MAX_TEXT = 8e3;
4159
4818
  MAX_SUMMARY = 200;
4819
+ MAX_DETAIL = 3e3;
4820
+ MAX_EDITS = 10;
4160
4821
  HISTORY_LIMIT = 300;
4161
4822
  SUMMARY_KEYS = [
4162
4823
  "command",
@@ -4170,78 +4831,22 @@ var init_transcript = __esm({
4170
4831
  "prompt"
4171
4832
  ];
4172
4833
  NOISE = /^<(system-reminder|ide_[a-z_]+|command-[a-z]+|local-command-[a-z]+)>/;
4834
+ COMPACT_SUMMARY_PREFIX = "This session is being continued from a previous conversation";
4173
4835
  clip = (s, max) => s.length > max ? `${s.slice(0, max)}\u2026` : s;
4174
- }
4175
- });
4176
-
4177
- // src/uploads.ts
4178
- import { mkdirSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
4179
- import os2 from "node:os";
4180
- import path2 from "node:path";
4181
- function safeFileName(name) {
4182
- const base = path2.basename(name.replace(/\\/g, "/"));
4183
- const clean = base.replace(/[<>:"/\\|?*\x00-\x1f]/g, "_").replace(/^\.+/, "").trim().slice(0, 120);
4184
- return clean || "tep";
4185
- }
4186
- function saveAttachments(files, root = UPLOAD_DIR) {
4187
- if (!files.length) return [];
4188
- cleanOld(root);
4189
- const dir = path2.join(root, `${Date.now().toString(36)}-${crypto.randomUUID().slice(0, 8)}`);
4190
- mkdirSync(dir, { recursive: true });
4191
- const used = /* @__PURE__ */ new Set();
4192
- return files.map((f) => {
4193
- let name = safeFileName(f.name);
4194
- for (let i = 2; used.has(name.toLowerCase()); i++) {
4195
- const ext = path2.extname(name);
4196
- name = `${path2.basename(safeFileName(f.name), ext)} (${i})${ext}`;
4197
- }
4198
- used.add(name.toLowerCase());
4199
- const file = path2.join(dir, name);
4200
- writeFileSync(file, Buffer.from(f.data, "base64"));
4201
- return file;
4202
- });
4203
- }
4204
- function promptWithFiles(text, paths) {
4205
- if (!paths.length) return text;
4206
- const list = paths.map((p) => `- ${p}`).join("\n");
4207
- const head = "T\u1EC7p ng\u01B0\u1EDDi d\xF9ng g\u1EEDi k\xE8m (\u0111\u1ECDc b\u1EB1ng tool Read):";
4208
- return text ? `${text}
4209
-
4210
- ${head}
4211
- ${list}` : `${head}
4212
- ${list}`;
4213
- }
4214
- function cleanOld(root) {
4215
- let names;
4216
- try {
4217
- names = readdirSync(root);
4218
- } catch {
4219
- return;
4220
- }
4221
- const cutoff = Date.now() - KEEP_MS;
4222
- for (const name of names) {
4223
- const p = path2.join(root, name);
4224
- try {
4225
- if (statSync(p).mtimeMs < cutoff) rmSync(p, { recursive: true, force: true });
4226
- } catch {
4227
- }
4228
- }
4229
- }
4230
- var UPLOAD_DIR, KEEP_MS;
4231
- var init_uploads = __esm({
4232
- "src/uploads.ts"() {
4233
- "use strict";
4234
- UPLOAD_DIR = path2.join(os2.tmpdir(), "manualofme-bridge-uploads");
4235
- KEEP_MS = 3 * 24 * 60 * 60 * 1e3;
4836
+ str = (v) => typeof v === "string" ? v : "";
4837
+ num2 = (v) => typeof v === "number" && Number.isFinite(v) ? v : void 0;
4236
4838
  }
4237
4839
  });
4238
4840
 
4239
4841
  // src/session.ts
4240
4842
  import * as sdk from "@anthropic-ai/claude-agent-sdk";
4241
- var EDIT_TOOLS, autoAllows, BridgeError, APPEND_PROMPT, errorText, AgentSession;
4843
+ var EDIT_TOOLS, autoAllows, BridgeError, APPEND_PROMPT, sessionTitle, errorText, TITLE_MAX, titleFrom, withWindow, AgentSession;
4242
4844
  var init_session = __esm({
4243
4845
  "src/session.ts"() {
4244
4846
  "use strict";
4847
+ init_bridge();
4848
+ init_compact();
4849
+ init_images();
4245
4850
  init_transcript();
4246
4851
  init_uploads();
4247
4852
  EDIT_TOOLS = /* @__PURE__ */ new Set(["Edit", "MultiEdit", "Write", "NotebookEdit"]);
@@ -4249,38 +4854,53 @@ var init_session = __esm({
4249
4854
  BridgeError = class extends Error {
4250
4855
  };
4251
4856
  APPEND_PROMPT = "Ng\u01B0\u1EDDi d\xF9ng \u0111ang nh\u1EAFn t\u1EEB app \u0111i\u1EC7n tho\u1EA1i qua agent bridge, kh\xF4ng nh\xECn th\u1EA5y editor. Tr\u1EA3 l\u1EDDi g\u1ECDn, n\xF3i r\xF5 \u0111\xE3 s\u1EEDa file n\xE0o. C\u1EA7n h\u1ECFi th\xEC h\u1ECFi b\u1EB1ng c\xE2u tr\u1EA3 l\u1EDDi th\u01B0\u1EDDng.";
4857
+ sessionTitle = (s) => s.customTitle || s.summary || s.firstPrompt || "(ch\u01B0a c\xF3 ti\xEAu \u0111\u1EC1)";
4252
4858
  errorText = (e) => e instanceof Error ? e.message : String(e);
4859
+ TITLE_MAX = 60;
4860
+ titleFrom = (text) => {
4861
+ const line = text.trim().split(/\r?\n/)[0]?.trim() ?? "";
4862
+ return line.length > TITLE_MAX ? `${line.slice(0, TITLE_MAX)}\u2026` : line;
4863
+ };
4864
+ withWindow = (win, text) => win.title ? `${win.title}: ${text}` : text;
4253
4865
  AgentSession = class {
4254
4866
  constructor(deps) {
4255
4867
  this.deps = deps;
4256
4868
  this.sdk = deps.sdk ?? sdk;
4869
+ this.saveImage = deps.saveImage ?? saveToolImage;
4870
+ const { state: state2 } = deps;
4871
+ this.wins = state2.windows.map((w) => ({ ...w, abort: null, tail: null }));
4872
+ if (!this.wins.length && state2.sessionId) {
4873
+ this.wins.push({ sessionId: state2.sessionId, title: "", fresh: false, abort: null, tail: null });
4874
+ }
4257
4875
  }
4258
4876
  sdk;
4259
- abort = null;
4260
- /**
4261
- * Query của lượt đã xong nhưng chưa đóng: Claude bật tác vụ nền (server, tunnel...) thì SDK
4262
- * còn chạy tiếp tới khi tác vụ đó tắt. Gửi tin mới / đổi session thì đóng nó trước.
4263
- */
4264
- tail = null;
4265
4877
  pending = /* @__PURE__ */ new Map();
4878
+ wins;
4879
+ saveImage;
4880
+ /** Cửa sổ dùng gần nhất. App bản cũ chỉ thấy cửa sổ này. */
4266
4881
  get sessionId() {
4267
4882
  return this.deps.state.sessionId;
4268
4883
  }
4884
+ /** Có cửa sổ nào đang chạy không. */
4269
4885
  get busy() {
4270
- return this.abort !== null;
4886
+ return this.wins.some((w) => w.abort !== null);
4271
4887
  }
4272
4888
  /** Chế độ duyệt đang dùng: chọn từ app, chưa chọn thì theo .env. */
4273
4889
  get permissionMode() {
4274
4890
  return this.deps.state.permissionMode ?? this.deps.config.permissionMode;
4275
4891
  }
4892
+ windows() {
4893
+ return this.wins.map((w) => ({ sessionId: w.sessionId, title: w.title, busy: w.abort !== null }));
4894
+ }
4276
4895
  stateMessage() {
4277
4896
  return {
4278
4897
  type: "state",
4279
4898
  sessionId: this.sessionId,
4280
- busy: this.busy,
4899
+ busy: !!this.find(this.sessionId)?.abort,
4281
4900
  pendingPermissions: [...this.pending.values()].map((p) => p.request),
4282
4901
  permissionMode: this.permissionMode,
4283
- model: this.deps.state.model
4902
+ model: this.deps.state.model,
4903
+ windows: this.windows()
4284
4904
  };
4285
4905
  }
4286
4906
  /**
@@ -4298,32 +4918,102 @@ var init_session = __esm({
4298
4918
  }
4299
4919
  this.deps.broadcast(this.stateMessage());
4300
4920
  }
4301
- /** `null` khi session đã đổi trong lúc đọc file: bản lịch sử này cũ rồi, không gửi. */
4302
- async historyMessage() {
4303
- const sessionId = this.sessionId;
4304
- if (!sessionId) return { type: "history", sessionId, items: [] };
4305
- const messages = await this.sdk.getSessionMessages(sessionId, { dir: this.deps.config.cwd });
4306
- if (this.sessionId !== sessionId) return null;
4307
- return { type: "history", sessionId, items: toHistory(messages) };
4921
+ /**
4922
+ * Lịch sử một session, mặc định cửa sổ dùng gần nhất. `null` khi cửa sổ dùng gần nhất đã đổi
4923
+ * trong lúc đọc file: bản lịch sử này cũ rồi, không gửi.
4924
+ */
4925
+ async historyMessage(sessionId) {
4926
+ const current = sessionId === void 0;
4927
+ const id = current ? this.sessionId : sessionId;
4928
+ if (!id || this.find(id)?.fresh) return { type: "history", sessionId: id, items: [] };
4929
+ const messages = await this.sdk.getSessionMessages(id, {
4930
+ dir: this.deps.config.cwd,
4931
+ includeSystemMessages: true
4932
+ });
4933
+ const compacts = messages.some((m) => m.type === "system") ? await this.compacts(id) : void 0;
4934
+ if (current && this.sessionId !== id) return null;
4935
+ return {
4936
+ type: "history",
4937
+ sessionId: id,
4938
+ items: toHistory(messages, { compacts, saveImage: this.saveImage })
4939
+ };
4940
+ }
4941
+ async compacts(sessionId) {
4942
+ try {
4943
+ if (this.deps.readCompacts) return await this.deps.readCompacts(sessionId);
4944
+ const file = findTranscript(this.deps.config.cwd, sessionId);
4945
+ return file ? await readCompactMeta(file) : void 0;
4946
+ } catch (e) {
4947
+ console.error("[bridge] \u0111\u1ECDc m\u1ED1c n\xE9n", e);
4948
+ return void 0;
4949
+ }
4308
4950
  }
4309
4951
  async sessions() {
4310
4952
  const list = await this.sdk.listSessions({ dir: this.deps.config.cwd, limit: 30 });
4311
4953
  return list.map((s) => ({
4312
4954
  sessionId: s.sessionId,
4313
- title: s.customTitle || s.summary || s.firstPrompt || "(ch\u01B0a c\xF3 ti\xEAu \u0111\u1EC1)",
4955
+ title: sessionTitle(s),
4314
4956
  updatedAt: new Date(s.lastModified).toISOString()
4315
4957
  }));
4316
4958
  }
4959
+ /**
4960
+ * Mở cửa sổ cho một session, đã mở thì thôi. `fresh` = cửa sổ trống, `sessionId` thành id của
4961
+ * session Claude khi gửi tin đầu tiên.
4962
+ */
4963
+ openWindow(sessionId, fresh = false) {
4964
+ if (this.find(sessionId)) return;
4965
+ if (this.wins.length >= BRIDGE_MAX_WINDOWS) {
4966
+ throw new BridgeError(`\u0110\xE3 m\u1EDF ${BRIDGE_MAX_WINDOWS} c\u1EEDa s\u1ED5, \u0111\xF3ng b\u1EDBt c\u1EEDa s\u1ED5 c\u0169 tr\u01B0\u1EDBc`);
4967
+ }
4968
+ const win = { sessionId, title: "", fresh, abort: null, tail: null };
4969
+ this.wins.push(win);
4970
+ this.saveWindows();
4971
+ this.deps.broadcast(this.stateMessage());
4972
+ if (!fresh) void this.fillTitle(win);
4973
+ }
4974
+ /** Đóng cửa sổ, session vẫn còn (mở lại từ Lịch sử). Đang chạy thì không đóng được. */
4975
+ closeWindow(sessionId) {
4976
+ const win = this.find(sessionId);
4977
+ if (!win) return;
4978
+ if (win.abort) throw new BridgeError("C\u1EEDa s\u1ED5 n\xE0y \u0111ang ch\u1EA1y, d\u1EEBng l\u01B0\u1EE3t tr\u01B0\u1EDBc khi \u0111\xF3ng");
4979
+ this.closeTail(win);
4980
+ this.wins.splice(this.wins.indexOf(win), 1);
4981
+ for (const p of [...this.pending.values()])
4982
+ if (p.win === win) p.finish({ behavior: "deny", message: "C\u1EEDa s\u1ED5 \u0111\xE3 \u0111\xF3ng" });
4983
+ if (this.sessionId === sessionId) this.deps.state.sessionId = this.wins.at(-1)?.sessionId ?? null;
4984
+ this.saveWindows();
4985
+ this.deps.broadcast(this.stateMessage());
4986
+ }
4987
+ /** Cửa sổ chưa có tiêu đề (mở từ Lịch sử, file trạng thái bản cũ): lấy tiêu đề session. */
4988
+ async fillTitles() {
4989
+ await Promise.all(this.wins.filter((w) => !w.title && !w.fresh).map((w) => this.fillTitle(w)));
4990
+ }
4991
+ async fillTitle(win) {
4992
+ try {
4993
+ const info = await this.sdk.getSessionInfo?.(win.sessionId, { dir: this.deps.config.cwd });
4994
+ if (!info || win.title) return;
4995
+ win.title = titleFrom(sessionTitle(info));
4996
+ if (!this.wins.includes(win)) return;
4997
+ this.saveWindows();
4998
+ this.deps.broadcast(this.stateMessage());
4999
+ } catch (e) {
5000
+ console.error("[bridge] \u0111\u1ECDc ti\xEAu \u0111\u1EC1 session", e);
5001
+ }
5002
+ }
5003
+ /** App bản cũ: chuyển cửa sổ dùng gần nhất sang session khác (`null` = cửa sổ mới). */
4317
5004
  async open(sessionId) {
4318
- if (this.busy) throw new BridgeError("Claude \u0111ang ch\u1EA1y, d\u1EEBng l\u01B0\u1EE3t n\xE0y tr\u01B0\u1EDBc khi \u0111\u1ED5i session");
4319
- this.closeTail();
4320
- this.deps.state.sessionId = sessionId;
5005
+ const id = sessionId ?? crypto.randomUUID();
5006
+ this.openWindow(id, sessionId === null);
5007
+ this.deps.state.sessionId = id;
4321
5008
  this.deps.broadcast(this.stateMessage());
4322
5009
  const history = await this.historyMessage();
4323
5010
  if (history) this.deps.broadcast(history);
4324
5011
  }
4325
- send(input, clientId, attachments = []) {
4326
- if (this.busy) throw new BridgeError("Claude \u0111ang ch\u1EA1y, \u0111\u1EE3i xong ho\u1EB7c b\u1EA5m d\u1EEBng");
5012
+ /** Gửi tin vào một cửa sổ. Không chỉ rõ cửa sổ (app bản cũ) thì dùng cửa sổ dùng gần nhất. */
5013
+ send(input, clientId, attachments = [], sessionId) {
5014
+ const win = sessionId === void 0 ? this.currentWindow() : this.find(sessionId);
5015
+ if (!win) throw new BridgeError("C\u1EEDa s\u1ED5 n\xE0y \u0111\xE3 \u0111\xF3ng, m\u1EDF c\u1EEDa s\u1ED5 kh\xE1c \u0111\u1EC3 g\u1EEDi");
5016
+ if (win.abort) throw new BridgeError("Claude \u0111ang ch\u1EA1y \u1EDF c\u1EEDa s\u1ED5 n\xE0y, \u0111\u1EE3i xong ho\u1EB7c b\u1EA5m d\u1EEBng");
4327
5017
  if (!input && !attachments.length) throw new BridgeError("Tin nh\u1EAFn tr\u1ED1ng");
4328
5018
  let paths;
4329
5019
  try {
@@ -4333,19 +5023,34 @@ var init_session = __esm({
4333
5023
  throw new BridgeError("Kh\xF4ng l\u01B0u \u0111\u01B0\u1EE3c t\u1EC7p \u0111\xEDnh k\xE8m tr\xEAn PC");
4334
5024
  }
4335
5025
  const text = promptWithFiles(input, paths);
4336
- this.closeTail();
5026
+ const files = paths.flatMap((p) => fileRef(p) ?? []);
5027
+ this.closeTail(win);
4337
5028
  const abort = new AbortController();
4338
- this.abort = abort;
5029
+ win.abort = abort;
5030
+ if (!win.title) win.title = titleFrom(input) || (files[0]?.name ?? "");
5031
+ this.deps.state.sessionId = win.sessionId;
5032
+ this.saveWindows();
4339
5033
  this.deps.broadcast(this.stateMessage());
4340
5034
  this.deps.broadcast({
4341
5035
  type: "item",
4342
- sessionId: this.sessionId,
4343
- item: { kind: "user", id: crypto.randomUUID(), text, ...clientId ? { clientId } : {} }
5036
+ sessionId: win.sessionId,
5037
+ item: {
5038
+ kind: "user",
5039
+ id: crypto.randomUUID(),
5040
+ text: input,
5041
+ ...clientId ? { clientId } : {},
5042
+ ...files.length ? { files } : {}
5043
+ }
4344
5044
  });
4345
- void this.run(text, abort);
5045
+ void this.run(win, text, abort);
4346
5046
  }
4347
- interrupt() {
4348
- this.abort?.abort();
5047
+ /** Dừng lượt của một cửa sổ, mặc định cửa sổ dùng gần nhất. */
5048
+ interrupt(sessionId) {
5049
+ this.find(sessionId === void 0 ? this.sessionId : sessionId)?.abort?.abort();
5050
+ }
5051
+ /** Dừng mọi cửa sổ, lúc tắt bridge. */
5052
+ interruptAll() {
5053
+ for (const w of this.wins) w.abort?.abort();
4349
5054
  }
4350
5055
  replyPermission(requestId, allow, message) {
4351
5056
  const p = this.pending.get(requestId);
@@ -4357,12 +5062,31 @@ var init_session = __esm({
4357
5062
  this.setOptions({ permissionMode: "default" });
4358
5063
  }
4359
5064
  }
4360
- closeTail() {
4361
- const tail = this.tail;
4362
- this.tail = null;
5065
+ find(sessionId) {
5066
+ return sessionId ? this.wins.find((w) => w.sessionId === sessionId) : void 0;
5067
+ }
5068
+ /** Cửa sổ dùng gần nhất, chưa có thì mở (app bản cũ chỉ biết cửa sổ này). */
5069
+ currentWindow() {
5070
+ const id = this.sessionId;
5071
+ const win = this.find(id);
5072
+ if (win) return win;
5073
+ if (id) this.openWindow(id);
5074
+ else {
5075
+ const fresh = crypto.randomUUID();
5076
+ this.openWindow(fresh, true);
5077
+ this.deps.state.sessionId = fresh;
5078
+ }
5079
+ return this.find(this.sessionId);
5080
+ }
5081
+ saveWindows() {
5082
+ this.deps.state.windows = this.wins;
5083
+ }
5084
+ closeTail(win) {
5085
+ const tail = win.tail;
5086
+ win.tail = null;
4363
5087
  tail?.close();
4364
5088
  }
4365
- async run(text, abort) {
5089
+ async run(win, text, abort) {
4366
5090
  const { config: config2, broadcast, push } = this.deps;
4367
5091
  const started = Date.now();
4368
5092
  let ok = false;
@@ -4371,22 +5095,26 @@ var init_session = __esm({
4371
5095
  let durationMs = 0;
4372
5096
  let ended = false;
4373
5097
  const mode = this.permissionMode;
5098
+ const denyPending = () => {
5099
+ for (const p of [...this.pending.values()])
5100
+ if (p.win === win) p.finish({ behavior: "deny", message: "L\u01B0\u1EE3t \u0111\xE3 k\u1EBFt th\xFAc" });
5101
+ };
4374
5102
  const endTurn = () => {
4375
5103
  if (ended) return;
4376
5104
  ended = true;
4377
- if (this.abort === abort) this.abort = null;
4378
- for (const p of this.pending.values())
4379
- p.finish({ behavior: "deny", message: "L\u01B0\u1EE3t \u0111\xE3 k\u1EBFt th\xFAc" });
5105
+ if (win.abort === abort) win.abort = null;
5106
+ denyPending();
4380
5107
  if (!durationMs) durationMs = Date.now() - started;
4381
5108
  if (!ok && !resultText) resultText = abort.signal.aborted ? "\u0110\xE3 d\u1EEBng" : "L\u1ED7i kh\xF4ng r\xF5";
4382
- const sessionId = this.sessionId;
5109
+ const { sessionId } = win;
4383
5110
  broadcast({ type: "turn_end", sessionId, ok, text: resultText, costUsd, durationMs });
4384
5111
  broadcast(this.stateMessage());
4385
5112
  if (!abort.signal.aborted) {
4386
5113
  void push({
4387
5114
  event: "turn_end",
4388
5115
  title: ok ? "Claude xong vi\u1EC7c" : "Claude g\u1EB7p l\u1ED7i",
4389
- body: resultText || "(kh\xF4ng c\xF3 n\u1ED9i dung)",
5116
+ // Nhiều cửa sổ chạy song song: ghi tên cửa sổ để biết cái nào xong
5117
+ body: withWindow(win, resultText || "(kh\xF4ng c\xF3 n\u1ED9i dung)"),
4390
5118
  sessionId
4391
5119
  });
4392
5120
  }
@@ -4397,9 +5125,10 @@ var init_session = __esm({
4397
5125
  prompt: text,
4398
5126
  options: {
4399
5127
  cwd: config2.cwd,
4400
- resume: this.sessionId ?? void 0,
5128
+ // Cửa sổ mới: tạo session đúng id app đã biết; còn lại nối tiếp session cũ
5129
+ ...win.fresh ? { sessionId: win.sessionId } : { resume: win.sessionId },
4401
5130
  abortController: abort,
4402
- canUseTool: this.canUseTool,
5131
+ canUseTool: this.canUseTool(win),
4403
5132
  permissionMode: mode,
4404
5133
  ...mode === "bypassPermissions" ? { allowDangerouslySkipPermissions: true } : {},
4405
5134
  model: this.deps.state.model ?? config2.model,
@@ -4410,17 +5139,22 @@ var init_session = __esm({
4410
5139
  systemPrompt: { type: "preset", preset: "claude_code", append: APPEND_PROMPT }
4411
5140
  }
4412
5141
  });
4413
- this.tail = q;
5142
+ win.tail = q;
5143
+ const sentReads = /* @__PURE__ */ new Set();
4414
5144
  for await (const msg of q) {
4415
5145
  if (msg.type === "system" && msg.subtype === "init") {
4416
- if (msg.session_id !== this.sessionId) {
4417
- this.deps.state.sessionId = msg.session_id;
4418
- broadcast(this.stateMessage());
4419
- }
5146
+ this.adoptSession(win, msg.session_id);
4420
5147
  } else if (msg.type === "assistant") {
5148
+ for (const item of toChatItems(msg, { sentReads }))
5149
+ broadcast({ type: "item", sessionId: win.sessionId, item });
5150
+ } else if (msg.type === "user") {
5151
+ for (const item of toChatItems(msg, { saveImage: this.saveImage, sentReads }))
5152
+ if (item.kind === "image") broadcast({ type: "item", sessionId: win.sessionId, item });
5153
+ } else if (msg.type === "system" && msg.subtype === "compact_boundary") {
4421
5154
  for (const item of toChatItems(msg))
4422
- broadcast({ type: "item", sessionId: this.sessionId, item });
5155
+ broadcast({ type: "item", sessionId: win.sessionId, item });
4423
5156
  } else if (msg.type === "result" && !ended) {
5157
+ this.adoptSession(win, msg.session_id || win.sessionId);
4424
5158
  ok = msg.subtype === "success" && !msg.is_error;
4425
5159
  resultText = msg.subtype === "success" ? msg.result : msg.errors.join("\n") || msg.subtype;
4426
5160
  costUsd = msg.total_cost_usd;
@@ -4434,14 +5168,22 @@ var init_session = __esm({
4434
5168
  resultText = abort.signal.aborted ? "\u0110\xE3 d\u1EEBng" : errorText(e);
4435
5169
  }
4436
5170
  } finally {
4437
- if (q && this.tail === q) this.tail = null;
5171
+ if (q && win.tail === q) win.tail = null;
4438
5172
  endTurn();
4439
- if (!this.busy)
4440
- for (const p of this.pending.values())
4441
- p.finish({ behavior: "deny", message: "L\u01B0\u1EE3t \u0111\xE3 k\u1EBFt th\xFAc" });
5173
+ if (!win.abort) denyPending();
4442
5174
  }
4443
5175
  }
4444
- canUseTool = (toolName, input, opts) => new Promise((resolve) => {
5176
+ /** Session Claude đã được tạo: cửa sổ thôi là cửa sổ trống, id khác thì đổi theo. */
5177
+ adoptSession(win, sessionId) {
5178
+ const changed = win.sessionId !== sessionId;
5179
+ if (!changed && !win.fresh) return;
5180
+ if (changed && this.sessionId === win.sessionId) this.deps.state.sessionId = sessionId;
5181
+ win.sessionId = sessionId;
5182
+ win.fresh = false;
5183
+ this.saveWindows();
5184
+ if (changed) this.deps.broadcast(this.stateMessage());
5185
+ }
5186
+ canUseTool = (win) => (toolName, input, opts) => new Promise((resolve) => {
4445
5187
  if (autoAllows(this.permissionMode, toolName)) {
4446
5188
  resolve({ behavior: "allow" });
4447
5189
  return;
@@ -4451,7 +5193,8 @@ var init_session = __esm({
4451
5193
  requestId,
4452
5194
  toolName,
4453
5195
  title: opts.title ?? `${toolName}: ${toolSummary(input) || "(kh\xF4ng c\xF3 chi ti\u1EBFt)"}`,
4454
- input: clipInput(input)
5196
+ input: clipInput(input),
5197
+ sessionId: win.sessionId
4455
5198
  };
4456
5199
  const finish = (result) => {
4457
5200
  if (!this.pending.delete(requestId)) return;
@@ -4471,13 +5214,13 @@ var init_session = __esm({
4471
5214
  this.deps.config.permissionTimeoutMs
4472
5215
  );
4473
5216
  opts.signal.addEventListener("abort", onAbort, { once: true });
4474
- this.pending.set(requestId, { request, finish });
5217
+ this.pending.set(requestId, { request, finish, win });
4475
5218
  this.deps.broadcast({ type: "permission_request", request });
4476
5219
  void this.deps.push({
4477
5220
  event: "permission_request",
4478
5221
  title: "Claude c\u1EA7n b\u1EA1n duy\u1EC7t",
4479
- body: request.title,
4480
- sessionId: this.sessionId,
5222
+ body: withWindow(win, request.title),
5223
+ sessionId: win.sessionId,
4481
5224
  requestId
4482
5225
  });
4483
5226
  });
@@ -4488,7 +5231,13 @@ var init_session = __esm({
4488
5231
  // src/server.ts
4489
5232
  import { createServer } from "node:http";
4490
5233
  import { WebSocket, WebSocketServer } from "ws";
4491
- function createBridgeServer({ token, hub: hub2, session: session2, state: state2 }) {
5234
+ function createBridgeServer({
5235
+ token,
5236
+ hub: hub2,
5237
+ session: session2,
5238
+ state: state2,
5239
+ readImage: read = readImage
5240
+ }) {
4492
5241
  const http = createServer((req, res) => {
4493
5242
  if (req.method === "GET" && req.url === "/health") {
4494
5243
  res.writeHead(200, { "content-type": "application/json" }).end('{"ok":true}');
@@ -4503,8 +5252,8 @@ function createBridgeServer({ token, hub: hub2, session: session2, state: state2
4503
5252
  handleProtocols: (protocols) => protocols.has(BRIDGE_SUBPROTOCOL) ? BRIDGE_SUBPROTOCOL : false
4504
5253
  });
4505
5254
  http.on("upgrade", (req, socket, head) => {
4506
- const path6 = new URL(req.url ?? "/", "http://localhost").pathname;
4507
- if (path6 !== WS_PATH) {
5255
+ const path8 = new URL(req.url ?? "/", "http://localhost").pathname;
5256
+ if (path8 !== WS_PATH) {
4508
5257
  socket.end("HTTP/1.1 404 Not Found\r\n\r\n");
4509
5258
  return;
4510
5259
  }
@@ -4526,14 +5275,25 @@ function createBridgeServer({ token, hub: hub2, session: session2, state: state2
4526
5275
  case "open_session":
4527
5276
  await session2.open(msg.sessionId);
4528
5277
  return;
5278
+ case "open_window":
5279
+ session2.openWindow(msg.sessionId, msg.new);
5280
+ return;
5281
+ case "close_window":
5282
+ session2.closeWindow(msg.sessionId);
5283
+ return;
5284
+ case "get_history": {
5285
+ const history = await session2.historyMessage(msg.sessionId);
5286
+ if (history) sendTo(ws, history);
5287
+ return;
5288
+ }
4529
5289
  case "send":
4530
- session2.send(msg.text, msg.clientId, msg.attachments);
5290
+ session2.send(msg.text, msg.clientId, msg.attachments, msg.sessionId);
4531
5291
  return;
4532
5292
  case "set_options":
4533
5293
  session2.setOptions(msg);
4534
5294
  return;
4535
5295
  case "interrupt":
4536
- session2.interrupt();
5296
+ session2.interrupt(msg.sessionId);
4537
5297
  return;
4538
5298
  case "permission_reply":
4539
5299
  session2.replyPermission(msg.requestId, msg.allow, msg.message);
@@ -4544,6 +5304,9 @@ function createBridgeServer({ token, hub: hub2, session: session2, state: state2
4544
5304
  case "unregister_push":
4545
5305
  state2.removePushToken(msg.token);
4546
5306
  return;
5307
+ case "get_image":
5308
+ sendTo(ws, { type: "image_data", ref: msg.ref, ...read(msg.ref) ?? { data: null } });
5309
+ return;
4547
5310
  }
4548
5311
  }
4549
5312
  wss.on("connection", (ws) => {
@@ -4606,6 +5369,7 @@ var init_server = __esm({
4606
5369
  "use strict";
4607
5370
  init_bridge();
4608
5371
  init_auth();
5372
+ init_images();
4609
5373
  init_session();
4610
5374
  WS_PATH = "/ws";
4611
5375
  PING_MS = 3e4;
@@ -4624,18 +5388,25 @@ var init_server = __esm({
4624
5388
  });
4625
5389
 
4626
5390
  // src/state.ts
4627
- import { mkdirSync as mkdirSync2, readFileSync, renameSync, writeFileSync as writeFileSync2 } from "node:fs";
4628
- import path3 from "node:path";
4629
- import { z as z7 } from "zod";
4630
- var MAX_PUSH_TOKENS, Saved, StateStore;
5391
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync2, renameSync, writeFileSync as writeFileSync3 } from "node:fs";
5392
+ import path5 from "node:path";
5393
+ import { z as z8 } from "zod";
5394
+ var MAX_PUSH_TOKENS, SavedWindow, Saved, StateStore;
4631
5395
  var init_state = __esm({
4632
5396
  "src/state.ts"() {
4633
5397
  "use strict";
4634
5398
  init_bridge();
4635
5399
  MAX_PUSH_TOKENS = 10;
4636
- Saved = z7.object({
4637
- sessionId: z7.string().nullable().catch(null),
4638
- pushTokens: z7.array(z7.string()).catch([]),
5400
+ SavedWindow = z8.object({
5401
+ sessionId: z8.string(),
5402
+ title: z8.string().catch(""),
5403
+ fresh: z8.boolean().catch(false)
5404
+ });
5405
+ Saved = z8.object({
5406
+ /** Cửa sổ dùng gần nhất: app bản cũ chỉ thấy cửa sổ này, app mới mở lên cũng hiện nó trước. */
5407
+ sessionId: z8.string().nullable().catch(null),
5408
+ windows: z8.array(SavedWindow).catch([]),
5409
+ pushTokens: z8.array(z8.string()).catch([]),
4639
5410
  /** Chọn từ app. `null` = theo .env. */
4640
5411
  permissionMode: BridgePermissionMode.nullable().catch(null),
4641
5412
  model: BridgeModel.catch(null)
@@ -4643,9 +5414,9 @@ var init_state = __esm({
4643
5414
  StateStore = class {
4644
5415
  constructor(file) {
4645
5416
  this.file = file;
4646
- this.data = { sessionId: null, pushTokens: [], permissionMode: null, model: null };
5417
+ this.data = { sessionId: null, windows: [], pushTokens: [], permissionMode: null, model: null };
4647
5418
  try {
4648
- const r = Saved.safeParse(JSON.parse(readFileSync(file, "utf8")));
5419
+ const r = Saved.safeParse(JSON.parse(readFileSync2(file, "utf8")));
4649
5420
  if (r.success) this.data = r.data;
4650
5421
  } catch {
4651
5422
  }
@@ -4659,6 +5430,15 @@ var init_state = __esm({
4659
5430
  this.data.sessionId = id;
4660
5431
  this.save();
4661
5432
  }
5433
+ get windows() {
5434
+ return this.data.windows;
5435
+ }
5436
+ set windows(list) {
5437
+ const next = list.map(({ sessionId, title, fresh }) => ({ sessionId, title, fresh }));
5438
+ if (JSON.stringify(next) === JSON.stringify(this.data.windows)) return;
5439
+ this.data.windows = next;
5440
+ this.save();
5441
+ }
4662
5442
  get permissionMode() {
4663
5443
  return this.data.permissionMode;
4664
5444
  }
@@ -4690,9 +5470,9 @@ var init_state = __esm({
4690
5470
  this.save();
4691
5471
  }
4692
5472
  save() {
4693
- mkdirSync2(path3.dirname(this.file), { recursive: true });
5473
+ mkdirSync3(path5.dirname(this.file), { recursive: true });
4694
5474
  const tmp = `${this.file}.tmp`;
4695
- writeFileSync2(tmp, JSON.stringify(this.data, null, 2));
5475
+ writeFileSync3(tmp, JSON.stringify(this.data, null, 2));
4696
5476
  renameSync(tmp, this.file);
4697
5477
  }
4698
5478
  };
@@ -4701,15 +5481,15 @@ var init_state = __esm({
4701
5481
 
4702
5482
  // src/tunnel.ts
4703
5483
  import { spawn } from "node:child_process";
4704
- import { existsSync } from "node:fs";
4705
- import path4 from "node:path";
5484
+ import { existsSync as existsSync3 } from "node:fs";
5485
+ import path6 from "node:path";
4706
5486
  import { createInterface } from "node:readline";
4707
- function findCloudflared(configured, env = process.env, exists = existsSync, platform = process.platform) {
5487
+ function findCloudflared(configured, env = process.env, exists = existsSync3, platform = process.platform) {
4708
5488
  if (configured) return configured;
4709
5489
  if (platform === "win32") {
4710
5490
  for (const base of [env["ProgramFiles(x86)"], env.ProgramFiles]) {
4711
5491
  if (!base) continue;
4712
- const p = path4.win32.join(base, "cloudflared", "cloudflared.exe");
5492
+ const p = path6.win32.join(base, "cloudflared", "cloudflared.exe");
4713
5493
  if (exists(p)) return p;
4714
5494
  }
4715
5495
  }
@@ -4822,12 +5602,17 @@ var init_index = __esm({
4822
5602
  config,
4823
5603
  state,
4824
5604
  broadcast: (msg) => hub.broadcast(msg),
4825
- push: createPusher(state, config.expoAccessToken)
5605
+ push: createPusher(state, config.expoAccessToken, fetch, {
5606
+ bridgeId: config.instance.id,
5607
+ project: config.instance.project
5608
+ })
4826
5609
  });
5610
+ void session.fillTitles();
4827
5611
  server = createBridgeServer({ token: config.token, hub, session, state });
4828
5612
  announcer = new UrlAnnouncer({
4829
5613
  apiUrl: config.apiUrl,
4830
5614
  key: deriveBridgeKey(config.token),
5615
+ instance: config.instance,
4831
5616
  onReplaced: () => void shutdown()
4832
5617
  });
4833
5618
  REPLACED_CHECK_MS = 6e4;
@@ -4838,6 +5623,7 @@ var init_index = __esm({
4838
5623
  const port = typeof address === "object" && address ? address.port : config.port;
4839
5624
  console.log(`Agent bridge: ws://127.0.0.1:${port}/ws`);
4840
5625
  console.log(`Th\u01B0 m\u1EE5c l\xE0m vi\u1EC7c: ${config.cwd}`);
5626
+ console.log(`Project: ${config.instance.project} @ ${config.instance.machine}`);
4841
5627
  console.log(`Ch\u1EBF \u0111\u1ED9 quy\u1EC1n: ${config.permissionMode}`);
4842
5628
  if (cloudflared) {
4843
5629
  console.log("\u0110ang m\u1EDF Cloudflare tunnel...");
@@ -4867,7 +5653,7 @@ var init_index = __esm({
4867
5653
  shutdown = async () => {
4868
5654
  if (closing) return;
4869
5655
  closing = true;
4870
- session.interrupt();
5656
+ session.interruptAll();
4871
5657
  for (const ws of hub.clients) ws.close(1001, "bridge t\u1EAFt");
4872
5658
  server.close();
4873
5659
  tunnel?.stop();
@@ -4883,13 +5669,14 @@ var init_index = __esm({
4883
5669
  // src/cli.ts
4884
5670
  init_config();
4885
5671
  import { randomBytes } from "node:crypto";
4886
- import { existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync2, statSync as statSync2, writeFileSync as writeFileSync3 } from "node:fs";
4887
- import path5 from "node:path";
5672
+ import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync3, statSync as statSync3, writeFileSync as writeFileSync4 } from "node:fs";
5673
+ import path7 from "node:path";
4888
5674
  var HELP = `C\xE1ch d\xF9ng: manualofme-bridge [th\u01B0 m\u1EE5c]
4889
5675
 
4890
5676
  M\u1EDF Claude Code tr\xEAn m\xE1y n\xE0y cho web/app ManualOfMe \u0111i\u1EC1u khi\u1EC3n. Claude l\xE0m vi\u1EC7c trong
4891
- [th\u01B0 m\u1EE5c], b\u1ECF tr\u1ED1ng th\xEC l\xE0 th\u01B0 m\u1EE5c \u0111ang \u0111\u1EE9ng. M\u1ED7i l\xFAc ch\u1EC9 m\u1ED9t bridge: b\u1EADt \u1EDF m\xE1y kh\xE1c
4892
- th\xEC bridge n\xE0y t\u1EF1 t\u1EAFt.
5677
+ [th\u01B0 m\u1EE5c], b\u1ECF tr\u1ED1ng th\xEC l\xE0 th\u01B0 m\u1EE5c \u0111ang \u0111\u1EE9ng. Ch\u1EA1y \u0111\u01B0\u1EE3c nhi\u1EC1u bridge c\xF9ng token, m\u1ED7i
5678
+ project m\u1ED7i m\xE1y m\u1ED9t bridge (t\xEAn project: BRIDGE_NAME, m\u1EB7c \u0111\u1ECBnh t\xEAn th\u01B0 m\u1EE5c). B\u1EADt l\u1EA1i
5679
+ c\xF9ng th\u01B0 m\u1EE5c th\xEC bridge c\u0169 t\u1EF1 t\u1EAFt.
4893
5680
 
4894
5681
  --token Nh\u1EADp l\u1EA1i token (d\xE1n nh\u1EA7m, \u0111\u1ED5i token), l\u01B0u \u0111\xE8 token c\u0169 r\u1ED3i ch\u1EA1y bridge
4895
5682
 
@@ -4904,8 +5691,8 @@ if (args.includes("-h") || args.includes("--help")) {
4904
5691
  var resetToken = args.includes("--token");
4905
5692
  var dirArg = args.find((a) => !a.startsWith("-"));
4906
5693
  if (dirArg) {
4907
- const dir = path5.resolve(dirArg);
4908
- if (!existsSync2(dir) || !statSync2(dir).isDirectory()) {
5694
+ const dir = path7.resolve(dirArg);
5695
+ if (!existsSync4(dir) || !statSync3(dir).isDirectory()) {
4909
5696
  console.error(`Kh\xF4ng c\xF3 th\u01B0 m\u1EE5c ${dir}`);
4910
5697
  process.exit(1);
4911
5698
  }
@@ -4949,10 +5736,10 @@ async function askToken(reset) {
4949
5736
  }
4950
5737
  }
4951
5738
  function saveToken(token) {
4952
- mkdirSync3(CONFIG_DIR, { recursive: true });
4953
- const old = existsSync2(USER_ENV_FILE) ? readFileSync2(USER_ENV_FILE, "utf8") : "";
5739
+ mkdirSync4(CONFIG_DIR, { recursive: true });
5740
+ const old = existsSync4(USER_ENV_FILE) ? readFileSync3(USER_ENV_FILE, "utf8") : "";
4954
5741
  const lines = old.split(/\r?\n/).filter((l) => l && !/^\s*BRIDGE_TOKEN\s*=/.test(l));
4955
- writeFileSync3(USER_ENV_FILE, [`BRIDGE_TOKEN=${token}`, ...lines, ""].join("\n"), { mode: 384 });
5742
+ writeFileSync4(USER_ENV_FILE, [`BRIDGE_TOKEN=${token}`, ...lines, ""].join("\n"), { mode: 384 });
4956
5743
  }
4957
5744
  function readHidden(prompt) {
4958
5745
  return new Promise((resolve) => {