sesame-kit 0.4.0 → 0.4.2
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.ja.md +11 -6
- package/README.md +11 -6
- package/clients/python/__pycache__/sesame_client.cpython-313.pyc +0 -0
- package/package.json +3 -2
- package/src/cli.js +9 -3
- package/src/session-ui.js +85 -52
- package/types/cli.d.ts.map +1 -1
- package/types/session-ui.d.ts.map +1 -1
package/README.ja.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
# sesame-kit — SESAME クラウド CLI & ライブラリ (非公式)
|
|
4
4
|
|
|
5
|
+
[](https://www.npmjs.com/package/sesame-kit) [](./LICENSE) [](https://nodejs.org)
|
|
6
|
+
|
|
5
7
|
> English: [README.md](./README.md)
|
|
6
8
|
|
|
7
9
|
> **ステータス** — pre-1.0 でありバグが残っている可能性があります。実運用で概ね安定が確認できた時点で 1.0 にします。依存する場合はバージョンを固定してください。
|
|
@@ -36,14 +38,17 @@
|
|
|
36
38
|
要件は Node.js 18 以上 (ESM / `node:` プロトコルを使用)。
|
|
37
39
|
|
|
38
40
|
```bash
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
npm install
|
|
42
|
-
npm link # グローバルに `sesame` コマンドを公開
|
|
43
|
-
# あるいは: node bin/sesame.js ...
|
|
41
|
+
npm install -g sesame-kit # グローバル CLI: `sesame ...`
|
|
42
|
+
npx sesame-kit --help # インストールせず実行
|
|
43
|
+
npm install sesame-kit # プロジェクトにライブラリとして追加
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
-
|
|
46
|
+
ソースから:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
git clone https://github.com/FukumotoIkuma/sesame-kit.git
|
|
50
|
+
cd sesame-kit && npm install && npm link
|
|
51
|
+
```
|
|
47
52
|
|
|
48
53
|
---
|
|
49
54
|
|
package/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
# sesame-kit — SESAME cloud CLI & library (unofficial)
|
|
4
4
|
|
|
5
|
+
[](https://www.npmjs.com/package/sesame-kit) [](./LICENSE) [](https://nodejs.org)
|
|
6
|
+
|
|
5
7
|
A Node.js CLI and library that drives the SESAME cloud WebSocket API using the same Cognito consumer client as the official SESAME iOS / Android apps. It covers lock control, Hub3 IR (emit and learn), device management, history, and battery level. With `sesame serve` it exposes every feature as JSON-RPC so you can drive SESAME from any language.
|
|
6
8
|
|
|
7
9
|
> 日本語版: [README.ja.md](./README.ja.md)
|
|
@@ -36,14 +38,17 @@ A Node.js port of the official biz3 admin web app ([CANDY-HOUSE/biz.candyhouse.c
|
|
|
36
38
|
Requires Node.js 18+ (uses ESM and the `node:` protocol).
|
|
37
39
|
|
|
38
40
|
```bash
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
npm install
|
|
42
|
-
npm link # expose the `sesame` command globally
|
|
43
|
-
# or: node bin/sesame.js ...
|
|
41
|
+
npm install -g sesame-kit # global CLI: `sesame ...`
|
|
42
|
+
npx sesame-kit --help # or run without installing
|
|
43
|
+
npm install sesame-kit # or as a library in your project
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
-
|
|
46
|
+
From source:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
git clone https://github.com/FukumotoIkuma/sesame-kit.git
|
|
50
|
+
cd sesame-kit && npm install && npm link
|
|
51
|
+
```
|
|
47
52
|
|
|
48
53
|
---
|
|
49
54
|
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sesame-kit",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"description": "SESAME cloud CLI & library (lock control + Hub3 IR + device management). Node.js port of biz3 React app with the Cognito consumer client swapped in for long-lived sessions.",
|
|
5
5
|
"author": "Ikuma Fukumoto",
|
|
6
6
|
"license": "MIT",
|
|
@@ -85,7 +85,8 @@
|
|
|
85
85
|
"build:rpc-schema": "node scripts/gen-rpc-schema.mjs",
|
|
86
86
|
"build:grpc-proto": "node scripts/gen-grpc-proto.mjs",
|
|
87
87
|
"build": "tsc -p tsconfig.json && node scripts/gen-rpc-schema.mjs && node scripts/gen-grpc-proto.mjs",
|
|
88
|
-
"prepack": "tsc -p tsconfig.json && node scripts/gen-rpc-schema.mjs && node scripts/gen-grpc-proto.mjs"
|
|
88
|
+
"prepack": "tsc -p tsconfig.json && node scripts/gen-rpc-schema.mjs && node scripts/gen-grpc-proto.mjs",
|
|
89
|
+
"release": "bash scripts/release.sh"
|
|
89
90
|
},
|
|
90
91
|
"dependencies": {
|
|
91
92
|
"@aws-sdk/client-cognito-identity-provider": "^3.1057.0",
|
package/src/cli.js
CHANGED
|
@@ -1508,6 +1508,10 @@ export async function run(argv = process.argv) {
|
|
|
1508
1508
|
.name("sesame")
|
|
1509
1509
|
.description("SESAME cloud CLI: lock control + Hub3 IR + device management (port of biz3 React with Consumer Cognito client)")
|
|
1510
1510
|
.version(getPkgVersion(), "-V, --version")
|
|
1511
|
+
// 引数不足/未知オプション時に usage を出す (commander 既定はエラー1行のみで不親切)。
|
|
1512
|
+
// この前に設定すると後で追加する全サブコマンドへ継承される。--json 時は writeErr 側で抑止。
|
|
1513
|
+
.showHelpAfterError()
|
|
1514
|
+
.showSuggestionAfterError()
|
|
1511
1515
|
.option("--config-dir <path>", "設定ディレクトリ上書き (default: ~/.config/sesame-hub3)")
|
|
1512
1516
|
.option("--debug", "詳細ログ")
|
|
1513
1517
|
.option("--json", "JSON 出力");
|
|
@@ -1751,9 +1755,11 @@ devices だけで完結します (手入力は呼び名のみ):
|
|
|
1751
1755
|
// BLE 権限/電源エラーは macOS なら該当設定ペインを自動で開いて誘導する。
|
|
1752
1756
|
if (maybeHandleBleError(err)) { finishCli(); return; }
|
|
1753
1757
|
const code = (typeof err.exitCode === "number" && err.exitCode !== 0) ? err.exitCode : 1;
|
|
1754
|
-
// commander の usage
|
|
1755
|
-
if (typeof err.code === "string" && err.code.startsWith("commander.")
|
|
1756
|
-
process.exitCode = code; finishCli(); return;
|
|
1758
|
+
// commander の usage エラー。非 JSON 時は commander が stderr に整形済み (usage 付き) なので二重出力を避ける。
|
|
1759
|
+
if (typeof err.code === "string" && err.code.startsWith("commander.")) {
|
|
1760
|
+
if (!CLI_JSON) { process.exitCode = code; finishCli(); return; }
|
|
1761
|
+
// --json: commander のメッセージ先頭 "error: " を剥がして封筒に載せる (error が二重にならないように)。
|
|
1762
|
+
die((err.message || "usage error").replace(/^error:\s*/i, ""), code); return;
|
|
1757
1763
|
}
|
|
1758
1764
|
die(withStaleHint(err.message || String(err)), code);
|
|
1759
1765
|
}
|
package/src/session-ui.js
CHANGED
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
// 状態が変わった瞬間に描き直す Ink (React for CLI) で実装する。BLE の mechStatus publish や
|
|
6
6
|
// バックグラウンド接続の完了を `bus` の "update" イベントで受け、React state を更新して再描画する。
|
|
7
7
|
//
|
|
8
|
+
// 操作: ↑↓ 移動 / → か Enter で決定 / ← か Esc で戻る / q で終了。
|
|
9
|
+
// アクション実行後はホームに戻らず、その操作メニューに留まる (続けて操作できる)。
|
|
10
|
+
//
|
|
8
11
|
// JSX は使わない (本リポは src を素の ESM で実行しビルド工程が無いため)。React.createElement = h。
|
|
9
12
|
|
|
10
13
|
import React from "react";
|
|
@@ -43,6 +46,7 @@ export function SessionApp({ devices, hasCloud, bus, exec, actionsFor, fmtState,
|
|
|
43
46
|
const [numVal, setNumVal] = React.useState(""); // autolock 秒数 / LED duty 入力
|
|
44
47
|
const [selRemote, setSelRemote] = React.useState(null); // IR: 選択中リモコン
|
|
45
48
|
const [irKeys, setIrKeys] = React.useState(null); // IR: 取得したキー一覧 (null=未取得)
|
|
49
|
+
const [hi, setHi] = React.useState(null); // → 決定用: ハイライト中の項目
|
|
46
50
|
|
|
47
51
|
// BLE onStatus / 背景接続完了 → 再描画。
|
|
48
52
|
React.useEffect(() => {
|
|
@@ -51,18 +55,82 @@ export function SessionApp({ devices, hasCloud, bus, exec, actionsFor, fmtState,
|
|
|
51
55
|
return () => bus.off("update", on);
|
|
52
56
|
}, [bus]);
|
|
53
57
|
|
|
58
|
+
// hi (→ 決定用ハイライト) は各 SelectInput の onHighlight が先頭項目で更新する。
|
|
59
|
+
// SelectInput を描画しない可能性のある mode (空の IR リスト) に入る時だけ明示的にクリアし、
|
|
60
|
+
// 前メニューの項目が残って → が誤爆しないようにする (下の selectAction / selectIrRemote 参照)。
|
|
61
|
+
|
|
54
62
|
const backToActions = () => { setMode("actions"); };
|
|
55
63
|
|
|
56
|
-
//
|
|
64
|
+
// ---- 各メニューの選択ハンドラ (SelectInput.onSelect と → 決定の両方から呼ぶ) ----
|
|
65
|
+
const selectDevice = (it) => {
|
|
66
|
+
if (!it) return;
|
|
67
|
+
if (it.value === "__quit") { exit(); return; }
|
|
68
|
+
setSelName(it.value); setMsg(""); setMode("actions");
|
|
69
|
+
};
|
|
70
|
+
const selectAction = (it) => {
|
|
71
|
+
if (!it) return;
|
|
72
|
+
if (it.value === "__back") { if (single) exit(); else { setMode("devices"); setMsg(""); } return; }
|
|
73
|
+
if (it.value === "autolock") { setNumVal(""); setMode("autolock"); return; }
|
|
74
|
+
if (it.value === "led") { setNumVal(""); setMode("led"); return; }
|
|
75
|
+
if (it.value === "ir") { setHi(null); setSelRemote(null); setIrKeys(null); setMode("ir-remote"); return; }
|
|
76
|
+
runExec(it.value, devices.get(selName));
|
|
77
|
+
};
|
|
78
|
+
const selectIrRemote = (it) => {
|
|
79
|
+
if (!it) return;
|
|
80
|
+
if (it.value === "__back") { backToActions(); return; }
|
|
81
|
+
setHi(null); setSelRemote(it.value); setIrKeys(null); setMode("ir-key");
|
|
82
|
+
Promise.resolve(listKeysFor ? listKeysFor(it.value) : []).then(setIrKeys).catch(() => setIrKeys([]));
|
|
83
|
+
};
|
|
84
|
+
const selectIrKey = (it) => {
|
|
85
|
+
if (!it) return;
|
|
86
|
+
if (it.value === "__back") { setMode("ir-remote"); return; }
|
|
87
|
+
runExec("ir", devices.get(selName), { remote: selRemote, key: it.value });
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// ← / Esc で 1 つ戻る。Esc は最上位 (devices / single の actions) では終了する。
|
|
91
|
+
// ← は最上位では何もしない (誤操作で終了しないように)。
|
|
92
|
+
const goBack = (allowExitAtTop) => {
|
|
93
|
+
if (mode === "actions") {
|
|
94
|
+
if (single) { if (allowExitAtTop) exit(); }
|
|
95
|
+
else { setMode("devices"); setMsg(""); }
|
|
96
|
+
} else if (mode === "ir-key") setMode("ir-remote");
|
|
97
|
+
else if (mode === "devices") { if (allowExitAtTop) exit(); }
|
|
98
|
+
else backToActions(); // autolock / led / ir-remote
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
// 現在 mode のメニュー項目 (render と → 決定で共有。順序が両者で一致する)。
|
|
102
|
+
const menuItems = () => {
|
|
103
|
+
if (mode === "devices") return [...names.map((n) => ({ label: n, value: n })), { label: "終了", value: "__quit" }];
|
|
104
|
+
if (mode === "actions") return [...actionsFor(devices.get(selName)), { label: single ? "終了" : "← 戻る", value: "__back" }];
|
|
105
|
+
if (mode === "ir-remote") {
|
|
106
|
+
const r = hub3RemotesFor ? hub3RemotesFor(devices.get(selName)) : [];
|
|
107
|
+
return r.length ? [...r, { label: "← 戻る", value: "__back" }] : [];
|
|
108
|
+
}
|
|
109
|
+
if (mode === "ir-key") return (irKeys && irKeys.length) ? [...irKeys, { label: "← 戻る", value: "__back" }] : [];
|
|
110
|
+
return [];
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
// → で決定。ink-select-input の onHighlight は初期項目では発火しないため、移動前は hi=null。
|
|
114
|
+
// その場合は先頭 (= 既定でハイライトされている項目) にフォールバックする。
|
|
115
|
+
const goForward = () => {
|
|
116
|
+
const it = hi || menuItems()[0];
|
|
117
|
+
if (!it) return;
|
|
118
|
+
if (mode === "devices") selectDevice(it);
|
|
119
|
+
else if (mode === "actions") selectAction(it);
|
|
120
|
+
else if (mode === "ir-remote") selectIrRemote(it);
|
|
121
|
+
else if (mode === "ir-key") selectIrKey(it);
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const isList = mode === "devices" || mode === "actions" || mode === "ir-remote" || mode === "ir-key";
|
|
125
|
+
|
|
57
126
|
useInput((input, key) => {
|
|
58
127
|
if (mode === "busy") return;
|
|
59
|
-
|
|
60
|
-
if (
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}
|
|
128
|
+
// q は**メニュー系のみ**で終了。autolock/LED の数値入力中は文字として TextInput へ渡す。
|
|
129
|
+
if (input === "q" && isList) { exit(); return; }
|
|
130
|
+
if (key.escape) { goBack(true); return; } // Esc: 戻る (最上位では終了)
|
|
131
|
+
// ← / → は数値入力 (autolock/led) ではテキストカーソル移動に使うので、リスト系のみで奪う。
|
|
132
|
+
if (isList && key.leftArrow) { goBack(false); return; } // ←: 戻る (最上位では何もしない)
|
|
133
|
+
if (isList && key.rightArrow) { goForward(); return; } // →: 決定
|
|
66
134
|
});
|
|
67
135
|
|
|
68
136
|
const runExec = (op, d, extra) => {
|
|
@@ -70,10 +138,10 @@ export function SessionApp({ devices, hasCloud, bus, exec, actionsFor, fmtState,
|
|
|
70
138
|
exec(op, d, extra)
|
|
71
139
|
.then((m) => setMsg(m))
|
|
72
140
|
.catch((e) => setMsg(`error: ${e?.message || e}`))
|
|
73
|
-
.finally(() => setMode(
|
|
141
|
+
.finally(() => setMode("actions")); // ホームに戻らず操作メニューに留まる (続けて操作できる)
|
|
74
142
|
};
|
|
75
143
|
|
|
76
|
-
// ヘッダ: 全デバイスの現在状態 (ライブ)
|
|
144
|
+
// ヘッダ: 全デバイスの現在状態 (ライブ) + 操作ヒント。
|
|
77
145
|
const header = h(
|
|
78
146
|
Box,
|
|
79
147
|
{ flexDirection: "column" },
|
|
@@ -85,6 +153,7 @@ export function SessionApp({ devices, hasCloud, bus, exec, actionsFor, fmtState,
|
|
|
85
153
|
return h(Text, { key: n, color: d.ble ? "green" : undefined },
|
|
86
154
|
` ${n} [${label}·${tag}]: ${fmtState(d)}`);
|
|
87
155
|
}),
|
|
156
|
+
h(Text, { dimColor: true }, " ↑↓ 移動 → 決定 ← 戻る q 終了"),
|
|
88
157
|
msg ? h(Text, { color: "yellow" }, msg) : null,
|
|
89
158
|
);
|
|
90
159
|
const box = (...kids) => h(Box, { flexDirection: "column" }, header, ...kids);
|
|
@@ -116,66 +185,30 @@ export function SessionApp({ devices, hasCloud, bus, exec, actionsFor, fmtState,
|
|
|
116
185
|
const d = devices.get(selName);
|
|
117
186
|
const remotes = (hub3RemotesFor ? hub3RemotesFor(d) : []);
|
|
118
187
|
if (remotes.length === 0) {
|
|
119
|
-
return box(h(Text, null, `${selName}: 登録リモコンがありません ( sesame remote add で登録 )
|
|
188
|
+
return box(h(Text, null, `${selName}: 登録リモコンがありません ( sesame remote add で登録 )。← / Esc で戻る`));
|
|
120
189
|
}
|
|
121
|
-
const items = [...remotes, { label: "← 戻る", value: "__back" }];
|
|
122
190
|
return box(h(Text, null, `${selName} の IR: リモコン選択`),
|
|
123
|
-
h(SelectInput, {
|
|
124
|
-
items,
|
|
125
|
-
onSelect: (it) => {
|
|
126
|
-
if (it.value === "__back") { backToActions(); return; }
|
|
127
|
-
setSelRemote(it.value); setIrKeys(null); setMode("ir-key");
|
|
128
|
-
Promise.resolve(listKeysFor ? listKeysFor(it.value) : []).then(setIrKeys).catch(() => setIrKeys([]));
|
|
129
|
-
},
|
|
130
|
-
}),
|
|
191
|
+
h(SelectInput, { items: menuItems(), onHighlight: setHi, onSelect: selectIrRemote }),
|
|
131
192
|
);
|
|
132
193
|
}
|
|
133
194
|
|
|
134
195
|
// IR: キー選択 (非同期取得中はローディング表示)。
|
|
135
196
|
if (mode === "ir-key") {
|
|
136
|
-
const d = devices.get(selName);
|
|
137
197
|
if (irKeys === null) return box(h(Text, null, `${selRemote}: キー取得中...`));
|
|
138
|
-
if (irKeys.length === 0) return box(h(Text, null, `${selRemote}: キーがありません ( sesame remote sync-keys )
|
|
139
|
-
const items = [...irKeys, { label: "← 戻る", value: "__back" }];
|
|
198
|
+
if (irKeys.length === 0) return box(h(Text, null, `${selRemote}: キーがありません ( sesame remote sync-keys )。← / Esc で戻る`));
|
|
140
199
|
return box(h(Text, null, `${selRemote} のキー選択 (送信)`),
|
|
141
|
-
h(SelectInput, {
|
|
142
|
-
items,
|
|
143
|
-
onSelect: (it) => {
|
|
144
|
-
if (it.value === "__back") { setMode("ir-remote"); return; }
|
|
145
|
-
runExec("ir", d, { remote: selRemote, key: it.value });
|
|
146
|
-
},
|
|
147
|
-
}),
|
|
200
|
+
h(SelectInput, { items: menuItems(), onHighlight: setHi, onSelect: selectIrKey }),
|
|
148
201
|
);
|
|
149
202
|
}
|
|
150
203
|
|
|
151
204
|
if (mode === "actions") {
|
|
152
|
-
const d = devices.get(selName);
|
|
153
|
-
const items = [...actionsFor(d)];
|
|
154
|
-
items.push({ label: single ? "終了" : "← 戻る", value: "__back" });
|
|
155
205
|
return box(h(Text, null, `${selName} の操作:`),
|
|
156
|
-
h(SelectInput, {
|
|
157
|
-
items,
|
|
158
|
-
onSelect: (it) => {
|
|
159
|
-
if (it.value === "__back") { if (single) exit(); else { setMode("devices"); setMsg(""); } return; }
|
|
160
|
-
if (it.value === "autolock") { setNumVal(""); setMode("autolock"); return; }
|
|
161
|
-
if (it.value === "led") { setNumVal(""); setMode("led"); return; }
|
|
162
|
-
if (it.value === "ir") { setSelRemote(null); setIrKeys(null); setMode("ir-remote"); return; }
|
|
163
|
-
runExec(it.value, d);
|
|
164
|
-
},
|
|
165
|
-
}),
|
|
206
|
+
h(SelectInput, { items: menuItems(), onHighlight: setHi, onSelect: selectAction }),
|
|
166
207
|
);
|
|
167
208
|
}
|
|
168
209
|
|
|
169
210
|
// mode === "devices"
|
|
170
|
-
const items = names.map((n) => ({ label: n, value: n }));
|
|
171
|
-
items.push({ label: "終了", value: "__quit" });
|
|
172
211
|
return box(h(Text, null, "操作するデバイス:"),
|
|
173
|
-
h(SelectInput, {
|
|
174
|
-
items,
|
|
175
|
-
onSelect: (it) => {
|
|
176
|
-
if (it.value === "__quit") { exit(); return; }
|
|
177
|
-
setSelName(it.value); setMsg(""); setMode("actions");
|
|
178
|
-
},
|
|
179
|
-
}),
|
|
212
|
+
h(SelectInput, { items: menuItems(), onHighlight: setHi, onSelect: selectDevice }),
|
|
180
213
|
);
|
|
181
214
|
}
|
package/types/cli.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.js"],"names":[],"mappings":"AA+9CA,
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.js"],"names":[],"mappings":"AA+9CA,oDAuQC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-ui.d.ts","sourceRoot":"","sources":["../src/session-ui.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"session-ui.d.ts","sourceRoot":"","sources":["../src/session-ui.js"],"names":[],"mappings":"AAmBA;;;;;;;;;;GAUG;AACH,oCATW;IACN,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE;QAAC,KAAK,EAAC,MAAM,CAAC;QAAC,GAAG,EAAC,MAAM,GAAC,IAAI,CAAA;KAAC,CAAC,CAAC;IACtD,QAAQ,EAAE,OAAO,CAAC;IAClB,GAAG,EAAE,OAAO,aAAa,EAAE,YAAY,CAAC;IACxC,IAAI,EAAE,CAAC,EAAE,EAAC,MAAM,EAAE,MAAM,EAAC,MAAM,EAAE,OAAO,CAAC,EAAC,MAAM,KAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACnE,UAAU,EAAE,CAAC,MAAM,EAAC,MAAM,KAAG,KAAK,CAAC;QAAC,KAAK,EAAC,MAAM,CAAC;QAAC,KAAK,EAAC,MAAM,CAAA;KAAC,CAAC,CAAC;IACjE,QAAQ,EAAE,CAAC,MAAM,EAAC,MAAM,KAAG,MAAM,CAAC;CACnC,iBAKH;AAED;;;;;;;;;QAkLC"}
|