@zeph-to/cli 1.27.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -45,9 +45,11 @@ binary.
45
45
 
46
46
  Once installed, the hooks fire in **every** session of each configured
47
47
  agent — `zeph cc` is the phone-control bridge, not the notification
48
- switch. Turn the volume down without uninstalling: `/zeph-quiet --global`
49
- (blockers only, all projects), `/zeph-mute` (silence, current project),
50
- `/zeph-status` (what's in effect). See [Mute & push mode](#mute--push-mode).
48
+ switch. In Claude Code the routine per-turn push starts off (`quiet` is
49
+ the default); `/zeph-normal` turns it on for a project, `/zeph-loud`
50
+ pushes on every turn, `/zeph-mute` silences a project entirely, and
51
+ `/zeph-status` shows what's in effect. See
52
+ [Mute & push mode](#mute--push-mode).
51
53
 
52
54
  `~/.zeph/config.json` is the single source of truth — the CLI, the MCP
53
55
  server, the plugin hooks, and the listener all read it. You never need
@@ -424,6 +426,7 @@ zeph notify --title "Hello" --json
424
426
  | `--device <id>` | Target device ID |
425
427
  | `--session <id>` | AI session ID so the push threads into that session's chat (or `ZEPH_SESSION_ID` env) |
426
428
  | `--auto` | Apply the push gate before sending — honors the `/zeph-quiet` / `/zeph-loud` push-mode dial, per project or machine-wide (`--global`); gated-out exits silently with code 0 |
429
+ | `--pushmode-default <m>` | Mode `--auto` assumes when the project has no dial: `quiet` (built-in), `normal`, `loud`. A dial the user set always wins |
427
430
  | `--marker <m>` | Push Signal marker for `--auto`: `skip`, `push`, `high` |
428
431
  | `--tools <n>`, `--nonreadonly <n>` | Turn tool counts feeding `--auto`'s heuristic (defaults assume real work) |
429
432
 
@@ -491,13 +494,25 @@ this order — first hit wins:
491
494
  |-------|------|--------|
492
495
  | 1 | `$STATE_DIR/pushmode-<hash>` | `/zeph-quiet` · `/zeph-loud` · `/zeph-normal` |
493
496
  | 2 | `/tmp/zeph-pushmode-<hash>` | older versions (honored only when you own the file) |
494
- | 3 | `$STATE_DIR/pushmode-default` | `/zeph-quiet --global` — the machine-wide default |
495
- | 4 | *(none)* | `normal` |
496
-
497
- So `/zeph-quiet --global` quiets every project that has no dial of its
498
- own, and a per-project dial always overrides it. Mute has no `-default`
499
- form on purpose: it is keyed on presence, not content, so a global mute
500
- could never be lifted for a single project.
497
+ | 3 | `$STATE_DIR/pushmode-default` | the `--global` form of any dial — the machine-wide default |
498
+ | 4 | `--pushmode-default <mode>` | the calling hook (the installed ones pass `normal`) |
499
+ | 5 | *(nothing above)* | `quiet` |
500
+
501
+ **Row 5 changed**: an install with no dial anywhere used to be `normal`.
502
+ It is now `quiet`, so upgrading turns the routine per-turn push off until
503
+ you run `/zeph-normal`. Row 4 is why the hooks this CLI installs are
504
+ unaffected: they name `normal` themselves, since a hook that supplies no
505
+ turn counts also supplies no `high` marker, and `quiet` would make it
506
+ permanently silent rather than merely quieter. Row 4 sits *below* the
507
+ state files on purpose — the flag names a default, it does not override a
508
+ dial the user set.
509
+
510
+ A dial file that exists but reads empty resolves to `normal`, not to row
511
+ 5: an empty file is a failed write, and resolving breakage to silence
512
+ leaves no symptom to debug.
513
+
514
+ Mute has no `-default` form on purpose: it is keyed on presence, not
515
+ content, so a global mute could never be lifted for a single project.
501
516
 
502
517
  Legacy `/tmp/zeph-muted-<hash>` files are still honored when owned by the
503
518
  current user (the state dir moved out of world-writable `/tmp`).
@@ -604,25 +619,40 @@ session.
604
619
 
605
620
  ## Encryption
606
621
 
607
- Push bodies are encrypted with AES-256-GCM. The wrapping key is derived
608
- via ECDH P-256 and synced across your own devices on first run so every
609
- device can read the same push. Toggle encryption in the Zeph app
610
- (Settings Encryption); when disabled, the CLI sends plaintext. No
611
- configuration needed.
612
-
613
- **Threat model honesty:** keys are persisted on the Zeph backend to
614
- enable cross-device sync, so this is *device-shared* encryption not
615
- true end-to-end. It protects push contents from passive network
616
- observers and from a leaked database snapshot taken without the key
617
- store, but it does **not** protect against the Zeph backend itself (it
618
- has the keys it serves to your devices). A true E2E mode (per-device
619
- keypairs, server stores only public keys, no key escrow) is on the
620
- roadmap.
621
-
622
- The `zeph listener` ignores `isEncrypted` pushes for now it has no
623
- per-device key to decrypt them. Stop-hook auto-pushes and `zeph_ask`
624
- responses are not part of the `@<session>` injection path, so this
625
- doesn't affect normal use.
622
+ Push bodies and long-body attachments are encrypted with AES-256-GCM.
623
+ This host holds its own ECDH P-256 keypair in `~/.zeph/device-keys.json`
624
+ generated on first use, and the private half never leaves the machine.
625
+ Each push is encrypted once, and its AES key is wrapped separately for
626
+ every device on your account using ECDH against that device's public
627
+ key.
628
+
629
+ Toggle encryption in the Zeph app (Settings Encryption); when it is
630
+ off, the CLI sends plaintext. No configuration needed.
631
+
632
+ **Threat model:** against a passive backend a leaked snapshot, an
633
+ operator reading the table the stored ciphertext and wrapped keys are
634
+ useless, so push contents stay private. Three limits worth knowing:
635
+
636
+ - **No protection from an active malicious operator.** Recipient public
637
+ keys come from `GET /devices` on that same server, unsigned and
638
+ unpinned. A backend that injects a device record carrying its own key
639
+ gets the message key wrapped for it, and reads everything. Closing
640
+ this needs out-of-band device verification (ADR-0007 Phase 4, not
641
+ built).
642
+ - **No forward secrecy.** The ECDH secret for a given sender/device pair
643
+ is static, so compromising either private key opens every past push
644
+ wrapped for that pair.
645
+ - **`senderPublicKey` is unsigned**, so a swapped one makes a push
646
+ undecryptable — that direction fails closed rather than leaking.
647
+
648
+ A device that has not registered a per-device public key cannot be sent
649
+ to; it is skipped, and if no device qualifies the push goes out in the
650
+ clear rather than arriving as something nothing can open.
651
+
652
+ The `zeph listener` ignores `isEncrypted` pushes for now — it does not
653
+ try to decrypt them. Stop-hook auto-pushes and `zeph_ask` responses are
654
+ not part of the `@<session>` injection path, so this doesn't affect
655
+ normal use.
626
656
 
627
657
  ## Requirements
628
658
 
package/dist/cli.js CHANGED
@@ -96,6 +96,10 @@ Notify options:
96
96
  --session <id> AI session ID (or set ZEPH_SESSION_ID env)
97
97
  --auto Apply the push gate before sending (honors the
98
98
  /zeph-quiet | /zeph-loud dial; silent exit when gated)
99
+ --pushmode-default <m>
100
+ Mode --auto assumes when the project has no dial
101
+ (quiet|normal|loud) [default: quiet]. A dial set with
102
+ /zeph-quiet | /zeph-loud | /zeph-normal always wins
99
103
  --marker <m> Push Signal marker for --auto (skip|push|high)
100
104
  --tools <n> Turn tool count for --auto [default: assume real work]
101
105
  --nonreadonly <n> Non-read-only tool count for --auto
@@ -254,13 +258,14 @@ const handleNotify = async (args) => {
254
258
  // GATE_DEFAULTS ("assume real work") so dumb hooks keep their historical
255
259
  // always-push behavior in normal mode, while the /zeph-quiet | /zeph-loud
256
260
  // dial now works for every hook-driven agent. Gated-out → silent success.
261
+ // With no dial the mode falls back to --pushmode-default, then to quiet.
257
262
  if (args.auto === true) {
258
263
  const verdict = (0, gate_js_1.decidePush)({
259
264
  toolCount: gateCount(args.tools, gate_js_1.GATE_DEFAULTS.toolCount),
260
265
  nonReadonlyCount: gateCount(args.nonreadonly, gate_js_1.GATE_DEFAULTS.nonReadonlyCount),
261
266
  alreadyAsked: gate_js_1.GATE_DEFAULTS.alreadyAsked,
262
267
  marker: (0, gate_js_1.normalizeMarker)(typeof args.marker === 'string' ? args.marker : undefined),
263
- pushMode: (0, gate_js_1.readPushMode)(projectDir),
268
+ pushMode: (0, gate_js_1.autoPushMode)(projectDir, args[gate_js_1.PUSHMODE_DEFAULT_FLAG]),
264
269
  });
265
270
  if (!verdict.push)
266
271
  return 0;
package/dist/crypto.d.ts CHANGED
@@ -1,31 +1,31 @@
1
1
  /**
2
- * Device-shared encryption for Hook SDK — self-contained ECDH P-256 +
2
+ * Per-device encryption for the Hook SDK — self-contained ECDH P-256 +
3
3
  * AES-256-GCM. Mirrors @zeph/crypto API but bundled inline (no external
4
4
  * dependency). Uses Web Crypto API via node:crypto webcrypto — Node.js 18+
5
5
  * (the `crypto` global only exists unflagged from Node 19, so we import it).
6
6
  *
7
- * Threat model honesty (do not call this "E2E" without a footnote):
7
+ * How it works (ADR-0007):
8
8
  *
9
- * The Zeph backend persists the per-user private key in plaintext so it
10
- * can be synced down to a fresh device (fetchServerKeys / uploadServerKeys
11
- * below). That means the backend can decrypt any push body this is NOT
12
- * end-to-end in the standard sense. What it gives you is:
13
- * Protection against passive network observers
14
- * Protection against a leaked DB snapshot taken without the key store
15
- * • Cross-device readability (all your devices share one keypair)
16
- * What it does NOT give you:
17
- * • Protection against the Zeph backend itself
18
- * • Forward secrecy — encryptPushBodyForSelf / encryptFileForSelf do
19
- * ECDH(self, self), which collapses to a static derived key. A single
20
- * device compromise (since all your devices share the same keypair)
21
- * lets the attacker decrypt every past push for which they have the
22
- * ciphertext. The per-message AES key is random, but its wrap key is
23
- * static, so wrapped keys are decryptable forever.
9
+ * This host holds its own ECDH keypair in ~/.zeph/device-keys.json. The
10
+ * private half is generated here and never leaves the server only ever
11
+ * sees public keys. A push is encrypted once with a random AES key, and
12
+ * that key is wrapped separately for each of the user's registered devices
13
+ * using ECDH(this host, that device). Same keypair the stream frames below
14
+ * already used; push and file bodies now share it.
24
15
  *
25
- * True E2E would require a per-device keypair (server stores only public
26
- * keys; senders wrap the message key once per recipient device public
27
- * key). That refactor is on the roadmap; until then, treat push bodies as
28
- * sensitive-but-not-secret.
16
+ * What it does not give you:
17
+ * Forward secrecy the ECDH secret for a given (sender, device) pair is
18
+ * static, so a compromise of either private key retroactively opens every
19
+ * push wrapped for that pair.
20
+ * • Authenticity beyond the key pairing — nothing signs `senderPublicKey`.
21
+ *
22
+ * Superseded scheme: a single account-wide keypair whose private half the
23
+ * backend escrowed so it could sync to new devices. Key escrow was removed
24
+ * server-side (zeph@8a6d21b) and `GET /users/me/keys` has returned a public
25
+ * key only ever since. This client used to react by generating a fresh
26
+ * account keypair and PUTting it back — which overwrote the account public
27
+ * key and encrypted to a key no device held. Both that upload path and the
28
+ * account keypair are gone.
29
29
  */
30
30
  interface EncryptedPayload {
31
31
  ciphertext: string;
@@ -34,57 +34,78 @@ interface EncryptedPayload {
34
34
  keyIv: string;
35
35
  }
36
36
  /**
37
- * Initialize crypto: sync keys with server, then fallback to local/generate.
38
- * Server is source of truth for per-user key pair.
39
- * Safe to call concurrently deduplicates to single init.
40
- * Returns the exported public key (Base64 SPKI).
37
+ * Initialize push/file encryption.
38
+ *
39
+ * Encryption turns on only when the account has explicitly opted in —
40
+ * `encryptionEnabled` from `GET /users/me/keys` is the single authoritative
41
+ * signal (ADR-0008). Server unreachable or flag off leaves it off and every
42
+ * send goes out in the clear.
43
+ *
44
+ * When it is on, this delegates to the per-device keypair: nothing is asked
45
+ * of the server but the flag, and nothing is ever uploaded.
46
+ *
47
+ * Safe to call concurrently — deduplicates to a single init.
48
+ * Returns this host's public key when encryption is active, '' otherwise.
41
49
  */
42
50
  export declare const initCrypto: (apiKey?: string, baseUrl?: string) => Promise<string>;
51
+ /** One target device, as returned by `GET /devices`. */
52
+ export interface DeviceRecipient {
53
+ deviceId: string;
54
+ /** Base64 SPKI of that device's per-device public key. */
55
+ publicKey: string;
56
+ }
57
+ /** deviceId → JSON `{ encryptedKey, keyIv }`, the wire shape the clients parse. */
58
+ export type DeviceKeyMap = Record<string, string>;
59
+ /**
60
+ * Keep only devices this host can actually encrypt for.
61
+ *
62
+ * A device without a public key has never run a build that registers one, and
63
+ * a device still advertising the account-wide key has not migrated to
64
+ * per-device E2E — wrapping for either produces a push it cannot open, which
65
+ * is worse than sending plaintext it can read.
66
+ */
67
+ export declare const selectRecipients: (devices: {
68
+ deviceId: string;
69
+ publicKey?: string;
70
+ }[]) => DeviceRecipient[];
43
71
  export declare const getKeyPair: () => CryptoKeyPair | null;
44
72
  export declare const getPublicKey: () => string | null;
45
73
  /**
46
- * Encrypt push body for a recipient.
47
- * Returns fields ready to merge into the sendPush payload.
74
+ * Drop the cached keys so every later send goes out as plaintext.
75
+ *
76
+ * Callers use this when the server rejects an encrypted send with
77
+ * `PRO_REQUIRED` (ADR-0008): E2E is a Pro feature, and a long-lived process
78
+ * that initialized crypto before a downgrade would otherwise 403 on every push.
79
+ * Same end state as the "server says encryption disabled" branch in initCrypto.
80
+ * Keys on disk are left alone, but this process will not pick them up again:
81
+ * `initCrypto` is memoized on `initPromise` and `ZephHook.ensureCrypto` short-
82
+ * circuits on `cryptoInitialized`. A restart after an upgrade re-adopts them.
48
83
  */
49
- export declare const encryptPushBody: (input: {
50
- title?: string;
51
- body?: string;
52
- url?: string;
53
- }, recipientPublicKeyRaw: string) => Promise<{
54
- body: string;
55
- encryptedKey: string;
56
- senderPublicKey: string;
57
- isEncrypted: true;
58
- }>;
84
+ export declare const disableCrypto: () => void;
59
85
  /**
60
- * Encrypt push body for self (all own devices).
86
+ * Encrypt a push body for the given recipient devices.
87
+ *
88
+ * Returns the wire fields the API expects: `body` carries the ciphertext and
89
+ * IV, `deviceKeyMap` the per-device wrapped keys, `senderPublicKey` the half
90
+ * recipients need to derive the same secret back.
61
91
  */
62
- export declare const encryptPushBodyForSelf: (input: {
92
+ export declare const encryptPushBodyForDevices: (input: {
63
93
  title?: string;
64
94
  body?: string;
65
95
  url?: string;
66
- }) => Promise<{
96
+ }, recipients: DeviceRecipient[]) => Promise<{
67
97
  body: string;
68
- encryptedKey: string;
98
+ deviceKeyMap: DeviceKeyMap;
69
99
  senderPublicKey: string;
70
100
  isEncrypted: true;
71
101
  }>;
72
102
  /**
73
- * Encrypt file content for a recipient.
74
- * Returns encrypted buffer + key material for file attachment metadata.
103
+ * Encrypt file content for the given recipient devices.
75
104
  */
76
- export declare const encryptFileForRecipient: (content: string, recipientPublicKeyRaw: string) => Promise<{
105
+ export declare const encryptFileForDevices: (content: string | Buffer, recipients: DeviceRecipient[]) => Promise<{
77
106
  ciphertext: Buffer;
78
107
  iv: string;
79
- encryptedKey: string;
80
- }>;
81
- /**
82
- * Encrypt file content for self (all own devices).
83
- */
84
- export declare const encryptFileForSelf: (content: string) => Promise<{
85
- ciphertext: Buffer;
86
- iv: string;
87
- encryptedKey: string;
108
+ deviceKeyMap: DeviceKeyMap;
88
109
  }>;
89
110
  /**
90
111
  * Flat ephemeral envelope — field-for-field what the web's decrypt()
@@ -1 +1 @@
1
- {"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AA+EH,UAAU,gBAAgB;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,EAAE,EAAE,MAAM,CAAC;IACX,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;CACf;AA6ED;;;;;GAKG;AACH,eAAO,MAAM,UAAU,GAAI,SAAS,MAAM,EAAE,UAAU,MAAM,KAAG,OAAO,CAAC,MAAM,CAsE5E,CAAC;AA2CF,eAAO,MAAM,UAAU,QAAO,aAAa,GAAG,IAAqB,CAAC;AACpE,eAAO,MAAM,YAAY,QAAO,MAAM,GAAG,IAA+B,CAAC;AAEzE;;;GAGG;AACH,eAAO,MAAM,eAAe,GAC1B,OAAO;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,EACtD,uBAAuB,MAAM,KAC5B,OAAO,CAAC;IACT,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,IAAI,CAAC;CACnB,CAeA,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,sBAAsB,GACjC,OAAO;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,KACrD,OAAO,CAAC;IACT,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,IAAI,CAAC;CACnB,CAaA,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,uBAAuB,GAClC,SAAS,MAAM,EACf,uBAAuB,MAAM,KAC5B,OAAO,CAAC;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,CASlE,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,kBAAkB,GAC7B,SAAS,MAAM,KACd,OAAO,CAAC;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,CAQlE,CAAC;AAWF;;;;GAIG;AACH,MAAM,WAAW,yBAA0B,SAAQ,gBAAgB;IACjE,eAAe,EAAE,MAAM,CAAC;CACzB;AAsBD;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,QAAO,OAAO,CAAC,MAAM,CAoBjD,CAAC;AAEF,eAAO,MAAM,kBAAkB,QAAO,MAAM,GAAG,IAA+B,CAAC;AAE/E;;;GAGG;AACH,eAAO,MAAM,gBAAgB,GAC3B,WAAW,MAAM,EACjB,uBAAuB,MAAM,KAC5B,OAAO,CAAC,yBAAyB,CAKnC,CAAC"}
1
+ {"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AA+EH,UAAU,gBAAgB;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,EAAE,EAAE,MAAM,CAAC;IACX,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;CACf;AAkDD;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,UAAU,GAAI,SAAS,MAAM,EAAE,UAAU,MAAM,KAAG,OAAO,CAAC,MAAM,CAyB5E,CAAC;AAsCF,wDAAwD;AACxD,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,0DAA0D;IAC1D,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,mFAAmF;AACnF,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAElD;;;;;;;GAOG;AACH,eAAO,MAAM,gBAAgB,GAC3B,SAAS;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,EAAE,KAClD,eAAe,EAG8C,CAAC;AA2CjE,eAAO,MAAM,UAAU,QAAO,aAAa,GAAG,IAAkD,CAAC;AACjG,eAAO,MAAM,YAAY,QAAO,MAAM,GAAG,IAA4D,CAAC;AAEtG;;;;;;;;;;GAUG;AACH,eAAO,MAAM,aAAa,QAAO,IAEhC,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,yBAAyB,GACpC,OAAO;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,EACtD,YAAY,eAAe,EAAE,KAC5B,OAAO,CAAC;IACT,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,YAAY,CAAC;IAC3B,eAAe,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,IAAI,CAAC;CACnB,CAkBA,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,qBAAqB,GAChC,SAAS,MAAM,GAAG,MAAM,EACxB,YAAY,eAAe,EAAE,KAC5B,OAAO,CAAC;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,YAAY,CAAA;CAAE,CAsBxE,CAAC;AAcF;;;;GAIG;AACH,MAAM,WAAW,yBAA0B,SAAQ,gBAAgB;IACjE,eAAe,EAAE,MAAM,CAAC;CACzB;AAsBD;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,QAAO,OAAO,CAAC,MAAM,CAoBjD,CAAC;AAEF,eAAO,MAAM,kBAAkB,QAAO,MAAM,GAAG,IAA+B,CAAC;AAE/E;;;GAGG;AACH,eAAO,MAAM,gBAAgB,GAC3B,WAAW,MAAM,EACjB,uBAAuB,MAAM,KAC5B,OAAO,CAAC,yBAAyB,CAKnC,CAAC"}