manualofme-bridge 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/cli.js +1358 -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,33 +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
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. */
4261
4881
  get sessionId() {
4262
4882
  return this.deps.state.sessionId;
4263
4883
  }
4884
+ /** Có cửa sổ nào đang chạy không. */
4264
4885
  get busy() {
4265
- return this.abort !== null;
4886
+ return this.wins.some((w) => w.abort !== null);
4266
4887
  }
4267
4888
  /** Chế độ duyệt đang dùng: chọn từ app, chưa chọn thì theo .env. */
4268
4889
  get permissionMode() {
4269
4890
  return this.deps.state.permissionMode ?? this.deps.config.permissionMode;
4270
4891
  }
4892
+ windows() {
4893
+ return this.wins.map((w) => ({ sessionId: w.sessionId, title: w.title, busy: w.abort !== null }));
4894
+ }
4271
4895
  stateMessage() {
4272
4896
  return {
4273
4897
  type: "state",
4274
4898
  sessionId: this.sessionId,
4275
- busy: this.busy,
4899
+ busy: !!this.find(this.sessionId)?.abort,
4276
4900
  pendingPermissions: [...this.pending.values()].map((p) => p.request),
4277
4901
  permissionMode: this.permissionMode,
4278
- model: this.deps.state.model
4902
+ model: this.deps.state.model,
4903
+ windows: this.windows()
4279
4904
  };
4280
4905
  }
4281
4906
  /**
@@ -4293,31 +4918,102 @@ var init_session = __esm({
4293
4918
  }
4294
4919
  this.deps.broadcast(this.stateMessage());
4295
4920
  }
4296
- /** `null` khi session đã đổi trong lúc đọc file: bản lịch sử này cũ rồi, không gửi. */
4297
- async historyMessage() {
4298
- const sessionId = this.sessionId;
4299
- if (!sessionId) return { type: "history", sessionId, items: [] };
4300
- const messages = await this.sdk.getSessionMessages(sessionId, { dir: this.deps.config.cwd });
4301
- if (this.sessionId !== sessionId) return null;
4302
- 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
+ }
4303
4950
  }
4304
4951
  async sessions() {
4305
4952
  const list = await this.sdk.listSessions({ dir: this.deps.config.cwd, limit: 30 });
4306
4953
  return list.map((s) => ({
4307
4954
  sessionId: s.sessionId,
4308
- title: s.customTitle || s.summary || s.firstPrompt || "(ch\u01B0a c\xF3 ti\xEAu \u0111\u1EC1)",
4955
+ title: sessionTitle(s),
4309
4956
  updatedAt: new Date(s.lastModified).toISOString()
4310
4957
  }));
4311
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). */
4312
5004
  async open(sessionId) {
4313
- if (this.busy) throw new BridgeError("Claude \u0111ang ch\u1EA1y, d\u1EEBng l\u01B0\u1EE3t n\xE0y tr\u01B0\u1EDBc khi \u0111\u1ED5i session");
4314
- this.deps.state.sessionId = sessionId;
5005
+ const id = sessionId ?? crypto.randomUUID();
5006
+ this.openWindow(id, sessionId === null);
5007
+ this.deps.state.sessionId = id;
4315
5008
  this.deps.broadcast(this.stateMessage());
4316
5009
  const history = await this.historyMessage();
4317
5010
  if (history) this.deps.broadcast(history);
4318
5011
  }
4319
- send(input, clientId, attachments = []) {
4320
- 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");
4321
5017
  if (!input && !attachments.length) throw new BridgeError("Tin nh\u1EAFn tr\u1ED1ng");
4322
5018
  let paths;
4323
5019
  try {
@@ -4327,18 +5023,34 @@ var init_session = __esm({
4327
5023
  throw new BridgeError("Kh\xF4ng l\u01B0u \u0111\u01B0\u1EE3c t\u1EC7p \u0111\xEDnh k\xE8m tr\xEAn PC");
4328
5024
  }
4329
5025
  const text = promptWithFiles(input, paths);
5026
+ const files = paths.flatMap((p) => fileRef(p) ?? []);
5027
+ this.closeTail(win);
4330
5028
  const abort = new AbortController();
4331
- 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();
4332
5033
  this.deps.broadcast(this.stateMessage());
4333
5034
  this.deps.broadcast({
4334
5035
  type: "item",
4335
- sessionId: this.sessionId,
4336
- 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
+ }
4337
5044
  });
4338
- void this.run(text, abort);
5045
+ void this.run(win, text, abort);
5046
+ }
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();
4339
5050
  }
4340
- interrupt() {
4341
- this.abort?.abort();
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();
4342
5054
  }
4343
5055
  replyPermission(requestId, allow, message) {
4344
5056
  const p = this.pending.get(requestId);
@@ -4350,22 +5062,73 @@ var init_session = __esm({
4350
5062
  this.setOptions({ permissionMode: "default" });
4351
5063
  }
4352
5064
  }
4353
- async run(text, abort) {
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;
5087
+ tail?.close();
5088
+ }
5089
+ async run(win, text, abort) {
4354
5090
  const { config: config2, broadcast, push } = this.deps;
4355
5091
  const started = Date.now();
4356
5092
  let ok = false;
4357
5093
  let resultText = "";
4358
5094
  let costUsd = 0;
4359
5095
  let durationMs = 0;
5096
+ let ended = false;
4360
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
+ };
5102
+ const endTurn = () => {
5103
+ if (ended) return;
5104
+ ended = true;
5105
+ if (win.abort === abort) win.abort = null;
5106
+ denyPending();
5107
+ if (!durationMs) durationMs = Date.now() - started;
5108
+ if (!ok && !resultText) resultText = abort.signal.aborted ? "\u0110\xE3 d\u1EEBng" : "L\u1ED7i kh\xF4ng r\xF5";
5109
+ const { sessionId } = win;
5110
+ broadcast({ type: "turn_end", sessionId, ok, text: resultText, costUsd, durationMs });
5111
+ broadcast(this.stateMessage());
5112
+ if (!abort.signal.aborted) {
5113
+ void push({
5114
+ event: "turn_end",
5115
+ title: ok ? "Claude xong vi\u1EC7c" : "Claude g\u1EB7p l\u1ED7i",
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)"),
5118
+ sessionId
5119
+ });
5120
+ }
5121
+ };
5122
+ let q = null;
4361
5123
  try {
4362
- const q = this.sdk.query({
5124
+ q = this.sdk.query({
4363
5125
  prompt: text,
4364
5126
  options: {
4365
5127
  cwd: config2.cwd,
4366
- 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 },
4367
5130
  abortController: abort,
4368
- canUseTool: this.canUseTool,
5131
+ canUseTool: this.canUseTool(win),
4369
5132
  permissionMode: mode,
4370
5133
  ...mode === "bypassPermissions" ? { allowDangerouslySkipPermissions: true } : {},
4371
5134
  model: this.deps.state.model ?? config2.model,
@@ -4376,45 +5139,51 @@ var init_session = __esm({
4376
5139
  systemPrompt: { type: "preset", preset: "claude_code", append: APPEND_PROMPT }
4377
5140
  }
4378
5141
  });
5142
+ win.tail = q;
5143
+ const sentReads = /* @__PURE__ */ new Set();
4379
5144
  for await (const msg of q) {
4380
5145
  if (msg.type === "system" && msg.subtype === "init") {
4381
- if (msg.session_id !== this.sessionId) {
4382
- this.deps.state.sessionId = msg.session_id;
4383
- broadcast(this.stateMessage());
4384
- }
5146
+ this.adoptSession(win, msg.session_id);
4385
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") {
4386
5154
  for (const item of toChatItems(msg))
4387
- broadcast({ type: "item", sessionId: this.sessionId, item });
4388
- } else if (msg.type === "result") {
5155
+ broadcast({ type: "item", sessionId: win.sessionId, item });
5156
+ } else if (msg.type === "result" && !ended) {
5157
+ this.adoptSession(win, msg.session_id || win.sessionId);
4389
5158
  ok = msg.subtype === "success" && !msg.is_error;
4390
5159
  resultText = msg.subtype === "success" ? msg.result : msg.errors.join("\n") || msg.subtype;
4391
5160
  costUsd = msg.total_cost_usd;
4392
5161
  durationMs = msg.duration_ms;
5162
+ endTurn();
4393
5163
  }
4394
5164
  }
4395
5165
  } catch (e) {
4396
- ok = false;
4397
- resultText = abort.signal.aborted ? "\u0110\xE3 d\u1EEBng" : errorText(e);
5166
+ if (!ended) {
5167
+ ok = false;
5168
+ resultText = abort.signal.aborted ? "\u0110\xE3 d\u1EEBng" : errorText(e);
5169
+ }
4398
5170
  } finally {
4399
- this.abort = null;
4400
- for (const p of this.pending.values())
4401
- p.finish({ behavior: "deny", message: "L\u01B0\u1EE3t \u0111\xE3 k\u1EBFt th\xFAc" });
4402
- }
4403
- if (!durationMs) durationMs = Date.now() - started;
4404
- if (!ok && !resultText) resultText = abort.signal.aborted ? "\u0110\xE3 d\u1EEBng" : "L\u1ED7i kh\xF4ng r\xF5";
4405
- const sessionId = this.sessionId;
4406
- broadcast({ type: "turn_end", sessionId, ok, text: resultText, costUsd, durationMs });
4407
- broadcast(this.stateMessage());
4408
- if (!abort.signal.aborted) {
4409
- void push({
4410
- event: "turn_end",
4411
- title: ok ? "Claude xong vi\u1EC7c" : "Claude g\u1EB7p l\u1ED7i",
4412
- body: resultText || "(kh\xF4ng c\xF3 n\u1ED9i dung)",
4413
- sessionId
4414
- });
5171
+ if (q && win.tail === q) win.tail = null;
5172
+ endTurn();
5173
+ if (!win.abort) denyPending();
4415
5174
  }
4416
5175
  }
4417
- 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) => {
4418
5187
  if (autoAllows(this.permissionMode, toolName)) {
4419
5188
  resolve({ behavior: "allow" });
4420
5189
  return;
@@ -4424,7 +5193,8 @@ var init_session = __esm({
4424
5193
  requestId,
4425
5194
  toolName,
4426
5195
  title: opts.title ?? `${toolName}: ${toolSummary(input) || "(kh\xF4ng c\xF3 chi ti\u1EBFt)"}`,
4427
- input: clipInput(input)
5196
+ input: clipInput(input),
5197
+ sessionId: win.sessionId
4428
5198
  };
4429
5199
  const finish = (result) => {
4430
5200
  if (!this.pending.delete(requestId)) return;
@@ -4444,13 +5214,13 @@ var init_session = __esm({
4444
5214
  this.deps.config.permissionTimeoutMs
4445
5215
  );
4446
5216
  opts.signal.addEventListener("abort", onAbort, { once: true });
4447
- this.pending.set(requestId, { request, finish });
5217
+ this.pending.set(requestId, { request, finish, win });
4448
5218
  this.deps.broadcast({ type: "permission_request", request });
4449
5219
  void this.deps.push({
4450
5220
  event: "permission_request",
4451
5221
  title: "Claude c\u1EA7n b\u1EA1n duy\u1EC7t",
4452
- body: request.title,
4453
- sessionId: this.sessionId,
5222
+ body: withWindow(win, request.title),
5223
+ sessionId: win.sessionId,
4454
5224
  requestId
4455
5225
  });
4456
5226
  });
@@ -4461,7 +5231,13 @@ var init_session = __esm({
4461
5231
  // src/server.ts
4462
5232
  import { createServer } from "node:http";
4463
5233
  import { WebSocket, WebSocketServer } from "ws";
4464
- 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
+ }) {
4465
5241
  const http = createServer((req, res) => {
4466
5242
  if (req.method === "GET" && req.url === "/health") {
4467
5243
  res.writeHead(200, { "content-type": "application/json" }).end('{"ok":true}');
@@ -4476,8 +5252,8 @@ function createBridgeServer({ token, hub: hub2, session: session2, state: state2
4476
5252
  handleProtocols: (protocols) => protocols.has(BRIDGE_SUBPROTOCOL) ? BRIDGE_SUBPROTOCOL : false
4477
5253
  });
4478
5254
  http.on("upgrade", (req, socket, head) => {
4479
- const path6 = new URL(req.url ?? "/", "http://localhost").pathname;
4480
- if (path6 !== WS_PATH) {
5255
+ const path8 = new URL(req.url ?? "/", "http://localhost").pathname;
5256
+ if (path8 !== WS_PATH) {
4481
5257
  socket.end("HTTP/1.1 404 Not Found\r\n\r\n");
4482
5258
  return;
4483
5259
  }
@@ -4499,14 +5275,25 @@ function createBridgeServer({ token, hub: hub2, session: session2, state: state2
4499
5275
  case "open_session":
4500
5276
  await session2.open(msg.sessionId);
4501
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
+ }
4502
5289
  case "send":
4503
- session2.send(msg.text, msg.clientId, msg.attachments);
5290
+ session2.send(msg.text, msg.clientId, msg.attachments, msg.sessionId);
4504
5291
  return;
4505
5292
  case "set_options":
4506
5293
  session2.setOptions(msg);
4507
5294
  return;
4508
5295
  case "interrupt":
4509
- session2.interrupt();
5296
+ session2.interrupt(msg.sessionId);
4510
5297
  return;
4511
5298
  case "permission_reply":
4512
5299
  session2.replyPermission(msg.requestId, msg.allow, msg.message);
@@ -4517,6 +5304,9 @@ function createBridgeServer({ token, hub: hub2, session: session2, state: state2
4517
5304
  case "unregister_push":
4518
5305
  state2.removePushToken(msg.token);
4519
5306
  return;
5307
+ case "get_image":
5308
+ sendTo(ws, { type: "image_data", ref: msg.ref, ...read(msg.ref) ?? { data: null } });
5309
+ return;
4520
5310
  }
4521
5311
  }
4522
5312
  wss.on("connection", (ws) => {
@@ -4579,6 +5369,7 @@ var init_server = __esm({
4579
5369
  "use strict";
4580
5370
  init_bridge();
4581
5371
  init_auth();
5372
+ init_images();
4582
5373
  init_session();
4583
5374
  WS_PATH = "/ws";
4584
5375
  PING_MS = 3e4;
@@ -4597,18 +5388,25 @@ var init_server = __esm({
4597
5388
  });
4598
5389
 
4599
5390
  // src/state.ts
4600
- import { mkdirSync as mkdirSync2, readFileSync, renameSync, writeFileSync as writeFileSync2 } from "node:fs";
4601
- import path3 from "node:path";
4602
- import { z as z7 } from "zod";
4603
- 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;
4604
5395
  var init_state = __esm({
4605
5396
  "src/state.ts"() {
4606
5397
  "use strict";
4607
5398
  init_bridge();
4608
5399
  MAX_PUSH_TOKENS = 10;
4609
- Saved = z7.object({
4610
- sessionId: z7.string().nullable().catch(null),
4611
- 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([]),
4612
5410
  /** Chọn từ app. `null` = theo .env. */
4613
5411
  permissionMode: BridgePermissionMode.nullable().catch(null),
4614
5412
  model: BridgeModel.catch(null)
@@ -4616,9 +5414,9 @@ var init_state = __esm({
4616
5414
  StateStore = class {
4617
5415
  constructor(file) {
4618
5416
  this.file = file;
4619
- this.data = { sessionId: null, pushTokens: [], permissionMode: null, model: null };
5417
+ this.data = { sessionId: null, windows: [], pushTokens: [], permissionMode: null, model: null };
4620
5418
  try {
4621
- const r = Saved.safeParse(JSON.parse(readFileSync(file, "utf8")));
5419
+ const r = Saved.safeParse(JSON.parse(readFileSync2(file, "utf8")));
4622
5420
  if (r.success) this.data = r.data;
4623
5421
  } catch {
4624
5422
  }
@@ -4632,6 +5430,15 @@ var init_state = __esm({
4632
5430
  this.data.sessionId = id;
4633
5431
  this.save();
4634
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
+ }
4635
5442
  get permissionMode() {
4636
5443
  return this.data.permissionMode;
4637
5444
  }
@@ -4663,9 +5470,9 @@ var init_state = __esm({
4663
5470
  this.save();
4664
5471
  }
4665
5472
  save() {
4666
- mkdirSync2(path3.dirname(this.file), { recursive: true });
5473
+ mkdirSync3(path5.dirname(this.file), { recursive: true });
4667
5474
  const tmp = `${this.file}.tmp`;
4668
- writeFileSync2(tmp, JSON.stringify(this.data, null, 2));
5475
+ writeFileSync3(tmp, JSON.stringify(this.data, null, 2));
4669
5476
  renameSync(tmp, this.file);
4670
5477
  }
4671
5478
  };
@@ -4674,15 +5481,15 @@ var init_state = __esm({
4674
5481
 
4675
5482
  // src/tunnel.ts
4676
5483
  import { spawn } from "node:child_process";
4677
- import { existsSync } from "node:fs";
4678
- import path4 from "node:path";
5484
+ import { existsSync as existsSync3 } from "node:fs";
5485
+ import path6 from "node:path";
4679
5486
  import { createInterface } from "node:readline";
4680
- function findCloudflared(configured, env = process.env, exists = existsSync, platform = process.platform) {
5487
+ function findCloudflared(configured, env = process.env, exists = existsSync3, platform = process.platform) {
4681
5488
  if (configured) return configured;
4682
5489
  if (platform === "win32") {
4683
5490
  for (const base of [env["ProgramFiles(x86)"], env.ProgramFiles]) {
4684
5491
  if (!base) continue;
4685
- const p = path4.win32.join(base, "cloudflared", "cloudflared.exe");
5492
+ const p = path6.win32.join(base, "cloudflared", "cloudflared.exe");
4686
5493
  if (exists(p)) return p;
4687
5494
  }
4688
5495
  }
@@ -4795,12 +5602,17 @@ var init_index = __esm({
4795
5602
  config,
4796
5603
  state,
4797
5604
  broadcast: (msg) => hub.broadcast(msg),
4798
- push: createPusher(state, config.expoAccessToken)
5605
+ push: createPusher(state, config.expoAccessToken, fetch, {
5606
+ bridgeId: config.instance.id,
5607
+ project: config.instance.project
5608
+ })
4799
5609
  });
5610
+ void session.fillTitles();
4800
5611
  server = createBridgeServer({ token: config.token, hub, session, state });
4801
5612
  announcer = new UrlAnnouncer({
4802
5613
  apiUrl: config.apiUrl,
4803
5614
  key: deriveBridgeKey(config.token),
5615
+ instance: config.instance,
4804
5616
  onReplaced: () => void shutdown()
4805
5617
  });
4806
5618
  REPLACED_CHECK_MS = 6e4;
@@ -4811,6 +5623,7 @@ var init_index = __esm({
4811
5623
  const port = typeof address === "object" && address ? address.port : config.port;
4812
5624
  console.log(`Agent bridge: ws://127.0.0.1:${port}/ws`);
4813
5625
  console.log(`Th\u01B0 m\u1EE5c l\xE0m vi\u1EC7c: ${config.cwd}`);
5626
+ console.log(`Project: ${config.instance.project} @ ${config.instance.machine}`);
4814
5627
  console.log(`Ch\u1EBF \u0111\u1ED9 quy\u1EC1n: ${config.permissionMode}`);
4815
5628
  if (cloudflared) {
4816
5629
  console.log("\u0110ang m\u1EDF Cloudflare tunnel...");
@@ -4840,7 +5653,7 @@ var init_index = __esm({
4840
5653
  shutdown = async () => {
4841
5654
  if (closing) return;
4842
5655
  closing = true;
4843
- session.interrupt();
5656
+ session.interruptAll();
4844
5657
  for (const ws of hub.clients) ws.close(1001, "bridge t\u1EAFt");
4845
5658
  server.close();
4846
5659
  tunnel?.stop();
@@ -4856,13 +5669,14 @@ var init_index = __esm({
4856
5669
  // src/cli.ts
4857
5670
  init_config();
4858
5671
  import { randomBytes } from "node:crypto";
4859
- import { existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync2, statSync as statSync2, writeFileSync as writeFileSync3 } from "node:fs";
4860
- 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";
4861
5674
  var HELP = `C\xE1ch d\xF9ng: manualofme-bridge [th\u01B0 m\u1EE5c]
4862
5675
 
4863
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
4864
- [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
4865
- 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.
4866
5680
 
4867
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
4868
5682
 
@@ -4877,8 +5691,8 @@ if (args.includes("-h") || args.includes("--help")) {
4877
5691
  var resetToken = args.includes("--token");
4878
5692
  var dirArg = args.find((a) => !a.startsWith("-"));
4879
5693
  if (dirArg) {
4880
- const dir = path5.resolve(dirArg);
4881
- if (!existsSync2(dir) || !statSync2(dir).isDirectory()) {
5694
+ const dir = path7.resolve(dirArg);
5695
+ if (!existsSync4(dir) || !statSync3(dir).isDirectory()) {
4882
5696
  console.error(`Kh\xF4ng c\xF3 th\u01B0 m\u1EE5c ${dir}`);
4883
5697
  process.exit(1);
4884
5698
  }
@@ -4922,10 +5736,10 @@ async function askToken(reset) {
4922
5736
  }
4923
5737
  }
4924
5738
  function saveToken(token) {
4925
- mkdirSync3(CONFIG_DIR, { recursive: true });
4926
- 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") : "";
4927
5741
  const lines = old.split(/\r?\n/).filter((l) => l && !/^\s*BRIDGE_TOKEN\s*=/.test(l));
4928
- 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 });
4929
5743
  }
4930
5744
  function readHidden(prompt) {
4931
5745
  return new Promise((resolve) => {