From 09c0e84ffb4a04f005d1b7d9d4f016205a3226a4 Mon Sep 17 00:00:00 2001 From: muzimu217 <1278844978@qq.com> Date: Thu, 17 Sep 2026 11:46:22 -0700 Subject: [PATCH] feat(pi.token-insights): read PI-Desktop turns through the formal usage API The PI-Desktop host now exposes a read-only usage facts surface (pi.usage.listTurns, host PR vastsa/PI-Desktop#503). This switches the plugin's PI-Desktop source from reading pi.sqlite directly to that contract: keyset-paginated completed turns in at-most-365-day windows, walking backward until an empty window, with turnId dedup across page and window seams. - usage.read is declared in the manifest; on hosts without the surface the plugin falls back to the previous local read, so 0.5.0 stays compatible with older releases - soft-deleted sessions are excluded by the contract (a small accounting fix over the direct read, which had no deleted_at filter) - the theme, language, and appearance paths are unchanged Tests: the manifest assertions move from pinning the absence of usage.read to asserting the formal contract usage; the API adapter gets its own unit tests (window walk, cursor paging, dedup, 8-window ceiling, fallback routing); an on-load test's fixed sleep is replaced by polling for the scan's terminal state, which fixes a pre-existing flake on Node 25. --- catalog.json | 17 +- packages/pi.token-insights-0.5.0.piplug | Bin 0 -> 177205 bytes plugins/pi.token-insights/host-read.js | 135 ++++++++++++- plugins/pi.token-insights/main.js | 4 +- plugins/pi.token-insights/manifest.json | 7 +- tests/token-insights.test.mjs | 247 +++++++++++++++++++++++- 6 files changed, 385 insertions(+), 25 deletions(-) create mode 100644 packages/pi.token-insights-0.5.0.piplug diff --git a/catalog.json b/catalog.json index dfed967..b8bcd86 100644 --- a/catalog.json +++ b/catalog.json @@ -2,7 +2,7 @@ "schemaVersion": 1, "providerId": "official", "name": "PI-Desktop Official Plugins", - "updatedAt": "2026-09-14T04:38:39Z", + "updatedAt": "2026-09-17T17:21:16Z", "homepage": "https://github.com/vastsa/pi-desktop-plugins", "plugins": [ { @@ -1027,16 +1027,17 @@ "safetyNotes": "Reads only local metadata, always read-only, always on this device. Usage comes from the tool metadata files PI-Desktop, Claude Code, Codex and OpenCode already write; appearance (theme, language, active plugin theme CSS) and provider display names come from PI-Desktop's own local records. It never reads message text, tool arguments, project paths or credentials, never keeps a full session id (only an 8-character prefix), never writes anywhere except its own plugin settings, and makes no network request.", "versions": [ { - "version": "0.4.8", - "publishedAt": "2026-09-12T17:13:25Z", - "changelog": "Release 0.4.8: Fold PI-Desktop completed-turn totals from the host database into the dashboard, so subagent spend that never lands on parent message.usage still appears without double-counting transcript rows. Settings no longer has a Usage page; this panel is the heatmap. Release 0.4.8\uff1a\u628a\u5bbf\u4e3b\u5df2\u5b8c\u6210\u56de\u5408\u7684 token \u5408\u8ba1\u6298\u5165\u4eea\u8868\u76d8\uff0c\u8f6c\u5f55\u672c message.usage \u672a\u8ba1\u5165\u7684\u5b50\u667a\u80fd\u4f53\u7528\u91cf\u4f1a\u8865\u4e0a\u4e14\u4e0d\u4e0e JSONL \u91cd\u590d\u3002\u8bbe\u7f6e\u9875\u4e0d\u518d\u6709\u7528\u91cf\u5165\u53e3\uff0c\u70ed\u529b\u56fe\u4ee5\u672c\u9762\u677f\u4e3a\u51c6\u3002 Release 0.4.7: Fix the extra empty right-side rail on Windows by keeping the stable scrollbar gutter on the dashboard scroller instead of reserving it again on the root viewport. Release 0.4.7\uff1a\u4fee\u590d Windows \u53f3\u4fa7\u591a\u51fa\u7a7a\u767d\u4fa7\u680f\u7684\u95ee\u9898\uff1a\u4ec5\u5728\u4eea\u8868\u76d8\u6eda\u52a8\u5bb9\u5668\u4fdd\u7559\u7a33\u5b9a\u6eda\u52a8\u6761\u69fd\u4f4d\uff0c\u4e0d\u518d\u5728\u6839\u89c6\u53e3\u91cd\u590d\u9884\u7559\u3002 Release 0.4.6: Fix the titlebar's appearance and refresh buttons overlapping the host window-control capsule. The panel paints through the 46px band under v3 chrome, but the capsule still floats over the top-right corner, so both buttons sat under it and stopped being clickable. The titlebar now reserves the capsule's 104px corner (kept at narrow widths too, where the gutter override previously reset it), the appearance menu stays anchored to the buttons that open it, and a long title truncates instead of running under the reserve. Release 0.4.6\uff1a\u4fee\u590d\u6807\u9898\u680f\u7684\u5916\u89c2\u4e0e\u5237\u65b0\u6309\u94ae\u4e0e\u5bbf\u4e3b\u7a97\u53e3\u63a7\u5236\u80f6\u56ca\u91cd\u53e0\u7684\u95ee\u9898\u3002v3 chrome \u4e0b\u9762\u677f\u53ef\u7ed8\u5236\u5230 46px \u9876\u680f\uff0c\u4f46\u80f6\u56ca\u4ecd\u6d6e\u4e8e\u53f3\u4e0a\u89d2\uff0c\u5bfc\u81f4\u4e24\u4e2a\u6309\u94ae\u88ab\u906e\u6321\u4e14\u65e0\u6cd5\u70b9\u51fb\u3002\u6807\u9898\u680f\u73b0\u4e3a\u80f6\u56ca\u4fdd\u7559 104px \u53f3\u4fa7\u7a7a\u95f4\uff08\u7a84\u5bbd\u5ea6\u4e0b\u7684 gutter \u8986\u76d6\u6b64\u524d\u4f1a\u91cd\u7f6e\u8be5\u4fdd\u7559\u503c\uff0c\u73b0\u5df2\u4fdd\u7559\uff09\uff0c\u5916\u89c2\u83dc\u5355\u7ee7\u7eed\u5bf9\u9f50\u89e6\u53d1\u6309\u94ae\uff0c\u8fc7\u957f\u6807\u9898\u4ee5\u7701\u7565\u53f7\u622a\u65ad\u3002 Release 0.4.5: Migrates the panel to v3 paint-through chrome so top-band controls remain clickable while blank space remains draggable. Release 0.4.4: Fix the window-control capsule being drawn with default black-on-white colors on a panel's very first open \u2014 with no cached appearance, data-theme stayed unset until the host answered, so the host's one-shot color snapshot at DOMContentLoaded missed; boot now falls back to the OS palette synchronously while content stays cloaked. Also stop startAppearanceWatch from resetting the appearance fingerprint, which made every open-command run rewrite hostAppearance. Release 0.4.4\uff1a\u4fee\u590d\u9996\u6b21\u6253\u5f00\u9762\u677f\u65f6\u7a97\u53e3\u63a7\u5236\u80f6\u56ca\u914d\u8272\u9519\u8bef\u7684\u95ee\u9898\u2014\u2014\u65e0\u7f13\u5b58\u5916\u89c2\u65f6 data-theme \u672a\u8bbe\u7f6e\uff0c\u5bbf\u4e3b\u5728 DOMContentLoaded \u7684\u53d6\u8272\u5feb\u7167\u62ff\u5230\u6d4f\u89c8\u5668\u9ed8\u8ba4\u9ed1/\u767d\uff0c\u73b0\u540c\u6b65\u56de\u9000 OS \u914d\u8272\uff08\u5185\u5bb9\u4ecd\u6309 data-booting \u9690\u85cf\u81f3\u771f\u5b9e\u5916\u89c2\u5230\u8fbe\uff09\uff1b\u4fee\u590d startAppearanceWatch \u91cd\u7f6e\u5916\u89c2\u6307\u7eb9\u5bfc\u81f4\u6bcf\u6b21\u6253\u5f00\u547d\u4ee4\u90fd\u91cd\u590d\u5199\u5165 hostAppearance \u7684\u95ee\u9898\u3002 Release 0.4.3: Align the panel with PI-Desktop's cross-platform 46px host drag band and minimal three-button window-control capsule. Fixes two visible faults: elements toggled with the hidden attribute were being kept on screen by their own CSS display, so the loading skeleton stayed over real data, an empty chip strip and the custom date inputs were always drawn, and the last scan sentence lingered; and the from/to fields now always show the window the selected range covers, so picking 7D/90D/ALL keeps them in step and switching to Custom inherits what you were looking at. Includes 0.4.1's scan progress with a real file count, stale-scan recovery, visibility-based polling and debounced source watching, on top of 0.4.0's theme/language following, filters and streak layer.", + "version": "0.5.0", + "publishedAt": "2026-09-17T17:21:16Z", + "changelog": "Release 0.5.0: The PI-Desktop data source now goes through the official pi.usage.listTurns contract (requires a PI-Desktop build with that API; declare the usage.read permission) instead of reading the host's pi.sqlite directly, which also fixes the counting gap where soft-deleted sessions were still included. Hosts without the contract automatically fall back to the previous database path. Release 0.5.0\uff1aPI-Desktop \u6570\u636e\u6e90\u8fc1\u79fb\u5230\u6b63\u5f0f\u7684 pi.usage.listTurns \u5951\u7ea6\uff08\u9700\u5bbf\u4e3b\u63d0\u4f9b\u8be5 API\uff1b\u58f0\u660e usage.read \u6743\u9650\uff09\uff0c\u4e0d\u518d\u76f4\u8bfb\u5bbf\u4e3b pi.sqlite\uff0c\u540c\u65f6\u4fee\u590d\u8f6f\u5220\u9664\u4f1a\u8bdd\u4ecd\u88ab\u8ba1\u5165\u7684\u53e3\u5f84\u7f3a\u53e3\uff1b\u65e0\u8be5\u5951\u7ea6\u7684\u65e7\u5bbf\u4e3b\u81ea\u52a8\u56de\u9000\u539f\u6570\u636e\u5e93\u8def\u5f84\u3002 Release 0.4.8: Fold PI-Desktop completed-turn totals from the host database into the dashboard, so subagent spend that never lands on parent message.usage still appears without double-counting transcript rows. Settings no longer has a Usage page; this panel is the heatmap. Release 0.4.8\uff1a\u628a\u5bbf\u4e3b\u5df2\u5b8c\u6210\u56de\u5408\u7684 token \u5408\u8ba1\u6298\u5165\u4eea\u8868\u76d8\uff0c\u8f6c\u5f55\u672c message.usage \u672a\u8ba1\u5165\u7684\u5b50\u667a\u80fd\u4f53\u7528\u91cf\u4f1a\u8865\u4e0a\u4e14\u4e0d\u4e0e JSONL \u91cd\u590d\u3002\u8bbe\u7f6e\u9875\u4e0d\u518d\u6709\u7528\u91cf\u5165\u53e3\uff0c\u70ed\u529b\u56fe\u4ee5\u672c\u9762\u677f\u4e3a\u51c6\u3002 Release 0.4.7: Fix the extra empty right-side rail on Windows by keeping the stable scrollbar gutter on the dashboard scroller instead of reserving it again on the root viewport. Release 0.4.7\uff1a\u4fee\u590d Windows \u53f3\u4fa7\u591a\u51fa\u7a7a\u767d\u4fa7\u680f\u7684\u95ee\u9898\uff1a\u4ec5\u5728\u4eea\u8868\u76d8\u6eda\u52a8\u5bb9\u5668\u4fdd\u7559\u7a33\u5b9a\u6eda\u52a8\u6761\u69fd\u4f4d\uff0c\u4e0d\u518d\u5728\u6839\u89c6\u53e3\u91cd\u590d\u9884\u7559\u3002 Release 0.4.6: Fix the titlebar's appearance and refresh buttons overlapping the host window-control capsule. The panel paints through the 46px band under v3 chrome, but the capsule still floats over the top-right corner, so both buttons sat under it and stopped being clickable. The titlebar now reserves the capsule's 104px corner (kept at narrow widths too, where the gutter override previously reset it), the appearance menu stays anchored to the buttons that open it, and a long title truncates instead of running under the reserve. Release 0.4.6\uff1a\u4fee\u590d\u6807\u9898\u680f\u7684\u5916\u89c2\u4e0e\u5237\u65b0\u6309\u94ae\u4e0e\u5bbf\u4e3b\u7a97\u53e3\u63a7\u5236\u80f6\u56ca\u91cd\u53e0\u7684\u95ee\u9898\u3002v3 chrome \u4e0b\u9762\u677f\u53ef\u7ed8\u5236\u5230 46px \u9876\u680f\uff0c\u4f46\u80f6\u56ca\u4ecd\u6d6e\u4e8e\u53f3\u4e0a\u89d2\uff0c\u5bfc\u81f4\u4e24\u4e2a\u6309\u94ae\u88ab\u906e\u6321\u4e14\u65e0\u6cd5\u70b9\u51fb\u3002\u6807\u9898\u680f\u73b0\u4e3a\u80f6\u56ca\u4fdd\u7559 104px \u53f3\u4fa7\u7a7a\u95f4\uff08\u7a84\u5bbd\u5ea6\u4e0b\u7684 gutter \u8986\u76d6\u6b64\u524d\u4f1a\u91cd\u7f6e\u8be5\u4fdd\u7559\u503c\uff0c\u73b0\u5df2\u4fdd\u7559\uff09\uff0c\u5916\u89c2\u83dc\u5355\u7ee7\u7eed\u5bf9\u9f50\u89e6\u53d1\u6309\u94ae\uff0c\u8fc7\u957f\u6807\u9898\u4ee5\u7701\u7565\u53f7\u622a\u65ad\u3002 Release 0.4.5: Migrates the panel to v3 paint-through chrome so top-band controls remain clickable while blank space remains draggable. Release 0.4.4: Fix the window-control capsule being drawn with default black-on-white colors on a panel's very first open \u2014 with no cached appearance, data-theme stayed unset until the host answered, so the host's one-shot color snapshot at DOMContentLoaded missed; boot now falls back to the OS palette synchronously while content stays cloaked. Also stop startAppearanceWatch from resetting the appearance fingerprint, which made every open-command run rewrite hostAppearance. Release 0.4.4\uff1a\u4fee\u590d\u9996\u6b21\u6253\u5f00\u9762\u677f\u65f6\u7a97\u53e3\u63a7\u5236\u80f6\u56ca\u914d\u8272\u9519\u8bef\u7684\u95ee\u9898\u2014\u2014\u65e0\u7f13\u5b58\u5916\u89c2\u65f6 data-theme \u672a\u8bbe\u7f6e\uff0c\u5bbf\u4e3b\u5728 DOMContentLoaded \u7684\u53d6\u8272\u5feb\u7167\u62ff\u5230\u6d4f\u89c8\u5668\u9ed8\u8ba4\u9ed1/\u767d\uff0c\u73b0\u540c\u6b65\u56de\u9000 OS \u914d\u8272\uff08\u5185\u5bb9\u4ecd\u6309 data-booting \u9690\u85cf\u81f3\u771f\u5b9e\u5916\u89c2\u5230\u8fbe\uff09\uff1b\u4fee\u590d startAppearanceWatch \u91cd\u7f6e\u5916\u89c2\u6307\u7eb9\u5bfc\u81f4\u6bcf\u6b21\u6253\u5f00\u547d\u4ee4\u90fd\u91cd\u590d\u5199\u5165 hostAppearance \u7684\u95ee\u9898\u3002 Release 0.4.3: Align the panel with PI-Desktop's cross-platform 46px host drag band and minimal three-button window-control capsule. Fixes two visible faults: elements toggled with the hidden attribute were being kept on screen by their own CSS display, so the loading skeleton stayed over real data, an empty chip strip and the custom date inputs were always drawn, and the last scan sentence lingered; and the from/to fields now always show the window the selected range covers, so picking 7D/90D/ALL keeps them in step and switching to Custom inherits what you were looking at. Includes 0.4.1's scan progress with a real file count, stale-scan recovery, visibility-based polling and debounced source watching, on top of 0.4.0's theme/language following, filters and streak layer.", "minPiDesktop": ">=0.2.9", - "shasum": "220506deb22c680074fb65856ad0b769435246a7a850184e8226f602bc9eb427", - "url": "packages/pi.token-insights-0.4.8.piplug", - "sizeBytes": 171505, + "shasum": "f252d06f15a7c8f1a5acb1ac78edf2a276fcc385cfec1a888bcf5a2bfb0b0db0", + "url": "packages/pi.token-insights-0.5.0.piplug", + "sizeBytes": 177205, "permissions": [ "ui.panel", - "agent.tool.register" + "agent.tool.register", + "usage.read" ], "fs": {} } diff --git a/packages/pi.token-insights-0.5.0.piplug b/packages/pi.token-insights-0.5.0.piplug new file mode 100644 index 0000000000000000000000000000000000000000..cdbd6997b08876654547f0a8c52ee28d9f3c18fa GIT binary patch literal 177205 zcmd44Ta#SZb?3>5nThVf^h9*TOic95(_vb+vM5v*KvEP162ySGm}mh68U>oIfnZct zWmZ)hS()X`EPy2l6OwI-lq`v|t&4T>MYd$B)fOpVbs;G|OvF4+%-g)XS-3p;E6ne| z*4q1=lbHpAvK`S30?2dDzOKFYy6?3weB{6RZ~y0q(trQd{euxYq9XcMlfrjg3)vgTBW_Z^9!zj5mijyhjFbU^1fDgVy-gu+#02 z{r6~dce1(VzsHnmUn~CMlV7&_gCPSPY*3{=DSG3<`zJPzaqGZ=U;3ruMSDB(UJe{s zEFO8}x$bEA$Rk#NGMu#gY0^DSwOfq4{qEYN+bM4J2A$yz?{jUlJ=o|n{C00Jo~Yb` z0x)*3_l7&;C|L~Gi|%jk&|tqi*qCe{baW z8BDsPag41u*xms|!<~u!yT$@^4fF@~_jov9(q8-c+N5{AH`$Gf?c#%vE#ByMuN9l! z_GGKQ{gk>NtEF+T!y41h+Rg|-Pm1+kpDv5>gz4TyjT z*L&mMY9B~UigvNlZx7bGK8SO+TE!CRruAPcE{ulP>29Ro%iZy~Hyos?joO22j9!47 zY_c@%QCGmT)$rCX+Iq)?1gFK}`eFj2=BD4B9;nb;-I>s|S6g<}5L#{Z`oMTN=%!B7 zl=ZPcWHngZkY-eky-)5~0{@1$)1?lef?ue;qJ8CBk1mw0q8%A%ox7mo@j-&Hw zTQ4^8B^EIjZJa`QZVIh0I+hRuTPFH z;}r@3asG1LGwONpfH2&slaHS*r6JQ+cWV_;I5rvD+u{mDLlD!Zx!oRg`>=-fqJ04D zSnUnKavIob19kcd67BoUaB-|#SVZSf#p}GLTw}p*HgYU7p+EIy+(_e2=thEP>r|1pX3p_^E_cNTyE+k{K+nq2K zAHV^9-R|#fz#D{gkwwIRGzbFzAZ3KV5D8Zy0}?~J~f z@IjVvbj|Spkw@Ap%ywBD4xE|+>s4_J_!@ZOy(fO9;aIcn&Tt!MUQii?wH6rA%Hm>Y zy!glph5Fse1U&5x*7`dg0Z`CrudPA#h06{wszAawc0?YU%e}4bevh5jDNcyjk0HvV z;?!UR{M}rrHZ|B0PS`|6xU@fqBNlr?KQ!*l(oj-63K|di*yzKZZIF@`90RC92h243 zKNuE+t`KYp1nW2zBaPW;O^pqTfQLZAntCNoaUkkdtV79#b(-Ui5tQD+bKn5@8S`d4 z*op-%tU6;Yn!`TCyBNWTAz<2#qT?pobb72VyJi%FIDFtz#sc90O#rzXUS5Ap zM07A&oNSJUI~$v<2$awYSGREa|mwcYAzby+35z18&h>gEYHjaU_HXD5F7lWVBdIkUD4W#@*3% zTP)D{@S(@))5{*Qy;s>?g5;}>>BMWE{5*cOfN44qOYn6P7fpD>R$;?vqpuLlo+{hD;{ zsu=H#*4svkBrf`7drc<8U3ajvjySN!u>ZCL~SI*=Q% z*T1=gET;K&ij{tEt2bEzEYBbl^wu_m+es+E=)d(4!S3)08db`Kml#m+&|F>(!Q7Ll{ji1#cG2mM{7 zt4|ac#Tq|>YO`B>g2%u?s`+e+~dpZWb_WwJ9GAUJ%2Lhy(#+R>t3F*)=nwnoWi z#yhKyK(0P+_2>(dV4~v67OTP6mUT#TFoqqp7DT>125s{rk8S#^PASBgjo?Ju<7%?V@BYN_B z@1=u9cVNtjQBS&%TgX6aO|k;AxO1AAfmsf+>{xVP5@j$cM$8gTL%fDDYqxH-uNm_0qZ!**ak+qht+<{*}c8r`7JJm!z#^Q*p zW#*ULJ*>$zFt?jvop9c3b`!&jWI72hxz3!! z2I`7$Qdd|Rpx^D9xxwvfK{ka^raO8AGJ&`{faWfcuqi0v8y?z=peQceUKKZ%^l05et2;76<$vW^TxY0hI0D01+`^P!Iv=&Je}V)krnyOdQjXi{seQ ziY8{R>ph0u=>kqb1pyYe*q&l!TX}Ic0UIZ@fKfp$w|WDaG8UwZs(IIJB+PZUg&Qu* z^hUx{4Gr6=Yl9Acc9XEvShJKixNdJ_V3K>OX|~7Le8s$`Os#pfAen-lcP`o^nbM_O z_?K-;JAjv)>QuHf5BF9JNZ=%`kj z)wXx~uww0n4XuWXxXCGWN+9Yqjfx{oix+BE?$7?xzx%)c-Pwm8(w~2Wl{?9tT{z{^ zBaa*?9x0Y(O+*Q=NTdDsuGSm4*Wu8jysBIhUP**CM71^j%^eWT^l)KCTJzCxO<3D> z2}jGdU^CF7s86<`+S~54k3}d%rojqpLR!1&KkY!WWbSjC^THWxDSsSwMPp@-eMAIG zO?a;r!FkkMXGa<1jG3H<;W~Gdy1li{BesU_B8m=7+*EsQ_9N^cq=n(2J<RF{28JnlQ-7Ai!56y+Aw1L)(=45xf3rrZXl-r|6j~0#Lsyb~Hzw#^by48J2S{szZ zi=}7UqgAzp??R9nPFjX_Y4aUV9*|V>)KJnbCbH}g#+Nq1nXU;!Qw~uLf4uMPOKXU5%s~_l{({?66_5mE*ffQboX z2OoloEv@1s66Js(3Oa-%$lndNTo{rhriidg*=}hy(|AvuKl{SD=WVdOD~bmArrzZu z(7%pv`epd09|_;=v*TL>{8r^axv@U|lc%mWZTb$`696TK4I(^oA=({9|zorS;}5!%A=R}BnP!TN)h7e#Jssy%-!+pJdC@AT&&mZJQS3ITh1%6r23N%MNU zzti>Yk#GotDVNUC;(0S;MiD_$vZjf){-xC$pGF{?bempffo*sW&4|T%sRwBX+wIPy_1>dxd{oGA@d*v2vn)46^HFf{P{UT+8qy{@D+9nHe=wjR7l^iA>+Z6s zn1*eCNb(bhcQM78Sa@p1jl?xvK(H;8VM9Y7>F%D!CU^-uC*tVVc2gEl`&9>)?7M(! z(7l0@03^y{HGr+W@1{z&Ho6mOi66zl+FZD`_`aJ4hV=+kTthBwlH zqp@=S-lY_KLBj49>I746@I3E*Xgw4@LK`8K;(BpnRnOHj0 zkzuD4M@(DoUxSn(kRB=O(uy!9V1YHCaD&S#K+55 z(sM|;)mJ7a)Cwheq4u(zblI8-5?i1I)?2HQKvr zipY2wQ6J&Y;4IB#ynL;Plh88J&4nKQ{9&+EtAGk z3T$iR8g+G=q+*iCbD@WQb1ft`oj#Psy;XBlAGMLRJQk-cp>CjaJ}4E8si`v!y|ukF z-b7Nn)@2XtAK-FCY`0P__NZ#7hG)QQ>S%%bf{z;Q$=6c4u8i#FvJ-cfQbO3mDw;0+ zqm8P@idpEehArFrtq3%-@PsmjCjs&$KOU|Vbj@A2=$X-Ds(}lk!mR+Y@vtvPn^|An zK1f5&(vs<+TIQ=-?``a`+t`oZo-EYxlh8vYq?%}fx`6|%jTuF;Tp`7~VZ`m3Zt%t~ zG~8}uk0pG;+mmf%xC2-=RoQHfGG|~u)chswXAZKu)9ZJfemLwPhua$27?eReuHkGQ zZWM^3t*H-i)h2d&6TkV#*D!Ef#!*0@AUCIx{OLZ;Xk9fzBcwt8yc_!hqGx(WsgPCE zXe%EV#PY>qos*b|oA!>V8h1S6Q^5TSBx<}m#ky$}R^#=dN|w?*_7kAXa=T}w6H-`)r&rZJbrP@q-Jq?HSJ`FBCr=?2IXD~k}Xc*oKl!8j7Kz=_3p9?{+n`K#}0Oe!i ztQEj5<@idvk@ZjEhvtGDV*o_cZLU1+5P#;#)4k3!D+^DdFv`sDMm?l`&AG!a z2Q@(1F<2wq%Jzt>Kx~p`Q!qgo7k9bxSIe0|n7)TFVEQLay(~AiTYOAxJUT6#*ABS#u^sFX zhu(KnLvmHMvQuI%X{-2S!jB-Dv|8eaFTfG`LD0TztVq~TV)*eP=dEN-+M35I`QAq? z@gYJd3@>ke;(a$o5cF@%+J?(};*MWjtv@)_ho*<9BT&S-pslKx6v){8w*pMOyL_UR^p`8e z2aXnpQyKqeYLMPJgtG6pXT{46Nnz<;>9yl zlEK2%^K9E@LBbY|dXI$M8{qMf^a~-8Z+!#BikI#UG$Zv>Y&oK^5XuP$Q(!8AWk{Js zMi2#IzBOx10p^op1?(msMh+i~ts4;Yx6!j`9TXa;eZE8803JMDgfrl3b|6Ges>)?+rqhw+v>SR1&@m9o{+<5)NNV;y-%#rM?xrXR_Tt9M(y3E zjV)+mV28g#vpj{5zS%^y88IG0%X^Z!t=F4rYpK1C>7j9#gvudf@gk=!H=w*)ha zco)=-=B-h0Mq~DxUc)JvD#fpsAXqKo5&{MM$ISOcT+~MXJyWV8&!(Rg@%`Mq3l*>YL`AVQvGGGRbEs*nMd-n)9rE#^=3k08C!Jy|iLCvk1^?3+cs73+cmj3jsp(5FilnY1QEx!Bbe$s%4z2WP4(!AoWuDbuMUu zq}Vdu)f4lxm&LJTMFTegU$&b3Gdw#ami9z@oXwV|KiUe*1!QuNqTO@^51W*XM!4D+ z!<%X`AQtsA=@JKI2Udu@AQURjPe{nOQNYcNJ%pbx8(8vjk7fF3+E&A}Y)h5)lk*Rq z0n6Bf_gQc{D8tzK^%}i1nb1jN;w{NuSL*sn>%Bn~s`=uiA2R~S&9QUGKu7S@@v#Q1 z#N1L#fQZqhW&#((q&LmaN{)LoQvwa!N7LGDkDIY{Ha%y93sbALK+q6JMF=8mdV5Z@ zM6}bd*a{T$H(?r>-XYLuWD^ubAUuVg9=0$}$cpA*oU+0*v0o*f(6P1Nj=xLIq^Idt zauXSq>#gM1siDe?>9$&VM0HHQsCY%Z(1gz|+Oa3!4|aBdGjxjak_L%G8gHiQ#qPj# zgJKHyGn3*p4qHps0%q($VSAS_*l;r51oTT>bo91}FKA{GHL%?WN}`P=$W~ru2-FHD z@uBuoY@zbP9WqHON|MdH8u(ZYxIIb#qS{6k$MM(Br zsboI5|7e6<x-rk@<>jR@H*B z-eoy^F;lEufU;zIH&ZyTLRmIm%@noymBn~;S*0#U0RTu;6T=c-DAcUds^PBy9u@&v z5PYs~IB>!qCEN^9772SntxVSZ%VPOe=mOE2y*71!s;JusYOQ81zbu+x)mt_FzAWWt z0B=iJQpQ9{J@%2{`7xkmJ4#tSTUOLU9iDz($AZ4HYcX*?TuHK*A94Rk@__H_Xs(c5 zX&)ygO)e4=p$FN!^)%1TEmz*J>ex`z22)$qSZ@0e=6nC{u<-Y3dizWmzqDG%pNL5T zV}ViC*a`m15zXD0{z2G>Im0W)Y-H<*9b}&sa#$}w+@V)@Wv*IZ06-SG85^!%FCnX% z^@*i1C)go0Ra*8kvYyo!Q%6{SRdXGC5F31j@YdC7l}oBIN^ncOfXLEm)%Y^v)Ipya zo7DUTG3r8sWEPnDoh6b&(Q&CeH<;KI>jiw)j#WtqY&m74<02nJ%-9%J74;Y8K}Zi|t(__Usme{+(HV0=yxE21s@ z%&0k>_CR-N$NA+s?qMIH`^R?2O`pw4SfV0>vsv0DJVq@uP+7*ttFsk25|;tTvFf){ zV5?MMHL{+YtZJNqmF96XWgoCuj7TtHzYTKd32m`p5IS|a?2>|u(29w{=_ZlT6j|4S~iB;ztErTq5;^_9eqc^cO7a%qNbo62>n1&*$jHDr=t`Px&*dUJ~@{)NFO4{?cHOhTuLI|2r6cLfl{{ zIVR?KzrSc$QXUMkOe+wGbgn?O!Qyz*F_Dyhh2W7eu|HCrtfpG2q*v*1qFG08{d6kk zeB9kB%Ze$OGNfR~FO#c?Le>IL^2ov{k`~S}$2)L)!?`j(sHpRK15+KJA|FoC3&%HtTL zscw)e+B^6n{Omp>Cysn(s5OF-1m!>%Th}9%*(Sjv=9C`&P?*k%`BXjl4H^|ME>C?R zu=q;!o0`zz#rs}Lo~nZCHE8J@q!{;)omK>Vm6&*&J1Ic&0%JWvhqL2;-Ym_PUx~i8 z8gK(7PC;1z1*^2@nh{BF9x2KrG$ImqdQnGZbgV8+tyazpuOvRP!jMUkgCSM5W>Oa9 zq!p3&Dxai-kgRsu*cKnbVJuZnDZ)gXn%0y<8uzt=;|9Y4$*PF^@!{YTun-EuQ%I5q z{qZzLK0Y_gN;2yAunK7l^W;5229i-mDpRJE?T1Ki3DDV%W;IQ=x<#Zjn^ z6>N%^nf{>d$-bYeCi8#LBA<>k0!CJgd#;XRAA0W9rE`G=M+^6sC~TZVJHFyaibo$O zwl+unIMXq?FKHV*$d_B~E@d#=r^ye!_ZC-oml-8O^bAk(IjruUQ+U+=pD(0S?-YkO@!)nh7crg0;EZCXg-FDAU#t;493w&Y=_m;?x=?RJqSM)I=mY{l_)laFOpeR zRUr&D7u9U=U!`_(Hl^Cnq}u7j{My=D$#SNu4_9B94C<^rnbPjliksESM)ota-KU9U zUK^LKtbt5XVV08{FMhh!-jow}agzrK45;fd7_^_;O+C9j!Mi3aRz zKvIhm#=rtmH9NXv9hAd%@)NS59?w3hj^U|ahfHmGOAIB9dLmq2Q% z;$F2+qob8BZP%I2nI8RP*`a^DPk&K+zwTnr1x+UcUFo{qmOkHHq#Qsla-T8eG8LN| zvriIX-hovp1T&}%CiZ87L@H1YB^9{ONK%m+_|1w%N)n?9+1ASTjnZ%&GnY6XMzI>N zDIcPsfz|OHk?`7cY%1FGeUne?Dsiil)>sG>vPTNivNy2*OtSITRo)dwelcm>wn1q$M+bkG-b7^B%fYQ8q5~ELA{F#qG4I|3J8=N*^qU;#3kr zn{ild^`RO8Scitb256);cy)|UR;sv_K@#1VMa+IDa_1n5RAY?wW|XX)iIB}q@nn@( z300kcr>9^rzRfWL$8-AZn3A513=_s;7BW%f5;ideEYTL=H3ulOGtIGk)Vfdfo&Ypu z&2=-bJ}l(@XoJJfAcd5%r7=71rKiA&^q3HvYyrM=>>sqR^r5C^kq(B95NJ8KN_?BD z6l=hLG$rtkoR_R6&}@6%&-un%6V zGR{Qur-zz2$ODWvH&Gk#g9CZL`VRu;p#0OLUv>?pGWbM3Qk0O>p`;_EW@rQ?4#aygXb{{EbXX`cVh-U4DR#3&$MuTP>k*-1tky%6R z2Zzejd4aTlVmfD)xljVZ;YTxGT7)OmKkW}Bx$)(56}wHThA&(Z(v_5%E{y<6&QCqb zN^v2X_m!)}y%&BE`KIdpBbuDsIH);wF<65s1R-E zY5Ae~@&J0h?!m%(_iP09hLXwXwAH9^A#+Ch7}AbseKFzd2sAXV;t7}8ImvrJNXj-RywJ=Xl{AJo`kMx_^CmFP`YzibI*m^DrZ;u90F^Qk0>e( zvkx_@QjalqoQlKj)rYHRo z(0=NGO!xH+V%xv}+F!qP;-QE1M;XMF)|ixL?M|6N>>^K|(M{QC$@hgnx;!QwISV~E ztEc$0j8n=~b&15bmc6waxkz}pGTfmy+o2NA>P@2aBdTFuiOzEj;<)8+mp7W1GBQ{4 zk1knnp1)b2*l3vYXcz;T5}UAZ7qvS4TZ%Bvu+N_!f3)OB=3v$%)p5!Bp}^ zPbFZ(abt(s)gtA#xW+)|ddN~n3P)K2tjEXb#oXa)@GTx51q%v+r7i zSn82l$)<(zS11xlp=4;}owhhtCE_B%+I0>W#d?zR(-JY*0g_9~n+w}qUnkKO(HxdU z8;Aj&waJ3z)|LGq<7djKYjriRqNpeG&8bAI3sT8jYoXYY!qys=&xS0Mijs;9?>Jc0 z?gRXnP^Oa2)@mska7sF0SoFXxN|FK+dA6Q(*g=p|qAr>*d`vvb12^klD11NQvz=)r zU_?Ont3`;vv(-O-;mll+&P%t8SxX3Gu#3chYrX-w@sKb^Er8SAR) z>Ovs1W4RzTP%Tr}*-^B~kgyr$s~m7G#Uqt$yKF1Vp9Xdb^CF=ox$cw)#PiH6N3Z#{ z@kEeDL0C^|v;mUV38R+|a2JEJ+}&i85vxC5-W{yTZ`bZ1*L3{}M4spusyUWSa^;eP zt=SmhDmbz})(PkkHPqL10S!P!4(Oc1xwz&)u+strnKUpiu0ce(WPrO7n!fcG$DL~= z5><{E) z5;hUWcxJrZUhg)Uk-L^96+f)`A)M;p76gNX9XWKz1Y_}j6?}F_u}-bk-AVVXT@j>q z$osW3S%0#zKvdwINzZR->3`?4210l~wz|}7=@5vGXrHoAe{AX4$HtG?{T9{Y8uQZA z4=*k*y=xP{(Z1gHgqAdPWTp|QIP_LAkbM4Fv-*8@Oak)_G7z7({57DJ`RXd66K1DT z+<@R|55$W!m$0*atV#RL%kOVoIkwQe{Oj*;EL?eH;bVu@>B7=RFUPc0nlHTzwp2^M zj`Y*cGqBb#KZ@nfmu+^m@=B|w0PKT_k*GguN(6)@Ek&A8+4XE&A^J^5$|iv@sExDo zv8#zkrpgo0JTM7(1H@;Zllwh=b=txQ=nn?stHT5OeGqO^G?vh?2mxPMIB~jU*v} z`lLG`ScZf-ywj3!xq>AtXFHHb+DLH*l?1HkSe%oq&CaUmu!Bo`!iS^Dl)&?MGo;sF zdxnZz^PrM+m4v+ydyQkQlK~A8{fwZgblGnJSAo}FvsN*NF11qZV9;dv@mQI6 z8B!H>*7}Hl|6!-sMaZJrY2@jw9&1?<_axUFUV>v%U9l68d)9?jkN`-uo6~#j@C1}X z9aY4_yKdF+R)b#-A@J9F?Pa_2ptdfKyq#56V0PuSR7O-Z?gX(&XH$aGo4}t!8=Y0d zc+3dUOFOLHSUz?3)QL-e!tJWAXedr!Jb$jZcD;DeKan;!H3TipX>&0;d(zEPJ&JQ-I`c%QX7p>_vLN8LK(cy} z3JieE%`}q|IaLG`ZDPa{{xNwE&L8-hHW9Y#oMFi}o*T2v1HZm8Q*pzndIfuhR54pc zprkoAn#w2vh6Z}M*HKmhz4Eq2@iR~LGPYahaS5N|d);iOftjusFwBMH2|$TS=A=M1 z4y%OKikfqEn|gLn8o1vUE^}Yk#HbEft<<;*%TISv*|i1Y;AXB+E8@#2HEY9d5_G_7 z?d~qS>JRF_>YGC*7d0e78`1s})ov(h^>520@pZbCnB)IR_0zZa1|S}c9+=H*|%dF#p}jii+~BvA9G zarxI9y!NjjyS22TRg+l2wH0VKNh7JnL`mGTAY=ZG{=AOP*E%|sdjzq9w=|?(uMI5` zz~wOPr{TL`STIXiOCVDXYYc5Gu|JM#mHm+gclA_C?mqi0oLA)=;i`iuv5hMKaq0X} zIz*+y+67Vc{w#;D4e({F+W?<}{g8elfay*6Bw-~}(^Lv`L<$P&<{SRSP~~$Axx$Xm znC&Pbvq={kpaaGKHtJ_xmV|~854Wx! zS-kRrg~rkb;o#fDobEHNO~TcE`beKMDmSC)#Va=-J$U%gt$8gAHa>8ajMPF2CgHi` zeb})KQd`|)t;>h6lr}A;mUHWteJI@z9R%r7Jny^yi#@c2A0;%h^bW) zVT2!frcdTOAspQDA(0CyZUXWC%2Ia4T{7)*%Ly(c@?x8GuAo}As9r4sf(^l?f=EJy zRIfvqb-f>aLI*j_Qp{1I8A@zQtS{mvZT&3GCzokh2{8WydHG|Gn#+qYfq_AC-K?rY9Dw|`(_(^> z=#K2}nCz$aT_c_YYX7(&pgUb|>C=@s980rS0@C(ip*TU%?1h2crC5W!y*%TYcCG8w z+&%z~O*0-}0626xk4jgemzwkC$`iz7XAM9(AFe1N@p^WuT30^Smy{Paf{M@-4X|pC z-q_E`!%XJ!rC)tXGn%#wCR~YL`nnwI0L`C-*N0l#FKKuw1(jwIsOz$6Ad%(!sgF-k6zZ6OQ(=Ia=$6Z=BGuxCf@!dtru8NJgbmwf zo?p3l1#5ib!qHefHsX6-ipaCG8#{i{=~a5vn4Ia9?E9h{X^ll<<>AT+^A@vc&Zm)0 zT|Sh*GIW2jNU_!Djc>~Td4os|)F#;u+IGK=S8nE;I?)Ui8e_SDBRN05wd%#l*qx*k zxl3!a?9)-E~_=B ztQuaN`^dvCg6Z1p-dg0PB~Oix<08))`$=loJ&>~7Wez4Api2eb1{{sdUfXEK5+N>o zM}XRUmxStfkD@)z9 znuO~l(}Qcj)t;oUPXqM|fYCvqW;is+R8g4)-w4e$N#VZZqU9KsR1o%Y!ZW9CieGnw zhz;@>E+*rnY0Rl~DP{_=Dc9d6BcrJW=j?e%xvHU;6{T)4(F^5E&OGR5w6cdv3qg#^ z(G-Ptx~%-pA2jzA+*p01rPS3Icp4&Wef29Feye*SOU@Qh7J_-T7+P|^YSF4dDUkk& z60s}+7@Q&mM3IM>>&zZStjCIzXKr@(;puPM>b`_p4OO%wmbB+Ik6Yq#5lbma`m~BW z-*Z{fL5KCagqbtK>bU3Y4&sl&D~RDNL0lb~hxN#O92p#gedJ|L%54q-)Br1kDI3eN zfm|o&egW5yH0Sbto@A|bWfF7700|_>Hb*Ta zv$0_M!t)nT9Y1;e*|VpvE}yz|>CE%Lx@G7s7n z<0;7ZOhbIB&xyljbmX2SU_!PKr z;-a@vmN5}FjSex-H%X}lXJb+OMjOb@A3n~)^V+?~?4pj9uoxra>b z!cd9zWFl)~PRW0xJsL3jWX3;_O(e!0lcB?k(QG~hEU;iTFC%d99aCIl=xBmxI@j z6i*yVe<{K@1qDUNc9>p~g=q`h#nqrIX@SR{6U4$bV}(HE4*fq>JQ+h67yt0tS9sEu zY#t)yBALdcs3afLGGygR?^%;iGbIAb499ETULTUut-#2`PJw8qRlE1$BzMCPI+txXja?D9M_sv8|f$c(I zX!}CMt)yD?p-i`};P|#5>&o_$O!=#$pkOxO!uK0dxvOBe-_9&=P$fnn9Lp~nhEC&; zqz&e*+l^Vt4K6ch-80>vN9sjs6Jnr$VLs9iaff|E06q|pj>!O^B*ujs~K-v%Hz@z|NK?)V3){2z5Go2O5r0D#e zooyw{x12nAFIc>=%E?d1L2Y5RKr1_an{XchsK$Id^;*DqiDJ3JbQ0tVj?~x1@=gDo}?Z33THEs;AIO&2QL zSGbv==VKdlt)h74oRkY96qDgOS*}VsTpQcGb^7Wg#|+UFI`mZS#nZ(UiK~BBOkHAf zu%$D~Xr{1T&O>#!2@eSJZYvEN3wESMQ%*@1G;IZFBzu5dDZ-v@){6gpZt@4%@NE`c zyk27g7_*&MX(o|pr_?&b$c?Gv%-}`r!4`UBL57^RA7-)GJMyfaCKy_mx}WkK?5VF) zR7b4e3L(y7Hq&15j#m_H5=oMDgNSsIe6WnTQd5lcVJkkbYbO>hCla1#%FC9Mj3U*= zFty+u;j>1!s!&Yh%*f!2p^QlzWAY`3Fm-=saRt+Qh;Fi@MUzw)qAD*K<0Gg!G%V8^N4wRm^>7;$m*7qS36_ zi7!lwa2>>OwwO_qoh{*{ey)q&h%qCvL2wu`Hc18MBqry{K*a~8{OIDBvC~^v?6f&x ziorP3NJ_%KXi;GkI(L|i+E|NjRw$W=8u&qEO2vs}Q!`_QLi6-5)0a;BTlsa*$tqd9 zdOCfBrPzw;FUROa)dpI6yfGpOejdyE^g@xY7{ckL{CTFBsm>P>vKW1o%&+Db_N2x2 z()MnqIN}$&zPNTSonKW7+qUyLVY_@d2CkoZB$mL&p1gN`%uOZHHX9}#02bG)+po)Y zF>qOu#p6}CYUtkPSgn$>^1V~Dh$GiR*qCeIAD&*}+<)o2NS4!@`E4Ju!jLTYa6~qS zBy%Wu+5_^>bZ&)goO^T2h@Bgks8Y_y^S%dZp1M}gE2Yr6se0vWqw1+@dW|R8#yW|t z&%QkXkPX?#)BQ(o8F3tQUsCa-RKVI<>-w~D_zu;c@7>%J%ij^BP47->(nMyzh@&SbK>$!4Akku&BZ z3aLFgDMT0Dv?$kXC58=skffLQH6u+CIbbozPBX3MS4iG}&64pPPflRD;CvCQcSs*N zUXY){gs)=+HT30V##1 zSpnAI1a+Q`)^#{AKK~(Hzr;K)PDy5G71U|@w~Xw3}$ zVqgrK>~MCh%4za_83@Cy#AeEjDWR<%srSL}lD-zSpVUzsH^|Lc{WeR@^-Sy3s~w?RJj+F)KKvGZl^sO{AEl8Ho>Mr%Ylw_1B!X*P>V zkeNXtBj{j8o-;@Iz zOXlMVm6MUOz(pgxRi310qVmfxMYz9Q;0xXf*Bx{%ug?~n| zXp%JNi(31CaUpE~?{2tE-=9OmDdS<%+Y7a0K6-ogs?H53FEJ{$ox%3=&HC=+T&^6C zgW0AJ(~-9MeRpy~Atpy6A#UP0hLXaepjc2CRpBq>?Rtiev;Xh^^KTw|?xBbD=U$zgkE71yJ3soN>DR6cS=*oaTllhSm+MrGD0`{l_yh0WN#7#ex9r(of>S^IG? z$F*|Rtts4#*jKRy5%z4(fUWKWClZl2WEt^I$w>I`#%7nnDZ#e6Nzght@FTT7AItHP zQVZKY(Pv6$-~dUJhb$G5aBa8Ant zf(HUkNWc*QeIVww);=SAJgpJ=8r<8*Ow5ULLuZ1p-R?}v^F|iv0oXWRoolA-3tpR6 zL2$Q6;Z?mc99?r&>OpcPIMcj|qecgr@1JzxCIn=wi>H20xnU(agZ3D=6mN7d^iI0R z!K7Hr&rW6x?KAs)(F8&Z&eSTN=(o8Z?gXhGPLlLtZ8GGhUy9l%UcNN*R>!L67QfKD zluu9AYn7SL)bwQNoPbRaDahF-T1#%vwE-TUKAP5}AgPQz;ddv`1J>^CcqkK;RLfye)ig5g6o- zE}lAj{L-0UI|Wdde!cmz&IcAa;?wMW;>IVoKC!#-z9mX3xxjN~!=F@+!-Lx8He(Ia zNjwr|fYoc)pj2)|w!LJ#0ar-Mkm0Lx{KAD($1fg#{=})P7tWtO>puBMuO|Pyw(7Dd z0UOow{+pE|6T;;DWH+(R))^e}Ch~Fytz=NKF&RQ-+oyeO&a_N-*4iDrJBYTpiW^!E z)$ehRZidrbbxwjIis%av)UL6Hiw*tp-Hl~ca3FfdAu)xY7?t$sME0(4EcUy2h)r1mY-aKigxAR>fa{bF zH9aH=$s7>?6)sHJASAiMXS1{PwS|xDQ&HEK= zSyrLk1{?*Rf-2tE`aj?99~T^8r<`(ZCyA3tQc=TbI=!vRq0Q_(bgtU6wBC^@M8T~Q z&Ql3F!s$Qo95Bcp^1j$|gSr(`VUkxyd&9A*A(pLZZR-Oq8*600QL05!zRwLnqb4V7RuLZq6Caa0x(F5p=t^ohvG>E2QK2V(#v7q z^|th8pJwz)t(4I9R&)+u%)Zu8dO5!>lSi$sO+;V)Pl~GUy$CBFGX-B1{BfohplxUXih>tVsmZzsD=pNAt^z z!*78~cdR&Ibn4#wzu+v#QVCqPCD8Ea3Pt$Ypl2m|DE8WUWGtR)p2(eMVJhlXhcmCD zMmST)hdrnG`DuABdPy^!8l0cPChkGIE(bf!Mjr_L;5zCPT0vqF_l_A3*Po6H!E~rm-%Y8g)CasX}RF# zcID{Y{*pBDo`DzEQJ7W@ab%$Imle)pBiGgy zWz7XX`R}!Ou9m@MB&XFyGV|D@gz>=yKZ}M_vr`_bgu)lQrqYj^(M6RqM2ND-c@yJu zNj92F#1-2Me*rH%=3A^@JU#L8L_505po~W!Ow)nkDUUvR%K3;$6z3bX8eyZd3YMdT zGF;^8Z9iR1OOMG6SW*4b=(n+7jK((2UhbcN@h>V8Q)M6}d=#Yi)A}yNMsYC4fXp3w-%+{JDqQ;;~E=L0H z5>^XxiVk-)wE{w8d+AduxP7TNX)21;woI6S$driBcR&7+bMJe_mgx0n~k+c=>bOpOr|5|_mf|i4unSHl7mjK0Uo?r zea;HQz>sF`4JQdq3T!xFVuG`9cm{OSk*f-T(!{vDs`}dCR)x-7?x@>T;rOOn&gGAG zJW_}zKR{&y^5XTAH%?FLu|#Vhn>Q3e%?q&8*_l)4&V>(w^_^ z`5A|D54gJkVTu6SBw&}y+I^|S*?QWY(%CsvK#OvrX3Y!%Yl@doxiD%qfJ@}QVHnOk z_6bI%WJnpH;Yb^a!_)B+)IQiw~JyMrzn;(Yh!^>JCtPUD^)j+RYp zB$0OAYyzMJ915gqV_ofaj4aimW66MIW$$>y6$YahPB3h!EeRS^;i1s0JTT2Hh>Dd@ z`ha)^2t;MmqGJb=R}(4{hDJ=WeKC51c%^m1HdGur5`uz)&e3#y%6gDFHmT!!?hlF3 z;0)+B3)s5RcB7@o3Y(&7_E_J3@EZ)R9tyTK#V0~7_3Z(+Aj+{aI$%#?l{oPseyw3i z7fiI+#;F&YeELYurVFQ#s3|^>p+%u-ZotM^4#Q*0GbV5dc^7dO^wxJ5o!OQ54w82W zA;;J$Ow+cSpx1N1It_|D!;CQ+^fop*b#z>?A$c&+gly|HkkY_*b}WffVy5hBlH_)! zmC81N3qq%D`Hu!omG6v(*j-r@r+H!#YEJoDS%NMt+Ty^;Y%xV0Ico9CMkYC*#^pQK zoN3J%R7F@gNa_a?)Q>)J?cWZ)CtY2yecw0?fU#4-3WzZ}=ho861J%1egq9$6x`t<6 z>$NV>N&tszYWbFBzqY7si$fqLJ1r_SJg=Q)#05nTmYf!|+pQFZK+BeNG7e|v^F@{K zumMnhcjowJbp{o9XtXXZI8;i`=U*H!rd!)piU3XsS^hT%nCYW|v#GklMErw9#rTz& zi#O?gB|bWT8MIYmDHh6w^z}*uBN17o*h&lOUradRG)W3n4qHQQtzvAy$cmYGy03^= z6PWUPyW=SlL!^(Ks5IeZqUAp7xy*J(1kK={YayB@4i5$qu82@bu+g!&x5LT0ma+=2 zyt=$js4haYTe&!d$p37!QqrVpJ674Uw|$N@0S1WR6xHtr_GM8|iPB!G!fI-oO0}NT z;_{8T039zOM%N!L#se;i-;~8qv=TQ_KJ44>JXMHL87QHB{A}`c7+=w+bPn`q+NfMW6 z!o|_>HX8GbQ@G|bP*OMf9W`2{cr5;Aif=PKjm%|#&Z(e|szKQj<-Ec8?OCwGujt}P zwr_-3Bc~{K^^;-vmYbRJN?n%Zr}peniFj=a)r>Ud*7ur>8DJ(v8G9udd*Z?iR$Gw4 z^h!SBl=(_+jX9~yHD&y)B*ZW>=W?V$)lE9iVstL7`pK3BQD4lLQ*?x{01>)9Zy`ON z@pT*xBqlXWEzOh3$jE%HWwi317+9wekVugHlrunD{g{)w2*r^v1WcQX5-_3UgvYTB z@xvfg_pbLiz3OYu8o5YiR3P~mVqMLxvbr-S17!Rs3gFRmv3TJTaj(TwU;>jiYcW*T znlz?sa=X#jN?HJikQ0=uuopW1GPrX((^nF?yYr^%;_!{5p_%Myq)9RAX+tCk`{0e~ z3ZCYJ;H(J*ZlvEJX43%eAIesdHgfrzpydsk*i@5@8&;kyMf2sGYRkTN%QvMw@!rQ- z8zt+RCRG)$eaN+vW#C8&pb~V`19TvlY*d%lt?P);=j&R-02Aoz?IFG~v5 z1#3ysUO(1K@vc?kYhd8kfZ>4e4I|r-dk0r3j;0YuX>80p<4wnu?kM`KYs9wMj^l(T zz!c629oQLIZOee3eK^H$=xn;{{BY#<&SZ$!+XD@)%Gfl4=U^n$<%J(!9L`;FjapRb80Ya|1D&*a+2&lf*cx7Qd9Csac2rH_ zGV@)DKxSTg7Lb|Ot`NYtmDv-;jwnj)^!t}1!x&i~>;er{IHc0b!GhXp1?q4Iq_`qy}P*}#jLt@@&CS51Gl z4PLfqyn$guUe724KHlh>cIrB`TTjcT9I@`h4j^Zsh&XJM{_DrCeLAR95)YfO|kzU+`{X&w`8@ZcEl~D9>~I5E4RZ{K9tIzb*0Mi>=2pt z1mqxfsWEyQ@GMgY2Bn`xJ#*N5j_ z=13MTSzHuhBh!YAmc2KQvOeE_KGJn7lEpR!$*}CGK%MAC818J~Oh&*#HL^y130m4oOxwl(tQc;ozm_n>!m*`J=Qk-flACtyr7QP@^eM^}6?s@R+^TJ-jF_>BvR2d* zu$cB|o2QXX0t5!OiCR<^BK)M*6Zd%#q-cuvRGf;rd)yea+CfU9hglm*_->dWCdnhvJsf?r^d<3p&FHN?64+g z%a((&vcj~ox_q6vkA!(QjcBynCErxZ* z)x9_zb%U{1Ip&fnwTB@qZcv;6iDzru7y8^d0+ukctk|GdOpVcrW6K01B`rLGI(Axp zt!yu=JA@j0=uY(}J6s<4Vickc>t~=UUN{d%*&Z z@y>7yXPb9W5-_uU(zU0t;0~dj&mu%i1A>)WkmP(Aag?C=rg-s!1$B>LJq(fCkqi@{UJH`z#YHm`7&}u z^ml8#az(?d@=$L9eJgH-w&%F5x!)N8P5K%iNa*SGtWoptiXM%MH#N}8`DD$S(< zL;whh^dNHN8nSgHKZOMy>zlnvxa=l>@Ul`M`~YeHnwK6Et-+vv>2{@(x61X|@uPa5 zHtfqZ@LSaoO3V-tX`< z@?f&}-83iw>a#j`#?6P4zA()V7GGFyEZhP}WXt5TTl`G#yO{*rDco>14~<%ZZ~KiZt+ z7|G&h>ZHKpTge!2rv+w;nW?|a>WpbzBT&}2-ElcApiHX1>Y$+Oyiuks9dNs4<;!;C zYa&~hbfiY&mNTP{HliJ~k@%IS8Fef@!TEue7MoUvzg^I{JE9FsHroKe=9@&Goar(? zq_iuk@$#y8=dzhxH5QdW_Ighq51b^uAx>2tk<-D%kr`t>S6d=*9Lb+kqZ~%=2%+PU zCh>t7^K6k@zZ?M08||004Y#>Di+LrA7iFNT&7fhiQ}Ptj>#g>76El+2Fh=>lo1#fj zv0GZWv=vG^RqhopY1HV}FCUDGCB6lgEv%HJ`7&_Bho`faemN5XCMM52eP~<>DwwBs zRbCoJ6G9O{vGKm~x3~OgB}VUblvGr#Rh)BrYWlULo#anqPa^HO8brpys|GJrEWi`G zkyG<}{%rGJbnPZCDD?eD{eYi$3r8 z@|anP%c#VsD1f+J8eCbtkxpPs$#l&XB}aEHIbtaeU$F}Md}E|jW9e!pKcUz5_Qt)J z7TsxVPRkSsV9v`3ym`---u+F`hSfQqhaIhWzA+b|=n?|dddKu0B&=a7Qc&X>P; z@8wtR*PDNG_w{c#t~kQr?d)!F)|LJ*j5xI)+%EdXgO4rMCERFf{IhJP}mnJqqJ*(Gd8foTd|PPlf}5aenQwpdYAmNoJ$ zPcRX2>fZiqh!H^lQfdJ6O%=z_ESl{pQ+>BJfZ!u;pmw2-h%BPwbzT#vfGbOpAIT2O zm>y+jSSD9|Aaw_GFzZENF}kHUp)My{ zfrL(s?`*}pF^IC&PB|V30Yb&s+S4)3-ai6v=Oj7`0Jcc-$2VNShrKc zYPsHJb*4MAdX~K(=qReD-fXf3`~oefs{1z{wvKlj@@cHCi(>08)fE;Xz0Y0~s?BVz z8d{lhU4fG;wKG_JuKiMPi{zq7M@K|uAviYQG>uK%5h&qgcoN$nZZS35McgiJy^fUKptVkZCL7N5 zi3?!Lyba}ZxuTNt%JVl~1?0C(2VTl^ZbFz&LKd_E)f~~f})no zcIO_1_0K8KMS3#%A%Ti`ex_o3gYzzusk!1fpTK~Zp(5Bvj8Ulw$U{?_fd*d6XDcnDD-EG^~9 z{DXxrIHzzr1X}9vmn`}yEh%auC@a&c8YGZ{qn-TXXuCvrMr=oUuDj9riigBXzw!X*n=@ho2OnU1`98mVX2H`+!#K z+<0$*w6v@yAN~UHixbO9K(m!W0=z$b1b_gLG8ZA6o4pG8t_qp4L&9d{SG7Dp1ha)2 z7ui^m<*779SOBtG!2ZzoW02Z_i+bNG&ZJvp^iKDjSj@MHiOMR2(29|r+}n0A4Weba z`;=S&qa6MkP6+XdE3e^wE^tEEiMqx8wyt=V6i$2Qq)(!Kjc=H$A`KD*4<4@**>0)| z*4X&?=HiLx_g`Aq)b?Ka%4eS7bZe^)`{v$PKBM1X`|j;;{mJcDzPk76 zzq$A2*YCdZg*!j^=DkmTmKOGY^7ngx^_4r{`RU!C|M8tSfAG%RKWA`z-}z6szw$%+ zy7%(u?|kof@BG>C{g+SvRyotDW#4=C`*+`XbMMu!`jq!R{o7Q&{e{=x`SF+cUi;lU zZ@%)*kALcA=KFj zb*`jBJu&4B&EAWq`~w=r6(iFgY1tocsH=-ynMK%9VRZM1#TAsJl2aKnWx|*=1j0Q* z<0RyXLZ;SD!xcBH+%biT$Fmy{Z1tNvB>2(ps5EZULkb=s?2~NG99{8SRE_)^5ki&4 zYRI%K{B}6dwGY@&WJz3?5zOt=3`OQ6s5R`zlN86-t^$9zxC&iP-CfL;)JvD{sT^6T z-GVLPDgqQ^73u0ZELvHKe|+m36-YT+-~Q>B?*9B!cV2sQ@0A~dzI$)Is*NqVb_(!& z-~GKiKmCh;eCw5a-}>a<>py?z$8Y+cy!*y?1>}Ex>zjLj{=?gU_(eZ8tFiEx3o3E<-N~*;m$YzRvX6Zmr&*N-j_aq`)hyy&d*=p`_32czWv7DD}Q?L>)!$S z@4oRz@4WeiyMOkdu_5h!`nwEn@6|tN8{K(}UHIqpdi!gC$`E{Xe7*g(*Sz(+FMogU zbKl$h<{#ht)*z^R~K^0qo2-tG_ zbDwiy?)~VmMWSB$67bynlF@#XiwYiCnA@L&BYd~Y0q(y2mwRu21rj^u`XB#2jDOtnD&IAEY zHEO63CHPXKGTggshvqOAwl^Tssh(tQE}QD!P@awYqI`%&@ja5qwD;=Yyz{fy_P+Ct zy_bJ_=Rf`Q&ez}O@%Eo;8{hld>-YW)e*BGh-u(35x4xIx=k}Z5(AIeUCwpK29{lId zSH6Dddw;X{`ZxUj?LYeV?ble7FXT;2-P?cqle@2d1`zv7-21cNq1H4&pC|!pHfBHq z?JNU_O=?Mp7q}B0$25Wua(bUK38=QO+|>f6L+p{7xSWV&+GJrt^>{r9)6neN=4B;A2o3`sBO6F;z`9-Gm7a$*fTBKQo^`x^BIE7t zKYbaTbo|`=&R6fg_FL@oy;pv8`>Q`h@Voc>$XQ=>nsev-UjyH6fAO`wSN{I)Z~y(? zH$Nxbd-YE}2H($B)TR+)* z`As@5r}4!fLS3R++e@*ezAxF4_rCVGaLqg4|DCbsY75 zhMf{1+iqJ-o6UEXHiveM?YP*fmh3>JEbj!RH2`^5;dfk(N2*_8h(u8`C6JJ8!*@$5 zraazyyrd%Y$v|v2s*@zv!bG`rR5oYzvUUw!Y>`e;lra%YjM55=C`3X;tK3aqjK~niM(43H?Bz( zwSY>S7gEP>SzAvjq4^)raI6!9$1^dw_ZMHi{g>aq{n?E6oD;qGVu z`renne)o+x^6n=Q2LH1y@4WQ~(x8l%P~Bv*STd6~Brs@uo6HF-n}s}WqVLhg-p~K@ zoln1Z`*(jXVfgP}z5CiLaL9Xa{^8x%zjN=+-&=yW#&$$l{lRx#wk%kbdp1CJ`QB$f z4OnAX-ha22|&}UwifL%YSp{TYs|m`XBpn>E`awer9|6o59xSE|HUknBwPO zzVp+cII+6@#;X~Ad%ySgJ8ypH-fzF{9D46FUq>XHIt*20=uL0_$GB>bv?kRsZrfR< zz5q7ceUyvaga=@P*>d7)=&zmZoL5|fY*jr@*MYHeB{+9spXwsOS&ptV0MMHokeS}N zCvnQ%N8CT%XV)2zA8u@r`6(%N$g8~$h7vXrE-{+NJulYiEZ8_GJ077-`DbNH+w79V zSqY_2EH9(KaWbQg`W72i)^)89xo6o}s_5I?gaS0dzAdBJVTgxon>}qLe4550#q!oT zqDi20)3fZkO58>T9ZUp%DkRs(61#rA*X?6T z5;=(08kGac`b>|hW= zNGC(n0qjL9t0c&x%G?Gn4B)oj$u3L~`e(O!CPQ}3-s@8p}wK>`9C)1e8?36it_)&|?#Z(RfB4HHoo;;LZnMj=*ri>Zi z=lLaX0}Qhx`nP)w1{Gio{jrrvp+Q7&e7 z$ta(qju=!u>?leLd|}CCsAjT}yK^6SCD(`zcFGr84s+T=vTOl=ie>Q~WVI`Kkt-1~ z$K$e$2n+{NlLgj)Sr~!;O#9cL6l5!$R`KL52%kEQke&mBudc%7#A(n4rS}LrXqzF0 z`mwW*&pic}ytV1fS2lPl5&##MT6ypnwi&?2F1=5Ta$6e`kV1_ElYeBn-Pb0_FFkz11YSWm@30@nvWiO z^ohkoA6h)5;}mECVu#33im|rfiQ1{E*(jabpu(r)<6{PQp7T*U=N+8+^Bs;FwbhSi8En@{-fQoGY@ZZS3Y17ixZnHui&=m@$o zyKx$h56-1X6B?UIuhv;0Zobu7pm!OYgLZZg0-LmZA8@msxKE6hL{y0h`^0J;^hDh# z8~Ed;qUy)H4BP?i|* zu}Jvz^B83>(V^4q6(wvlox$*};aq$AcX}H<9bg-X8>P2Tpmkc(Ab{3|I^Xrq`Uy1| z_VHkOs8kRH4}UhlR_XO535r7BGkl0sIq7!*@v8C6(L=3ATOU^OTLgF2@7elSC;s)D zKlmRWdPsl%E%7i}q?E;=yl{)7DT`)(tIGIRgagq!WPMRLjMS|`ke8?N>2#if<#$ol z-E6!%L|^FI&8k`Zbwk+th%g$MTHBJYrCY$0HOV3!T-yk`6~?V(iV{Uj+D7E7=%lM0HGDYny71@hIAwn7~0t*RCkz52*^Jd_xx67Qsir z1ZwuCS@)8nFSYv`iZq6UQd?4XS1uvbl~P8QO*C8fyAn*45sfyxpUm$%gs$>0=#U4W z>n%m=5)L}UH4F2Qo{PmDxUGaCXhp9SsHkFSVQS7bo(%6nvV=61BhPG_o^{u)n#uqh z2X7W4d)|8YhT0+Chb#b*z|K?6YE|W9#94xbV|GxtAKpNMvBM6%Es0+Y2{R&Y2;G?D zXYHFgzd=MFvIArV9re~G4GRyyj?3%B@e|LTy84k*A0-TIDyFambL;wHY7pm61@mrk zZ*D-Sm42LZbB~Ith)!8z1DLdu0Zq-5$j*DtLla(}(CWn)>j_1zzqYc)yR_D-F0T@W zoFO$rGf%-wWYn$h^Qq2ZQ$E_|54y!U&6lwfAg+o&dgsNKEZVJe|{}3ygaq}2;b=5K| zHe14sfJPx9Ng$I);+lP9qd#25s>Am33*ank1I7g50vxuuEE^dO4a~3NO|#W%Y%tI~_SUnzXFAPBrRNjlv1f4bY%cRgL6NjFUg%-#pnRv`i!qwy)LP@)Shak7@sV9+^%FR9b?0i=?N-Fu@7To1R( zK44`+UiB6hW(b-klh6puDS1=4sjy3?Lprv39l}OL1mX;@^hU_{@l9cu{R8G;*74qI zi!W|TeXmyeUVBd}f@*V$;p41qrNyr0!M$nNN8md+h}F-IL_-VgU%zF;hOTWDY)%8u zR8zeR1pY0-0C3l~X?#`T0NYL54IHgHEnnKA9DsK6#Z{)d>nKhIlCKX4rM-Xy|s4WOaJMNuy(nEtVA=h@gE%@g@u5voL9J*zv&d z<0HzJkeR6q)Q&hu0`>geR9}{%uZnG6s!EUR)Y-p_NnJ$N;gq7gCp7IuG|Uzi?pS5w zw>$sL>Fcttndc{nI_`dsT$8l0)xD&kAM}IThP5wZC=K$bA(brnaYpih+_L=2b#6+%A$%Vnj-6t|9DuW!@|Qn zysV-8IF(PMjn#HF<(v_zx_she%#w-dhxz&{oW*p*WjNCs0yB)Ihi`BPg$RX(AQTW+JAp9OL2+==l~twQk3XG}96z7;87v?seWa1!`Jh`0AGGU(ntj0gaoj1BGR>jGVEK~HojLekQi05buF7E%Ut;& zMw2Erzw4=X%2Cix`A}|UOIEwZ>9tm9Ax^sl7Y=xp4*^|` zvh1fE7lV9{{gid-r+g?WmKIV{u?{3j%E&@)-c9Li#&3--y_BqG%+;4Ezow<~DvY3t zz(r}0lovWTo*PBfnKugIecE2$1^FxvmBZIqWG%xWdu|795pDYXszr~D$alrgRtOq_ z8Qg}H7`AeHO_$B{S!fNtdbxR~HUJsJZ8vhbN+On$HKCy@Z+N^Wkd@;TQd=u8vjd*< zigVwew(JNHQzYobv{^ELlE9(|!>@EaJh2@YO42CJ>H>+7j&D*?cy)nj z(fD=YPqQ%{WkZK)Nt7ksFW=JGBR6bKx=AHdhRAue{$Kxp7k~8WUw-Hz{rR7&w7&EV zKK(HB(0Uf~6Fzg`>GY3GG{E<1`GJ*MegrZdZM<;l^x~6^Vkv*JCD@!ywigxge*I`8 z1l+}C&OD-J?Jiyz5_lLHm_iOZinZ4wDHg}0wc`B)#FFO8?Js;tN z$nVVi;{9;D_BuDS50v7826|f?v!%4Sw6Tr^HuLc*31$O0gnx-~uxYmz<(EcZOk)#1 zEHa;XA*)#l19M9sZKSAJ(gnJ>ceG*t)}!uhU$h7LWVi^Y-B)};zkBpBXTPGUhZh$O znx#`)eqr)Izr~iohapEzRZe81rEr5WB?&n)^95+!|x=WdR(&y-)U+Qbf?f8 zOP#_ogtcqCG2Zdch-4_V%D`<@5YuLr;HRk8MR#i-G>|P|Ga}PR#@Se}>3LVy?fPSB zQLMpdF4+;WBwaqeY=W>-4Tq7bJxval~F6fL*dDI;7}*Dmj%HH@pZfM?jefD&w{&a?r}h)PpQU#98hq z7UtQ>py45T3WYw}D3>fZP2=P18y0&1?C_qm2&-Vyc+_ds^pm3IKNQ@T0|J*AFL3?_a0BKiPh~c$`5T zDh~6fUX+EpzL5s;^pXQ2Kffm+v6C=mYzjiDfnOXHd47+PXrle$;=?ii4=?h&^`Q^? zUp}WNS|0+u{B`!x!&B3%LV}NvUH{InwiI09Dy* z|37nY_FU(2caUyp~QAVJ`OJSMir z0Yp)3M9A$c%d#!Y-R-R{x!v}7>u%e!+wS%vFEKrD6ET0ly!I79YF_$f9_Dd=zh7q7 z@}2Jh;G&L8TITt_uPUo5D>ExID=SL{l&#R(=JQ#awK}6t_DxPnnXG{N0!S5Lq(cB?(5#JlB=V!bqpA%tN&CCeq37d!_{e61Qwq;Z;9 z?5#aT*T@stZIKe=&@8$T=Uxnn`O$$C_!;yrp>3G8R#cIjGjN{G!GG9>?jZuRsMASR zG#K{g4Df-`7?lM-KcTo(Wh(*AL60tLFu`m=?oU8dUK*L#9@ zb|pb`v<-FMFat9Zd1%)P;a(ufzPZe{2WS9_{md0G1v`m42J2Sqfz{VqU?P2Xu)4Ck zvFbwI*f0Vi5+^prYt3^2zyMams5NkUWi@7?buvxbCz@)qV(ZX7LLp}0R!6St@5{gu zrYi(c`*ylgVkbU6U)k^G(X0&Kjdnjye5UJm&f78hY)&s~X`GBw{ow@%U| zbU!GM2^1~GVX0UO$y=MGh+epT3JnPF%T(FOqlu9dRF64hV-z?0301j->-o+4fI9(0!Qy6=mubyp8He6Y^*tX9)iokL-@1 zu2E=kp|%bOavDS>Od0PEnYXy;vI38%_2k03A!B!vY?ovSS_y-uJIap9g_ohH2r7Jn>k?*<03p+MY{FQ;T_^ zO`=s7lsY7&dTR*}HBAjYr&Q=;Pwg*g+WS~(mA%FF+1$M&_Lzh~scC`;8of-qyZQ*v zbOtnohZ;Fg)TOhg&&mxQzd1s5W9NwV24Ye(Mep=7?9Si(Lj66jBr^)<9*84fz(Sb8vs=-1U~sC za_Y6gx>lP}lr&Dg_A`&WBy|%=iNWd~^5bM-3G-PidqRXs?`RhI~v?6C9A*P(OK==8lF{s-^X{DVScweJXOA}(W&P9KDaY+ zUBK-QFafx&L8jWf4|I3#^(8v-Wl7cgHZ04YxO_lM2$dw>IGoh5%Lg*+hro05&_FFG z*9{!xNPHIqsHNEsd6GaEd@j*r@u3JdHBFvIIFNRNXcJGH%Zmzt45jT zV++z3d)V-*u~%7DjMiW8qbH{A%wj_xGug7BX#sTVX#pDaMKJdH1kU}mK|;yC?IkQS zKN@xleATPpPPM*0U~dz92upRv2U;sW*j6zu>el0vh(KYnn*7n<#+wT5Ei7G3gKjb` zXt(L}Y_fYz*CQAng`DNhtu=z-QLLP_Cd*shc9!Ru8`N0x2Hd7R<77l5IPjb5O_Pd; zvBO@~3dl-9xvDRwr(LCP?LZukBG-$SH@Rw{)gv13n^B_p_DhayAK zv}IXr*(huQ+jR=Msv~qap8NUp|Kq>=zz6u>e<-82)NBzVa{qmuXBl0?SRLe?!Fhq% z<=Mq+2$(?twnfVDTs@QJUokw1wdu+x5O8QfO&P$;i z7JmYlxlWjIWV%iR5e2;YT)U%)^5QILN|@#t&QBx?05B}0D;O$|W+G&m*O~DQ)1*$s zM<0<+hgt3jShExT?A`-pn0K2IU+4e=VHAuE#7ti8UA)p?pR|~?C9ohbszRta!v1mN zDMHPVY-iJ}PYbz|9&O{mnlBA>{Z>(v2>qmwXu5L%xW`!}oNG~Z?c`GbMczTTa%FM$ zB9hxEjB0*>u(vskbfWXf{tqKd9|SNo9HszO{HFt(L<(Hck6`j%fUeC624+8i_DRDJ zE_KqBax{OelYE_V9M!f+xarPA5AR=n(Sk&$$(2M+syT%ovmOHNaQQDWk246wGp}OI zG_r`We!jKTh?)#bT#M_)E&NHO?Cc0=_`{4VMG{3-w*-hhziCHId2GVYlNE;-EQ=F~ z2dOU=m)}P37LN#mC(aGCSsEx(cESec*B#f~Qr7lj^af^r0^mmjHC^d;)i-2pWVPmr z-s}J0|NSTb=cNyPfdBpbA)&&6Gx1^_=oV3TjK9QL3}6o_=@muqUx2PiU-L8^^B9~l zq@uo%g%X=J%a~W=dCe0doBk(;0onn*0UP#+L~Kyuz!O-nLk^o%uB@d#uL6$b4@F=k zav3loSUP6H;Nbzxh~C`&`yZU#c=3LuwAj!sDJV8+#kx!LoqztpgWUu7^Z%JQP^V2H zBL4XY4=MTpn#R&9ql*9e2M>1-Sh%I<6Esa0k%aha@o(g#4aDRl1@%>g&gr0EK7}}o zi})tNQ1@NH!C#zAv2;EpH9caOYm8v@)8eVEnYvQ*ALC}Z}IvOy?$Un|CX;GBJ2Z?@^A4P*`xS+ihqmOkLmT)qx@UEMh97Z z|2+Tta5!}vgUJCdT7sfrh5g+Uu8f<)T2Z}R#ELmZX{T`;n|j#qv{;uPB-A=($g|>@ zGoO;fxI*B}T29eB-V6;!NHo&pU_A#&oZ`-P_K-#pYaq6uapqt7d}l9$!3e=1HIn)= zuMwmU>;xWY|zS!Ws5tHpA1vat?w zR?`fI%5We}G~JGqa}XjZHx(1Dmq=hg-Fbko0DICDsg3@0M{&=PQaXLOuk$o?pDQP4 z=VtVL49YK#bj;w2zh38K$9cTtG%$UP=UrzGM<$JD`UpbreQ2Fb zmEZZq>@&SSUY%LNxpE{vex|qh0yaYDAQ=uJ3KuCt2B?PgR%?A{F)4Z2iNu?^=?>N; zI8j%iRn6f*?98!~c$hrXyR@-5i;T`=Gsil7g9c9aaJLfgYgxv^G7CYLs_7~w2%RI4 z1e95v*2sl}#B#h!&?v}G5F-Mgc_pN;NVM#`XLuRW8S>|)F`9bF(cA zeHN@G9$>7Hj3puCSY;M?!gDV}d*H#5XvT3^q<(W+-3zQocLBwNU%V}JcMLqV8zh*; zG{JUDbKg?!`Mc0`)Pe0dP2MyGKgxBJjT=nD?y z4$BeCO3@}r^u@(>Ly}bbi+wAo`-2OQ@o(xU03|%BrP;7f*xUi#&)X9@)7KmjoXyl|dkH zHwh5X9~>_~i$cTI2R4`c;zY#bhoof%UxF@ZJPg!@E9oFBWl_GGSYD_SOnIEoLbv#>H(Ed zcnDV(01x*}!3_#W8;>sl7_-aMp~KUY7d(sb1=oK)@wNkc!e9bc9_o zO^f+0CY6H~7Q=;e)+Y$O$lW~+^$Q8Wc!e>VrkfOAGj)vCCjHg$Z&-?O?p1*gTFGJaFJfhLae@gOTk%;Jl*9vI(>vzv?1IeBKnyo~H5tOSmUHiS;~@yG>w zMVGTuZZqxv8Khc85+t)T04$xB#;BHhvs@ymTZVP76x$cF@Ad*e!v2;8oRI)Pj$=nd zuAwQXpLO3o>d1AJwKJMD0``BHG;QVw)*e1^;INrsup7DrAXQDxEG+h90rFogUd#}N zbGur|Hq{T1HB9<(vzIwIM&!0i(SRSTimUMLO zd@@I;y2iA7aQt#%mDOr_+^F5>(||;~yP7Po3!Oc5RNy@wZ8C1@*@5eVX49#m7Jy8aC3CD2XQ=g_#gT%oG4fs}R2$Pwa+IP876zo)tn=fXJqfrIEY zYn8mG@!^xeNctiIxnrMG+NhzhF5KAY{^|-G3*w^41AYT@9?@ez6m54J}VE5 za5}`K*xZ0O7D?fkpqbLa#z}@Rn8T(|8^5s(ouRWISBzs{#gY$_2js@1|-aosi3Hfd0)|jA*%RL4(npP&<`ULkAd=0)=k|@PXn8BgJjS?$Kbww%6 zRKl!9^NW_+@r#=&f-*)3KrC-G53^*1YoxylHzvoR{EL%vERYZxF~--DQnh5#tE)=; z4p-ZLH6~9uvh~3)p|yH{cmGsxi2$4#nM4aKYS4H!G8)gkE$h09x6zJCcC4*5Yl3`k zi9Ez-Y_&K=tp*bm+c=qw1z`lOeG->P^cSuv^A!eElA*Ks0jI>E?szs5D5;~^A0wum zM$Bg!i%Ifo4iQbGcIy&U5))>h`*F^hnfyS-` zj4Gk-w)QEnn~mu>r+AorK2ayppWjzaS+`F$SiX8v%cw3`k=y74;Ukkge3CA=p~~=P zN~#0{T;tY(l?-H2*kX^mzl=mRyraSv-8+@3+ysF3guBcrQ?-a6x(N3jZ-KEr)3kcq zrFz(7r)&#R3_<2n3$*lN^4Yq;0=mq3JkKjhx!N-YAOK+PSEJM$;Sk0n6b58}fEu8i z`{cRxq||DUrL_rXo2ES+$AEO-KuX%Q|8Fc!kGJl@lWxG|5CGao3RaA-t2S$vpY_p_ z8*qRG%r(0>hm>H<(NQlCoyiUyznWlmA}FxoAXb5wkl_UvS-`0;QRQq3aXAjJ$SUxp zR-i<_a1~%F)A3Kc%%2dzv^3Q6zhv#E;CH8$s_(?-OY>GFC7X(Y8Ee)2EaOOSm!ec< zx&auOjHtGg^_`D&$|s`VWvlRQ14>n}x8GY7uX`alt+m31)&iZ3<{8yicd%=Pk)UB3 z95}3zJY;PSvW0WnvPwK`t3<1$kk7Cj0=9^2nH|Df+B=u5X(*+(a7ighQePMV&G(AY zpwpadXe8`gxzPDo$mLBHytSipdss*B*bHXUBIq^T)R$(JA30g=OaHKHl~%PKFf?L? zE48Ui>0p%bqJyV3Tqr&<;y`(|XIuC3s#7-F)paLLG}z%!gc`ZHA|lU^HL zb|{I!3USvKPX1%-)^5uI?tF`!S&7W@zeG!$@Z7lJPg%uo4MB(@bci=k+C`8iw^#z% z;1Hoy%||J_OivM6xpdO+;GRjRucRTE7Mb<3uvkYgaPm?*YoreKYvxdF2T42HnQ}YX zC7|`GP12a7NmE?v4626{$aw{fh3`3D<^|t`F2L$s!ySTj0_gsBGZz^JMTH%>$3lt- z*i|f^X^S4|Fkyvqhu(s~Vnk**&ht1H#5J50aTNafj*Znv0>5Dy7=|I2-43g&T*}o0 zC_bV2V2PN730JS^$P4IcENa9yG{wXul{IiTR)fKkv02+36@KiX1M98PrNz!+%>&=Y z)^}*2PK?@332oE2QA>qNIJEFBQctFkOU9J&+38>JsT&>UrB?Ijt2A%W|U1ZELnVgbkgzL6Q@h*6fCS z*_Ak`!=<3e0}=mF%~w+d+}4CW_Sj?Y?-(JFxJ4z2)k>42WrYjQ5iW?fb#Vtr#%;(D zgyxtgqkazw7Y@rOYVQIj+4D_YcvH546fBxspaqJU1yN4ftG!7e;iY7SQiQ1|yG8D{ z?4C7M`otYpZ_!N4FKl*4~Bbw@RY4VWbhM&_*hQ72@QO&lQ z8v4nrQa&+035w#K1k6M+{@_FNy-Om?6}qR^;ep@Z#f~2J?NF*uu%950 z1*}l=l7(rJZiVE8hGDoUI2519A0U@DEKf=xGcZ{=kf)NwW@dwMfF%yU>(coBF+*d` z0hPENmY0|Vj|x%6oPZ0MF-cPsLb}@ML8g+z8ZB!w50dI7ky^>^)@>e%@3aQ2tkUPx z4r3>I>@(5mZ<;93PIKvOQ^w4Da5r6lrDo>RR-!^6obUQ<#xgz0V| z(&$=IR;Z1;i}g_|gW<7#1Z;i>(VRh+(#?dJZ#hcT?eDs{_8Do&@=DaCqd8p?w#o8D zBc(E&<|J4adsn;FC?tCb^8F^r^F(yIW^r;UYTY9c-M8$!dO)Sv7r53fv?R{AYJk(zo->-%=}`j>t+?}OWT6^6h{#C>FN!e>ht28+uQLM4?&7eaZ;tapEgI)g;3=^F!e>w@&>r`M zMyfHUO4VRnyP6?jS0!%|nX8FF%M^CTKRbD3Zp$3|H_#HABe~mpfoln+6`VMM3&b9l z|KW8t7-l#8q?M`w(Uej3BGw|*^{Uzo2hQ$z69D^wG&;H8p-q5zATbZ?PH9=FwA@kK zA%(Dk+ zw;3EQQd#QPF5cg~c$b{v+GM?ZW};T}Z-c+fqU0{H9HLKkINVq+4kag!qaGEOH!b{F z=Y-EN&L`nqh(hTdwx08jPLgjXe(cnemb-Q~9&N1JM3b!$(%IPAj6M`@hX+FkOg*5c zgbM)Nrza2Cjuw`}rq$KPqFzbbMf0_^E}P}y9sw$i&ExY1Jaw9`95vw?Xw_86hpC4} zc$&^DAZcuq5~cC^QSqOD_2c-(=cz}MPl~{i=C!Kr-hNcSa{^HK5rTbIOaST^dz$5i z8;r4%jMiGqI7joWw<=|~AS#F;_K1mP>zS-bvQ~O3PaVrmf??MbJ!}(~e2xMgNofV& z7}~QxZYKv!sY;CqSk3q_f1uFPIuAp-+>TzN1XtTLO2#PAF4w#T{j=K!%Y(y* z#46}Gu-v=Id;~Z`LH4Vg*o)E9@LN_EYCwnB#D0dyeJb?ffT?JrN98AHdaOLt5dcH+U$#1EzBTxhxv|_R=b|{SfqwmB*Z->UhzJKYVe>FK&4Y=bs@E6 zu~2!9O3Hs;q8xH*xA?1uW`?)Q_2d{UwRnwvH|h3JyL8KF>5~$mT)AG@7Djx6j{@=B@)1lNWpIh$z!>u}bK* za9b+{g>zQjV7QxFPjnDl$jq)`!}OsyCT~|4afA()yUAgi=C=15M|exEr%A1Y^W@4$ zWOgh?5(0Ncmql1S?~jx*V?Qf7e7gn!PqdFV0Fru7PV;RFsfS63*93YO?Co1HpE$+r zp{0%&jkQpyWLmNNcDF*f)w12w&6y}!*~xh?D^bzmh;-7c##*m;yDsQEF5c>IV>vPI za~V@~CBol#?gHh( zXQ6cR;UCDkCFzNA(T3uu`W|3tzRDSj%GYEzdX*GE-ntnRF7v&(3QwB<}#jsR^Js3 zGs>`Iu+&Ytn%L5`w?KHq23A25?r&)oD#g?$JbPo%NlbG#m9RW=J948pkgDO5PK)01 zUDeli3Bv@A$#eEdBn2g4CE+R@W@g0d3)H1-uh~%|4>GNct)d3!LREG|3$zTLb@lir z#T-@>)0`2_@aRFBBT(rC(5z<$5Q>uT@D7Gw@JfBKp>5cq6}EkAXk?+{w<+0h#fLUn zHE(AwQtE%p#Z#IA)~=mL+a1Ex*f0c5+-`=%0cLR-Zp*DiC)ZBD>h6KtZA?CyTWsus z+igtQv~IDn2U{9TAd+fbO(gBCb=FjZpH3xgGGKP<`G-;LCR(+h8}I>c0~lGWyNaU~`6-)SVCii8|> z!(BOugvcGWo~V_rYzdaVJJe$cdy#%44r0|}v=tZ8%{WCXMarfJv(vf1Gbz`42r;*e z8ZzvTon4$)_TX;1yY{5oJlyUhMSrFj`fCHI%a{9$^Boah?qI79RxSOC58wr-L(85k zf~~Yu_#W?Elpt`;GM$@^s4H{Sn;}6`X#|ogJ9xq$OKu2*xCf876HTceggRXBNaO7t zdAvvL=XpnO(c7(Ble}}N9c#NQOr@Z0@l)+Zxp1Mj2j$1FGOGK!i}dx-3K)oVHPPgf z40hj9+5H^g6z%Ye7;2$SAza#eS-ZR)r7BuVHgqCfd6(GT5KE5ThVRFtllCI4_9QGu zkXQ5*7)d0S`n;>CxKl>wO*wwkk1=}j%=xKS_7*DiUH;0%_jG2hGOBcTU_$(EAR`6k z954K`SX}OScqz)rRH8q|O*IQh^^ffYB?00P;Y#8^zMEku5dECW0}uN_m5wgbxTmOY zaqO&!+@Sb>?7@H7;WR8O*6nlk!ZIES8=7T#q0Hkz4u?@7u&qChDAp$0EE7dFs$%0qKN%PH zKaWqc9SN&OqwU2HVBWm+os%OoHm)MRkQY>NH|vjwZ^A)VDjs;{K=Snuy3Rq#TA)<9 zc;&m~k;7|W4$#R?2Yc6x$p1tjBje)Sdun(b?#CnCl42SnXfvtX$-6GD=#j3t(n(Ba7cPgJbZF zf?`fuU8O`w2BB*uo`w^j$$VgB;Gpm-E^GnaD2os8dgH}g{lS3ecrP(Nnjhl!A-m{} z0GSP~af1+C^z)OvAI$QZk^R_3NPL1|CP^eBxF(Lx&f;)DLuatBc6;>8?9Z*US=UIc<4vVP(j!|K+iJKF%42op0Ab7CBfpgUt zh4jvoZBy~MmpPO_i!6>qdLb$2;Ls(2W6z`DMPA#LK%GCY>w=>6)BO0Se=3PI$6G<3 z?O}&#tSZyZ$!)tYOz9F1X0N+;2zke5;e?n&zM=7n&XZ4d#;+rv%K(qq(pkHLD>AT) zicH9O?k$gXHYdoC&c-L$*{HXGB+xy61a_8sXV-9=aJq92c?ma>j^0pm?D&bZN6(yp z_UI=tmPY?fl(26x=2QRq=$SLej~s0;PiFuy+$(F$96kEn`IE;_9Gy9P>bVj1Qk;xc zbf!GbeYZK>V+cerN42pbtc$pI%lJw3jJIg}8d44NsQo$@&M_7Nl11VRKdnd zm@8UZMN=f$xRdF=oYVI{Fqkeu0@r|IRR?(n2~e4#dA}etcSoDnOJw{~R%_;~X4Y!3 zuu>E*&n<4){ZgGV9>8R>=`m%pCO~LnOmdC)m@woqW`;&F9&83AQbaSCR*ar)?oOXN zapL^R8BE;Nd0bl&lcd+r96xe|nCI7r&*S2d$DvwMnhE?>j}ybYqUz1WI{!y z&qhnkrpRbfU6;kHre^1L2UJQaM#i#^%p5-S-1(WahfW-|1|QgOE!x*-jvhLD{^LiF zKFhv5gc}RI(&!Ad(uI5?5YpMZsz4?ol)JKTTAcUdtA9ZrP))T7IV z%$YfR=J<0T<$$Q2^sp|PmZHp;;NW`g@vC^ynJgSE#U+rf#j_9&V_8yoJ@Gk#lHdK^ zB9fqQD0$h~3IFL|jNAbPHI9J=u)LwwsAhs>OajH~AJxIz~=^Q{kESJBOXdHI{eU_Z7z`vN2xnOCal@|l$iMQgTf9GpfBen*3d`X}DUpfa0)0AJ?y=MH+iRf1rxrk4%TQB{v<_jN`p|Vgch^@=;PTD2!&t1xCvXvhwG5Sq zmky?C%mfE@*J@kM&1rzp+gyt&WKJ*Qwyeu5i|7pcVyYFdHdMd&$(O;7LUW>Wg!RL( zSB^o;kg&0#{*2ibHfS%hymE4Oo!6G(J8~f+V=y25qq2%ytNea=5deZLq6uHdz!%F@ zc~R`d%8GVjxSlQb)@MmoO0qcMosEtZuLj_38kbluLmOU4+M`9o%Nejd*W?df=ci3x zT=Ajn`o5W91%d(V7&w$SIYGvOj^V~p3zNCV`nya)x=x1=$-DILY7~$ zPr5dFZeicgI%Z%ELt~E>g*`gPO#*=b1Vy2C41iSJ z5efkaSpCYEo+@6Szy@eB^nFPiRVpeSColVTBMgj)=I`o=8`!EFx}vVpydJ` zF{C`9&um--Y?$Rp9F5!N;(~JCEplzZri4RWi{d@~=U)QbUVM|s?O3m)w^4U9L9zFQ zw27NFmHitO&A_ZDpM>7lIjAtLu?}#;w_`Db?AX%~$>Cer7Saqy^AKWk?$L7 zDcJHVEK`DB?`@*=I(}P~O~-IKL57m-aTE_$SE*mnb}lz2`XfiUGl00LlXlH`6|f+oUw@1jWOLi zL-8?aB;%`dPU0W=&~^OY{BY+5EU0U}7y2t3_<_4LR_2|LROtu~U6j@KR_hp6J`BKa zST_s2(S}nt4rKvpXBm~EIWf}cT(H^PoWy%xet|L}mGtllV6wNidnMYwcIoo9s8PXr z4nk0f_C}bgw(40Tmgv|CT!X`D^bS_g<y+RTz`>=PZj;aET1Im>h7j%OH!fC_uW7 zmsFSu45Q|EZz5GY#rYsX8jXRwoj#idpM&UX0Ou?0m0)o|7WN)0Ix#n^LcZd9KJ>p) zKUJX6zO>S;hBX`5geZg2m|H334<*UP9#~+Wm) zXA;*wgzWPsSpDeID$^(ybm>INdbs;Ex?WVlJn~AGQI#;?y0L}oyxm_7Q(A(~VVJ&k zwyz5tYfaNWIhrc1(3HW(OgKQ>rv_@yT*j5Tq3_yTheaKR`jnxD@*Y^{OV~JnNku9a znq128(RUTgk;%F zD+;bau$&7Fj=Hz(&ONMs)Hv2*%h)^}<{A&`kp_zS;`dGS_BAwVQLB8h(8*tb{vqGZ zJXS|sSPV+uSS2&eW6}vdMwT}qEn*x&;>oXRMhO$u!+XqC8+(TEp)xbN5~Y%wU}Ip< z5;ywlhB<1+Yb)}BuOgj+>2T&CCed^|W1Ww;$JpyG*Z@!+b5 zJwmk%#l!Zew&g*!U!)Wcbnzg~??)`Q5GA{J4f*&v;^MmM|2rzmArof7I$)*zc&4j38 z>O#!OS?b-La9BH@)$dGdrVaoQOwR0&!QIS@v(OQ7Me{r~Q~v*pnjsoceCl?dp{_kk z(!mlt(1s8-95iT#mIlBsY_y*5>-vd zysh?rV*9GxIN0%c35s^N1Cw=t&dE##x9+1UE#sHaaTNn}RMM7mFxcHP3I9yh7JUQ% zHl0IEb}oa)sX?jT3R*B15d=-H2zH>4yRnr7Qs7;Vyw^tETkG*w8ul;TOc2d_Y?!Ul zQ`}TQ<8ds3QI9rhkG}+0P@7xw#+QHi-m7nHefBfkfBhxze|B>L)Yf;udh-W==3j^@ zuGjDW>Yew#^L2k&cpq=R{o|WIeA(ZdPwbFZ8_{z=K z{=r|m`*Hi1Kg;j#x&w0i$N$^bTR*fe6*T{^zy0jZx8K=%Jz5A=zw?6w*!`_W={1=;&n?L#ejoP z>)T)b=@5ohL z5a4r;?3EwB_vxTSwU-={n0<`z-Cw^})(W5G_SgRDy|4Y^<`+J_^=3fj0tbjU|M8bz zK~Bl-AAiRmvsZHKm*3lZB}$oEh>F`k{Pxygf8Glxjfo%L_|m(-e{<_ApYd<#dGars zE@7i^_hM3o_TY;A*WZ3)55CClFT7@*`>!v5Ds=|GZ5-R5{p!2F{l?9=zj@=wZ-NwJ zw6!~O`%7=#_{DdsA(r0A?SJ_8_FJ!Qec{*dz4H2d-~P43Xt*S|fAz|HfA!tE6?ZL4 znUZBVW=yyJt?zBW_WP|@K5YbrZHaf^`KMcONjmFF3_u2$U-EvcSkx9a!NPx+U(77p?mNy`H~;XZo8SJzXm2GL>c-!H ze(Uw08(SIll9xt#CU5-UorF1zVK0tJBW=;gQ1M~cq)o3%m%hn{GjjWDe+#^9eeZXU ziy=?syT8KdKHDgOwcO3+?Z5iQaQ*N=-u%PYK}9#d`E3VIuUfbuZ~W*-@BZrL8$bFo zls#%fkY_jEdE@3!eu@S&{#YYS?{GJUxf#!!kfb;N@K@VkfBW6v{ms^2d;$MYK`e$w z6JY+W&-@jj;w1n(Wf1VLp2rbB$L()^d+W`=-umwAX6uH%v)S4H$v*e?U{_E&6MgY-?!I9+YUBqZh?+U%Q z^{|G#i$KWs%ip~D@}~iG>+R3H_vU8+7t)g5ed~vSE5~97t!{nki#4q9Fap0|FC)OM z&%9z23-MsA%!TfApR=#qU;h2pPky)c)1N^AZN2)N8(;isp3N5T;?3WG8uY_K{`n8L z-}+W3yp4{jR@Y*)j=N&YD(+W&_g7ymp>KcTQ(M3OxfA0kkK)Zg{Pm4@ek^Qr`ha`~ zY!%u)io=z-ynpH~F415AVH1XX6B8D#HfVqAcVDS`mxv_1iX@A#{9$D03df>L%Rl@M zBfjyQzsr!5zYTU%i(3(Vc=Pwa-~Q~E(8vgjIcvk0VZ{qiTH>zQkD`}=QU;z8!ArT0GlI}YtTe-CcB`Hw%{`pFkEU4|R+=I{S< z>y=m)%#fw_fVZ#y#^!z3b=VT;PNBH6oZ+s~fgi&t9t=GYu??B7P zh~#E>VjJN{+c-Gc)?`L?J*v((O1Ff_1-rYNe=Ma>4^ADUa_Z@;? zy{qmy-1_OCI}mpqZhiL~{^I@i8*YE?Z)_CaBeu7T*N|#Ec=NTFp&li%=GAM5VWl9A zau{yC@-v_YX5{j^B)&sRp2C}-`?Ia@z2WG{K0o-lsG??1A!Pp6&wmO~e7(D`5X^An zE%4OqAkeA_S8T&DuC&{v$L`?6qb?OHu8$>_xq!L-PoKT{^4GYeeD@n%r=)NC2&3!{ zEu^)w2H)Z)G|0foEqDnNlQ{J76K;Rw&qyq#MYFR@5Qf^#pM2Y_PWJ~AH*fuD>#d(Q zR9P2u`8V#>xk!9lFHLU%tKddn!JK zH*o7q{{>D3e5V!h_8SinL+P2UwkX~pmOk~6`N|mCG)W^4Xi8`W!mG^~U#zcU$MS{^-9w)@cftPn8D2W3}>HJ7lz>JyNiL29@dI& z4UgxvFjot6EaQ{uU&L;x$3b>`%Y2c2O4FU?jm1U2!^VihhvABp*Ok6RCNC-Do%P(2 z&ysQL*d>VC^rp`+(zbYoO@>*zJfjSpB#axIX}(VyX#vluX&P~_3$q)G>&FzpGpiZX%^tc6|p|6M}PZ4PT_$L;iG=`}3yO<8|a&Hmps2ylaW9luA_vh_+iVtOeWlmdD z-Aldoql<7xFRwp+?f5*4sWYjnd`RQV5NuZ80>$+LH}4=q=`c?*PhdM3dl_qr;L@9+ z5;a)Hf{){#V8r)fo(9h$A`ps|Lbd2zx3Fiz>^zBJ8}I8JH1_qLcbBzw_^Q8v&Bp-Z z9ZAoqo_;?Lz%0yWI`=*)Qup$Fb=daqP=ik_?6_h#>fWOt^E2z%1slha!F{C6MIP%g z3}NTMHwbJLYqM7Y)4~I~;&xWWRt|B09EpVpL?cNpvdY2Ef1Ek>To>s-5g>^d6Nr0? zhS<4HY|0`2(3|qERt0!?FwLWmp%i^q?to^8+d7HkInl{|)+N~1a256!u8kWII|5gA z2Yb1*zSW}ee7YGC229H2rG0h)gQpd6)KZaeS69|7*B#IMmHkE(;^GM%;lTM0l7^2| zBGk|c8bN&6*h`nk7$Wx4<;la(DM+lhJo&Miv3lk%A#e!c``+^s;By;G7kg{=fyYty zPbfr{heu{K;PJ96O)kNfErd@A)+XPqa?JlQcut}hx`+_B4x)=LcX4Z5#G>#!$rZ0U zPa*!TH+2A)U794i@S*Eo3~3bf1)V+Ce-R@)FtK^|!cg7EMpb=`!%Q`w9q$?i0tR3JvDIf@~#BT*K1S zWHfJQ8GF>nLEzmZkZW9$j;UTnY5TD3DB~NJ9T`13i-a7`f6kR~`(F-n%olXnCdVj35L{iO!2K^Y655NmxVxBDQZgEK0u)l#3#TfSe zH!{X&uy}i{=XIbV56>uzXa!+$-Z*9BLM77s;WzH}TW9Tos_%miqW^h`Tf`2XycKr% zV(P%OX_6uK?hUb*34lErJ--ke1hhH5f4`WnZI(%faS>bb z>xd(G1nPNb{QN$|aUd>4dnKg0SQm($vK6-4ptvI_GS2r@CiF0Y%Zm*hXQrH+)R(=A z9Up!YJjFDyYu=Sw5lM}tMUx}5uJBO(CVrQaQlgCr9@nqqh6rLI^WH5IRV^)xPA3Ym z>BNhv!Eh+hb?i9XZT^2@;OQ)!pXof)Lo#zj23gR`A`VF- zN(gZ^5ha2}h$nnIN_q+P`)U{GB6&3OG$N6DItq&1tIM6;COt*6ksxhOis2-!o4iUN{ zqk8?WNw$7!CLKq(((A1viVcjTpdGRd4)!B7h;XP?8ZJfetGI8Bm!W0{C2Ya_7~^DA@yy#Q8t4zyqq-vKyRmW zZ^FkP{gDv_kYIsaDt34AWMW`C^*lKIMd~EH7GG*vuKW{c<_LZ|ZHfEMb zIvmH_&eqlkAMdYU9;cO~Iit0M3B#fG+)sz`q|h6L8p=YRHGS?lWe^qqg0JdMrVoMU zg8>cw#8J;a4*9RFopAHaOBXa?(AYs2rPo!4WM!+h6|mpML>$MIH<0d>)`1~< z{yZ<3_K~$fDMdB&w5yMNd>-?4LA%sww z)&OU%Ug+_hA2DZLa)~yNezgcUHZ4Ik3`l~I{v9tN0#&Ai?&`+iGR_scpCx{{r-jA@ zW^CQrpFyS8+rb zAR)UgUxqmKm+ntJ7nt)x&y(A-ua$ME=my5^-^1n= zqNcC4UQ4XMq)sMB0r%fytcM0-^@PuAl7B!hTB*HE%gHqO@1*~(i$NIU9*}%yc zOaho{jR_(yf`G5Uq}7Bm(QMFCH-0pUij&MCV8w#>YS4o`y9-K{(hHbhy5S3@(>OfT1y>E|&NH40 z-(Jr5fn*L?OGNB4g}gAxXZ_l05BT#JGRQ2|4WzWoyoN)WWM+))t82^mN?Izaa=K&5 z9=d(y#eg|tO?LH_D?<4USx4aZ(WEQL0MV6IcV&&(;x&+mi~chQ!{UX+gfh<0?^P9&z<~^7 z#kkgztv?ALn|{%|Ccyx!qysTM*Mo};C=KP4ueCPRuPH_xbK+n_tS4!))gCKY2IpnP zmJmAuy;H*WGGM=|1e;2bpr5AR;CFieRC*nahm;yzon0k~cwa~!{1eak6aeE>JhK)m z+iThW$b1ZwhaJl4XU>&qcs^P$z#r``E zl*yOqygen^<(>5BS`_&=qjCF`NE&-KqOfjyV+CAJ_(whQLqjv=nW7~ekcFdB1nP?q zd@o&X=_YJ(Gq=lwV%Wnih%c0&dNa4BK;G4@J{q0^YbGUfD~Gy?jUdfar=Yb2LmqSP z5`$Qgp{|UCavi?0>)1D2zYLFtPO|$RuzW}{w5sQa@;!1~AXAq@a@?7tM*bbp4)ej4 zo|@zIR#>u&x%CYU@LJ@3I>=mHvQW7Bi`_0NQph(jk2B>^vF34Q4m>n=Dv{dP1wJ!a z`$C+jhUcA{qR!-@5e~^fV4YSTDuk?4dxDC_4L0jX!v#X1$6@ck5M*S6Ol^mhg@YZnD(%$SBSvW7E z@>9630y2qS%r5*{p!rEAXLzDh<;-fVkiB(wd!u&GBJ-eP0nneY#yJw_8a7fqb}w$s zUFoe4aK{6^vg%aE5N88m*!#d?LXkic5>O;nNRcZUKV$YBVt5hz~ zp7o8rw9m+snY&v^-!bl@U&A0u^Vhg*hL{~kLJbdE@RgVIAe?J=-d-XdEM6nPVN9Q% zUU|`sQYT5pbZ~byl{jRtycDzajAQ4Pr>4u9zl;Q5Pa;%+2hsPzpq|GT9V*4H)!S#YoPpj;r~%tWqQ zeg_7Sy=8s)vr`SxYh)W#7PC7Fdg@pZ0Q<)orNGGGY_^zy?5Qb89d39CnQWFPG zqoSePKH)$!&&I5uKuAJw8GHX@bMTZiXCy=qCi{gIIGY$OG)}bI27zUA0I?_|P?g2;TnXrIFCNLQ7 z=qeiN?uNAMAP4?2MzzkPXDvpqN)-lQFL_=^AYme zIvtdnPAOwAqpKoAXul<*1pVf|tX*rz@xL8OlDk`EgEM!#Fz(Ntg@~(+M=th-G!FL; zZXSj7!#B7KLk_Y*Jduai;lO|-=%FFp4xC?fXIVSK^N>@~C z=mp*Kb5x-eV>+Nw+!5cSyqTG!N*7gOFW}g7gSoK;etH76a|@TO5{9>eROWDER91)Fs(-ww-aS$dF^qGDlW(U87Q=6 z4Lnn&Kyy;!|BnnsmStLx|duSXTA$2C~~)v_!6uG~>9%Xdhw-kf#)012O`X?Um)z z(83CJ+bWr!he}N)XVAOEZ45|z0aDXYo23)AueJWIMUY@`9NFcO)XjTCyNk-7_FA<< z8cGuU(dv*7gEI@A2H_c8AVOiZ9;mBO@`)HB-1dxtWv*N)?FWp1Sv_e-S6DXH-Gly3 zVa0$>`N066M-0Ty1(^2Q(u(%}cWS|c141WmZ^FuqY=cn+J2j!GgPmJYk*WcwtDP}r z`^&qb%1X84s%XaZrwomxYy^A3tpmwTaJSgl!=}7w069*X33|r{ul>Mtfi1JjkBDtn0GRYHAqeVAB*@%PJ&d#ho8%YFw1T6XyMRun41-gV1vKN@_)+c z7=6-5+!&2f6tM~(LQ`!2s~mZ)22|&lL2M=hZ;D#j$%pWnNR_e4RrDfNwtBYw88r?5 z1h=R_ENRTZ|Am*y6H^$Fmo7l811VYOZU+xoEV2U9huL`+pRi-r}JkmQ<_5fl&w!-vlF zgv*#jLjWkxcGCj^_Hhpy#`?Jn0)eE)?B(W#=hZ?|2U@*Nu5LsWjr9S+JA}aArrJ9y zBo*!)2Hf)Y>-Nun%7%Gvid|nd6b@$VcJ1YfvgN=$qJ|^2O@zh{CkOAy z$8mgca~zavuv4_xXpxkKDi>8(;S3`Rg4R;8^_3_@w&0&~n?1hm*$cIkY6ti>I+uyD zybd8ZuIFIuq-ZODhIqrenLNYy+IyBl(A6=-=s?0T#s?HmIf{)ZR=`tKX;srGutw1_PyjGwH{k-k?1II$aMi`;LWxhA@^w)VMoU45ii^WA zoaRP-d2kc4w(*(47m=Rmtyd*JfKibirKigmA(uR`1a z#Y-SORmRc_0S7w;3pNtaDyj>iV^oy@anQpG#$85RO1acwlhL~2@ng8$iF<~~Fo`o~ z*vwQe&)LqU#g&V*iwG{@NjT0c#X;+?6(i|SB0fjb&qzGEdZmL5Z2?0KN&Sa1a_)K4 zM6B)}_LL3$&O5>e3=By*;xLO=In(E!CrS8f6{BDc#q-b}mEn2L)i^RYHUSPElm%zH zA_5WdR(oU5Q_(Ib+jJt@uW(nV@nQo>)+l3^S65c~4}$A(T4;dNYXXYMhDv2%|A%(E z!pjc=40r|p0T{LLO)V53LZeDB4V49Ah!VpcZ{|R`^>fiHq01Mr+M5+%ecM!Ocf32b zpgTW6o*-1-=Mj6W;Be^I%)e)sg0N+Wu4B5~iUDdbU0Q@Ew?CM~iU@P30figI8lTr} zSvGO#0m_~XdBbCI4qf_BnwwD;{%F`j4SI!LETT)-ku-aVIJy}5Cs+E*<0uYot1)=r z=)!zu2|wIh+EuByg%gDq2BsMp+e5g z;3OFj@KUQz(Jc@|YgmSOdc(qvjlLiExV5g0J( zDZuS3YvzxJ6_K9zJ_&^D>b=b6(m`fUK3scD&M`XlC;~csQtC_dq=Y~xVL}@7V(X;w zYvi;wz2REr_f>J&sc0X<*=JAf&P1COb6#Su6W=Qa5toD5gb)|(9#VHmA86aJ*}L=6 zVGJXTcS93n2pVuNQ)+N@;SgvAkpy0MZAh{LEpp2iI4B9kz$F4{g4|{E-RL4%Nhb>I z-0ci#&pB3hWWlnIVbfb!rHjCrjDO^M;5+Ok^{`Qxx@XfuJrS{qDgmeN@aGY2V#?3K z=fZkgD>=n?WKRsSg3W=(R-e~3WdjiitG8q=!^^mOX60%b07JEhXfRr40g8X>N_a`O zEeG#*p8S1`1g<&C1fdD=hP5XB-DJ6$2qDWU&9RzRjYt=!YR;syxU>jyzKs&Dy6gJ| zu@-TUH8$Dk6+pEPgWaZEW3*#0IF4%X@~f|H_?1@DvE&OXAeixj+U3wn4)6%2n96!Kc3D!1zJs<} zCUg>^-WC^Hc!JU(o9Z$ycb1Z)A9(X?ULq_Kjf3Jen!8rfP72!?eJvuDVnvR)Jf#sXX|RWJ=rzBA9p zV=HqkG%OdZBnwo#TvbtqtYe)(dH1=yv^JSz>NbYi*xj>QWB2Ojs6tqPeTBX}Fz6dtcQM_2C>>HL7pdNAOo?i&pBacsE0}*ARv8z$$uL;oJ zL8~hl6Dm%jq%z{HB9fC20d!yI0k5#b$ctP$h6HTH2y!Tdq?mw+VDBl;GsGK`w=MUP zLBh$K3G)>2ixRN2`I>m6DWl~vCS52(wS}Z8 zg1qU&9Z{?Jf;KH3mRI*{j0OPM_!3EChrgKRgVQ+!PG}-loJOD_?+!ODogP(w5mSre z%)EvowqhC{m_d77)W06Nh62xCNqLRXhcGvqpEZsNqpY$`+!B9dx0I;Ah_q%cCR?kNBC?@T-j|EJB>X}tkhfy zL(u4b7VZ)Cy=gYgbxZp-7Xpm@C`eUnh?<71; z`1FL+vp(^D$@u7{ndH3$-s^V&vaqxNP7 zIlQhQ8;PfFvX>|0-bCNXP6sDzD7!C<^2P;qSmULMl^k&JoSmhMUsqmgW1R97Vt@S_ z6))|uVDVnT%|#%mcr~|y1Kl{Q9d2;={gKk8KfvGske;->djS9HB-0{cBj#e^T=Nz~ zMXnEG@u&}=Tp_kZfqDvhGm5!LjDlt=wQkJb(pRf0p{H4u+WxQ--587XPqb%$NLN;X z8TZp`njh>~a#Wq=ws!h#;T~2*U$9K5xHl5P7Hos=SNodo1!$Hw3e}J*J*yhB>7*K3 zxrTYei+-0VNw{H+QHZ&3ll`fC8pofSZ#iEYQ2@?}iW>xqkx7Ks6~q{9Z{)Dnks<)M z5=+EjuL8Y{aZx|%DE6j8hahE!i@Dal=s{okU>;3CehS*Mr$ElGv=tOJvis)qvRPTr zCjigbl?iZ3nQO|LMG_0>L%b#&0^LzF5^{~00`{q8Un;PW!B@c1+0v9?f-7Ezi@d485~TCKq}LTOHX@flW2{bwIHqe8Kp9vIePTD^Cyp=I68Co)N@DA zKYR2O$Y|ymqDb(Kz?_no=jdFUCa?*t0Yf-W9AG%ka-2Rk?(hUSDNM$GReVxguZCKQ zdZx7nNTqX#wb69#U1)h&Z!Fe~VhNxy_thJQuq`UB!rq8`9=EIWSZ<|Mf?n`8c;a3? zC9k-dGyG@}khVknBFHp|O5Fj@DfPxDXHFkF^X&N}hdwc*X|kfWA8i$2?aptYc)`Ak;Wi_~HHl)UoqZYkznK(>Gg_=>&ku1ux-5fSiqA^GZu$2eU6$s7}$-g
#rl#8)o|fyLP8~oN~6IFZX1f)*i9aQ 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 literal 0 HcmV?d00001 diff --git a/plugins/pi.token-insights/host-read.js b/plugins/pi.token-insights/host-read.js index cba1e77..63a4853 100644 --- a/plugins/pi.token-insights/host-read.js +++ b/plugins/pi.token-insights/host-read.js @@ -10,9 +10,11 @@ * them here and hands the result to the panel through plugin settings. * * Everything here is strictly read-only and stays on this device: + * pi.usage.listTurns (official host contract) → completed-turn usage * ~/.pi-desktop/pi.sqlite kv(ns='app', key='app') → { theme, language } * providers(id, name) → display names - * turns (completed usage) → subagent-inclusive remainders + * turns (completed usage) → subagent-inclusive + * remainders, only on hosts without the API * ~/.pi-desktop/plugins/registry.json + the theme plugin's manifest + CSS * * No message text, tool arguments, session ids or project paths are touched, no @@ -288,12 +290,135 @@ function readHostAppearance(hostRoot) { return value; } +/** + * Completed-turn usage from the host. Prefers the official read-only + * `pi.usage.listTurns` contract (which already excludes soft-deleted sessions + * and carries no message text); hosts without it fall back to the direct + * database read below. Either way only token counts and ids leave this module. + */ +async function readCompletedTurnUsage(hostRoot, pi) { + const listTurns = typeof pi !== "undefined" ? pi?.usage?.listTurns : undefined; + if (typeof listTurns === "function") { + try { + return await readCompletedTurnUsageViaApi(pi); + } catch (error) { + // An older or momentarily unhappy host must never cost the panel its + // PI-Desktop numbers, so the database read stays as a safety net. + warnApiFallbackOnce(error); + } + } + return readCompletedTurnUsageFromDb(hostRoot); +} + +/** The largest window the contract accepts, so walks never exceed one window. */ +const API_WINDOW_MS = 365 * 24 * 60 * 60 * 1000; +/** A window with no completed turns means everything older is done; 8 windows ≈ 8 years. */ +const API_MAX_WINDOWS = 8; +/** The contract's page ceiling; the fewest round trips per window. */ +const API_PAGE_LIMIT = 500; + +let apiFallbackWarned = false; + +function warnApiFallbackOnce(error) { + if (apiFallbackWarned) return; + apiFallbackWarned = true; + try { + console.warn( + `[pi.token-insights] pi.usage.listTurns failed (${String(error?.message || error)}); reading the host database instead.`, + ); + } catch { + /* a broken console must not break the scan */ + } +} + +/** + * Walks completed turns backward through the official `pi.usage.listTurns` + * contract: at most 365 days per window, every page of each window via the + * keyset cursor, and the walk stops at the first window with no rows at all. + * A `turnId` set dedupes across windows so a turn straddling a boundary can + * never be counted twice. Returns the same `{ events, diagnostics }` shape as + * the database reader, with `filesScanned` counting pages fetched. + */ +async function readCompletedTurnUsageViaApi(pi) { + const result = { + events: [], + diagnostics: { sourceId: "pi-desktop", filesScanned: 0, filesSkipped: 0, malformedLines: 0, usageMessages: 0 }, + }; + const seenTurnIds = new Set(); + let toMs = Date.now(); + for (let windowIndex = 0; windowIndex < API_MAX_WINDOWS; windowIndex += 1) { + // Inclusive endpoints, so the span stays within the contract's 365-day cap. + const fromMs = toMs - API_WINDOW_MS + 1; + let cursor = null; + let rowsInWindow = 0; + do { + const input = { fromMs, toMs, limit: API_PAGE_LIMIT }; + // `cursor` is optional in the contract; a strict host may reject null. + if (cursor) input.cursor = cursor; + const page = await pi.usage.listTurns(input); + result.diagnostics.filesScanned += 1; + for (const row of page?.turns || []) { + rowsInWindow += 1; + const turnId = String(row?.turnId || ""); + if (turnId) { + if (seenTurnIds.has(turnId)) continue; + seenTurnIds.add(turnId); + } + const event = turnEventFromApiRow(row); + if (!event) continue; + result.events.push(event); + result.diagnostics.usageMessages += 1; + } + cursor = page?.nextCursor || null; + } while (cursor); + if (rowsInWindow === 0) break; + toMs = fromMs - 1; + } + return result; +} + +/** + * One contract row → the event shape the aggregator already consumes. + * The contract carries no `total`, so it is the sum of the five components; + * rows with no tokens at all are skipped, exactly like the database reader. + */ +function turnEventFromApiRow(row) { + const n = (value) => { + const parsedNumber = Number(value); + return Number.isFinite(parsedNumber) ? Math.max(0, parsedNumber) : 0; + }; + const input = n(row?.inputTokens); + const output = n(row?.outputTokens); + const cacheRead = n(row?.cacheReadTokens); + const cacheWrite = n(row?.cacheWriteTokens); + const reasoning = n(row?.reasoningTokens); + if (!input && !output && !cacheRead && !cacheWrite && !reasoning) return null; + const timestamp = Number(row?.endedAt); + const sessionId = String(row?.sessionId || "").trim(); + if (!sessionId || !Number.isFinite(timestamp)) return null; + return { + sourceId: "pi-desktop", + sessionId: `pi-desktop:${sessionId}`, + timestamp, + modelId: String(row.modelId || "Unknown model"), + providerId: String(row.providerId || "Unknown provider"), + tokens: { + input, + output, + cacheRead, + cacheWrite, + reasoning, + total: input + output + cacheRead + cacheWrite + reasoning, + }, + }; +} + /** * Completed-turn usage from the host database. Only token columns and ids; - * no message text. Used to fold subagent spend that never landed on - * transcript `message.usage` into the PI-Desktop scan. + * no message text. Fallback for hosts without `pi.usage.listTurns` — the API + * path is preferred because it also excludes soft-deleted sessions. */ -function readCompletedTurnUsage(hostRoot) { +function readCompletedTurnUsageFromDb(hostRoot) { const result = { events: [], diagnostics: { sourceId: "pi-desktop", filesScanned: 0, filesSkipped: 0, malformedLines: 0, usageMessages: 0 }, @@ -362,6 +487,8 @@ function tokensFromTurnRow(row) { module.exports = { hostRootFromDataPath, readCompletedTurnUsage, + readCompletedTurnUsageFromDb, + readCompletedTurnUsageViaApi, readHostAppearance, readProviderLabels, __test: { diff --git a/plugins/pi.token-insights/main.js b/plugins/pi.token-insights/main.js index d378c8b..bc264dd 100644 --- a/plugins/pi.token-insights/main.js +++ b/plugins/pi.token-insights/main.js @@ -93,7 +93,9 @@ async function scanEvents(progress) { scanCodexDirectory(roots.codex, progress), scanOpenCodeDirectory(roots.openCode, progress), ]); - const turns = readCompletedTurnUsage(host); + // The official pi.usage.listTurns contract when the host offers it, with the + // direct database read kept inside as the fallback for older hosts. + const turns = await readCompletedTurnUsage(host, pi); const piDesktop = mergePiDesktopTurnRemainder(piJsonl, turns.events); if (turns.diagnostics?.filesScanned) { piDesktop.diagnostics.filesScanned += turns.diagnostics.filesScanned; diff --git a/plugins/pi.token-insights/manifest.json b/plugins/pi.token-insights/manifest.json index bf5e8c1..a102ec2 100644 --- a/plugins/pi.token-insights/manifest.json +++ b/plugins/pi.token-insights/manifest.json @@ -2,7 +2,7 @@ "schemaVersion": 1, "id": "pi.token-insights", "name": "Token Insights", - "version": "0.4.8", + "version": "0.5.0", "description": "A private local dashboard for the tokens you spend across PI-Desktop, Claude Code, Codex and OpenCode: follows the app's theme and language, filters by tool, model, provider, date range or keyword, and keeps a streak and milestone worth looking at.", "i18n": { "en": { @@ -23,7 +23,7 @@ "developer-tools", "official" ], - "changelog": "Release 0.4.8: Fold PI-Desktop completed-turn totals from the host database into the dashboard, so subagent spend that never lands on parent message.usage still appears without double-counting transcript rows. Settings no longer has a Usage page; this panel is the heatmap. Release 0.4.8:把宿主已完成回合的 token 合计折入仪表盘,转录本 message.usage 未计入的子智能体用量会补上且不与 JSONL 重复。设置页不再有用量入口,热力图以本面板为准。 Release 0.4.7: Fix the extra empty right-side rail on Windows by keeping the stable scrollbar gutter on the dashboard scroller instead of reserving it again on the root viewport. Release 0.4.7:修复 Windows 右侧多出空白侧栏的问题:仅在仪表盘滚动容器保留稳定滚动条槽位,不再在根视口重复预留。 Release 0.4.6: Fix the titlebar's appearance and refresh buttons overlapping the host window-control capsule. The panel paints through the 46px band under v3 chrome, but the capsule still floats over the top-right corner, so both buttons sat under it and stopped being clickable. The titlebar now reserves the capsule's 104px corner (kept at narrow widths too, where the gutter override previously reset it), the appearance menu stays anchored to the buttons that open it, and a long title truncates instead of running under the reserve. Release 0.4.6:修复标题栏的外观与刷新按钮与宿主窗口控制胶囊重叠的问题。v3 chrome 下面板可绘制到 46px 顶栏,但胶囊仍浮于右上角,导致两个按钮被遮挡且无法点击。标题栏现为胶囊保留 104px 右侧空间(窄宽度下的 gutter 覆盖此前会重置该保留值,现已保留),外观菜单继续对齐触发按钮,过长标题以省略号截断。 Release 0.4.5: Migrates the panel to v3 paint-through chrome so top-band controls remain clickable while blank space remains draggable. Release 0.4.4: Fix the window-control capsule being drawn with default black-on-white colors on a panel's very first open — with no cached appearance, data-theme stayed unset until the host answered, so the host's one-shot color snapshot at DOMContentLoaded missed; boot now falls back to the OS palette synchronously while content stays cloaked. Also stop startAppearanceWatch from resetting the appearance fingerprint, which made every open-command run rewrite hostAppearance. Release 0.4.4:修复首次打开面板时窗口控制胶囊配色错误的问题——无缓存外观时 data-theme 未设置,宿主在 DOMContentLoaded 的取色快照拿到浏览器默认黑/白,现同步回退 OS 配色(内容仍按 data-booting 隐藏至真实外观到达);修复 startAppearanceWatch 重置外观指纹导致每次打开命令都重复写入 hostAppearance 的问题。 Release 0.4.3: Align the panel with PI-Desktop's cross-platform 46px host drag band and minimal three-button window-control capsule. Fixes two visible faults: elements toggled with the hidden attribute were being kept on screen by their own CSS display, so the loading skeleton stayed over real data, an empty chip strip and the custom date inputs were always drawn, and the last scan sentence lingered; and the from/to fields now always show the window the selected range covers, so picking 7D/90D/ALL keeps them in step and switching to Custom inherits what you were looking at. Includes 0.4.1's scan progress with a real file count, stale-scan recovery, visibility-based polling and debounced source watching, on top of 0.4.0's theme/language following, filters and streak layer.", + "changelog": "Release 0.5.0: The PI-Desktop data source now goes through the official pi.usage.listTurns contract (requires a PI-Desktop build with that API; declare the usage.read permission) instead of reading the host's pi.sqlite directly, which also fixes the counting gap where soft-deleted sessions were still included. Hosts without the contract automatically fall back to the previous database path. Release 0.5.0:PI-Desktop 数据源迁移到正式的 pi.usage.listTurns 契约(需宿主提供该 API;声明 usage.read 权限),不再直读宿主 pi.sqlite,同时修复软删除会话仍被计入的口径缺口;无该契约的旧宿主自动回退原数据库路径。 Release 0.4.8: Fold PI-Desktop completed-turn totals from the host database into the dashboard, so subagent spend that never lands on parent message.usage still appears without double-counting transcript rows. Settings no longer has a Usage page; this panel is the heatmap. Release 0.4.8:把宿主已完成回合的 token 合计折入仪表盘,转录本 message.usage 未计入的子智能体用量会补上且不与 JSONL 重复。设置页不再有用量入口,热力图以本面板为准。 Release 0.4.7: Fix the extra empty right-side rail on Windows by keeping the stable scrollbar gutter on the dashboard scroller instead of reserving it again on the root viewport. Release 0.4.7:修复 Windows 右侧多出空白侧栏的问题:仅在仪表盘滚动容器保留稳定滚动条槽位,不再在根视口重复预留。 Release 0.4.6: Fix the titlebar's appearance and refresh buttons overlapping the host window-control capsule. The panel paints through the 46px band under v3 chrome, but the capsule still floats over the top-right corner, so both buttons sat under it and stopped being clickable. The titlebar now reserves the capsule's 104px corner (kept at narrow widths too, where the gutter override previously reset it), the appearance menu stays anchored to the buttons that open it, and a long title truncates instead of running under the reserve. Release 0.4.6:修复标题栏的外观与刷新按钮与宿主窗口控制胶囊重叠的问题。v3 chrome 下面板可绘制到 46px 顶栏,但胶囊仍浮于右上角,导致两个按钮被遮挡且无法点击。标题栏现为胶囊保留 104px 右侧空间(窄宽度下的 gutter 覆盖此前会重置该保留值,现已保留),外观菜单继续对齐触发按钮,过长标题以省略号截断。 Release 0.4.5: Migrates the panel to v3 paint-through chrome so top-band controls remain clickable while blank space remains draggable. Release 0.4.4: Fix the window-control capsule being drawn with default black-on-white colors on a panel's very first open — with no cached appearance, data-theme stayed unset until the host answered, so the host's one-shot color snapshot at DOMContentLoaded missed; boot now falls back to the OS palette synchronously while content stays cloaked. Also stop startAppearanceWatch from resetting the appearance fingerprint, which made every open-command run rewrite hostAppearance. Release 0.4.4:修复首次打开面板时窗口控制胶囊配色错误的问题——无缓存外观时 data-theme 未设置,宿主在 DOMContentLoaded 的取色快照拿到浏览器默认黑/白,现同步回退 OS 配色(内容仍按 data-booting 隐藏至真实外观到达);修复 startAppearanceWatch 重置外观指纹导致每次打开命令都重复写入 hostAppearance 的问题。 Release 0.4.3: Align the panel with PI-Desktop's cross-platform 46px host drag band and minimal three-button window-control capsule. Fixes two visible faults: elements toggled with the hidden attribute were being kept on screen by their own CSS display, so the loading skeleton stayed over real data, an empty chip strip and the custom date inputs were always drawn, and the last scan sentence lingered; and the from/to fields now always show the window the selected range covers, so picking 7D/90D/ALL keeps them in step and switching to Custom inherits what you were looking at. Includes 0.4.1's scan progress with a real file count, stale-scan recovery, visibility-based polling and debounced source watching, on top of 0.4.0's theme/language following, filters and streak layer.", "safetyNotes": "Reads only local metadata, always read-only, always on this device. Usage comes from the tool metadata files PI-Desktop, Claude Code, Codex and OpenCode already write; appearance (theme, language, active plugin theme CSS) and provider display names come from PI-Desktop's own local records. It never reads message text, tool arguments, project paths or credentials, never keeps a full session id (only an 8-character prefix), never writes anywhere except its own plugin settings, and makes no network request.", "ui": { "panel": "renderer/index.html", @@ -104,7 +104,8 @@ }, "permissions": [ "ui.panel", - "agent.tool.register" + "agent.tool.register", + "usage.read" ], "engines": { "piDesktop": ">=0.2.9" diff --git a/tests/token-insights.test.mjs b/tests/token-insights.test.mjs index 31406c3..2b45047 100644 --- a/tests/token-insights.test.mjs +++ b/tests/token-insights.test.mjs @@ -12,6 +12,7 @@ const plugin = require("../plugins/pi.token-insights/main.js"); const manifest = JSON.parse( readFileSync(join(here, "../plugins/pi.token-insights/manifest.json"), "utf8"), ); +const hostReadSource = readFileSync(join(here, "../plugins/pi.token-insights/host-read.js"), "utf8"); const panelSource = readFileSync(join(here, "../plugins/pi.token-insights/renderer/panel.js"), "utf8"); const panelCss = readFileSync(join(here, "../plugins/pi.token-insights/renderer/panel.css"), "utf8"); const panelPolishCss = readFileSync( @@ -45,19 +46,34 @@ function usageRecord({ createdAt, modelId = "alpha", providerId = "local", usage }; } -function waitForBackgroundScan() { - return new Promise((resolve) => setTimeout(resolve, 10)); +/** + * The load scan finishes in the background, and a fixed sleep races it on slow + * filesystems, so poll for the terminal scanState the scan publishes instead. + */ +async function waitForBackgroundScan(scanStatus) { + const deadline = Date.now() + 5_000; + while (scanStatus() !== "ready" && scanStatus() !== "failed" && Date.now() < deadline) { + await new Promise((resolve) => setTimeout(resolve, 5)); + } + const status = scanStatus(); + if (status !== "ready" && status !== "failed") throw new Error("background scan did not settle"); } -test("manifest declares the independent scanner and minimal host permissions", () => { - assert.equal(manifest.version, "0.4.8"); - assert.deepEqual(manifest.permissions, ["ui.panel", "agent.tool.register"]); +test("manifest declares the official usage contract permission and minimal host access", () => { + assert.equal(manifest.version, "0.5.0"); + assert.deepEqual(manifest.permissions, ["ui.panel", "agent.tool.register", "usage.read"]); assert.equal(manifest.engines.piDesktop, ">=0.2.9"); assert.deepEqual( manifest.contributes.agentTools[0].schema.properties.groupBy.enum, ["model", "provider", "source", "day", "session"], ); - assert.doesNotMatch(JSON.stringify(manifest), /usage\.read|project rankings|price table/i); + // The July 2025 snapshot banned usage.read because the host's old usage API + // had been removed. The official pi.usage.listTurns contract is back, so the + // manifest must declare it and the host reader must go through that contract + // instead of reading pi.sqlite directly. + assert.match(JSON.stringify(manifest), /usage\.read/); + assert.match(hostReadSource, /pi\.usage\.listTurns/); + assert.doesNotMatch(JSON.stringify(manifest), /project rankings|price table/i); assert.match(panelSource, /function compact\(value\)/); assert.match(panelSource, /sourcesTitle: "Tools"/); assert.match(panelSource, /sourcesTitle: "工具"/); @@ -231,7 +247,7 @@ test("completed-turn rows fill a session-day with no transcript", () => { assert.equal(merged.events[0].tokens.total, 10); }); -test("readCompletedTurnUsage maps turn rows without message text", () => { +test("readCompletedTurnUsage falls back to the host database and maps turn rows without message text", async () => { let sqlite; try { sqlite = require("node:sqlite"); @@ -278,7 +294,8 @@ test("readCompletedTurnUsage maps turn rows without message text", () => { endedAt, ); db.close(); - const result = plugin.__test.readCompletedTurnUsage(fixture.root); + // No `pi` argument: the wrapper must route to the database reader. + const result = await plugin.__test.readCompletedTurnUsage(fixture.root); assert.equal(result.events.length, 1); assert.equal(result.events[0].sessionId, "pi-desktop:sess-uuid"); assert.deepEqual(result.events[0].tokens, { @@ -295,6 +312,216 @@ test("readCompletedTurnUsage maps turn rows without message text", () => { } }); +function apiTurn({ + turnId, + sessionId, + endedAt, + modelId, + providerId, + input = 0, + output = 0, + cacheRead = 0, + cacheWrite = 0, + reasoning = 0, +}) { + return { + turnId, + sessionId, + sessionTitle: "must not leak", + projectId: "must-not-leak", + providerId, + modelId, + startedAt: endedAt - 1, + endedAt, + inputTokens: input, + outputTokens: output, + cacheReadTokens: cacheRead, + cacheWriteTokens: cacheWrite, + reasoningTokens: reasoning, + }; +} + +/** A fake host whose listTurns replays pages in call order and records inputs. */ +function createFakeUsageHost(pages) { + const calls = []; + return { + calls, + pi: { + usage: { + listTurns: async (input) => { + calls.push({ ...input }); + const page = pages[calls.length - 1]; + if (!page) throw new Error(`unexpected listTurns call #${calls.length}`); + return page; + }, + }, + }, + }; +} + +test("readCompletedTurnUsage walks listTurns pages, dedupes, and stops at an empty window", async () => { + const endedAt = Date.now() - 1_000; + const host = createFakeUsageHost([ + // Window 1, page 1: one normal row, one all-zero row, then a cursor. + { + turns: [ + apiTurn({ + turnId: "t1", + sessionId: "sess-1", + endedAt, + modelId: "alpha", + providerId: "local", + input: 10, + output: 5, + cacheRead: 2, + cacheWrite: 1, + reasoning: 3, + }), + apiTurn({ turnId: "t-zero", sessionId: "sess-zero", endedAt }), + ], + nextCursor: "page-2", + }, + // Window 1, page 2: t1 repeats (as if straddling a boundary) and must be + // dropped; a row without a session id is unmappable and dropped too. + { + turns: [ + apiTurn({ + turnId: "t1", + sessionId: "sess-1", + endedAt, + modelId: "alpha", + providerId: "local", + input: 10, + output: 5, + cacheRead: 2, + cacheWrite: 1, + reasoning: 3, + }), + apiTurn({ turnId: "t2", sessionId: "sess-2", endedAt: endedAt - 5, input: 1 }), + apiTurn({ turnId: "t-nosess", sessionId: "", endedAt, input: 7 }), + ], + nextCursor: null, + }, + // Window 2 comes back empty: the backward walk stops here. + { turns: [], nextCursor: null }, + ]); + + const result = await plugin.__test.readCompletedTurnUsage("/nonexistent-host", host.pi); + assert.equal(result.events.length, 2); + assert.equal(result.events[0].sourceId, "pi-desktop"); + assert.equal(result.events[0].sessionId, "pi-desktop:sess-1"); + assert.equal(result.events[0].timestamp, endedAt); + assert.equal(result.events[0].modelId, "alpha"); + assert.equal(result.events[0].providerId, "local"); + // The contract carries no total, so it is the sum of the five components. + assert.deepEqual(result.events[0].tokens, { + input: 10, + output: 5, + cacheRead: 2, + cacheWrite: 1, + reasoning: 3, + total: 21, + }); + assert.equal(result.events[1].sessionId, "pi-desktop:sess-2"); + assert.equal(result.events[1].modelId, "Unknown model"); + assert.equal(result.events[1].providerId, "Unknown provider"); + assert.equal(result.events[1].tokens.total, 1); + // Contract-only fields (session titles, project ids) never reach the events. + assert.doesNotMatch(JSON.stringify(result), /must not leak|must-not-leak/i); + + // Pagination: 500-row pages within a contract-legal window, the cursor + // threaded back, the next window abutting the previous one, and no calls + // after the empty window. + const dayMs = 24 * 60 * 60 * 1000; + assert.equal(host.calls.length, 3); + for (const call of host.calls) { + assert.equal(call.limit, 500); + assert.ok(call.toMs - call.fromMs < 365 * dayMs); + } + assert.equal(host.calls[0].cursor, undefined); + assert.equal(host.calls[1].cursor, "page-2"); + assert.equal(host.calls[1].fromMs, host.calls[0].fromMs); + assert.equal(host.calls[1].toMs, host.calls[0].toMs); + assert.equal(host.calls[2].toMs, host.calls[0].fromMs - 1); + assert.equal(host.calls[2].cursor, undefined); + + assert.deepEqual(result.diagnostics, { + sourceId: "pi-desktop", + filesScanned: 3, + filesSkipped: 0, + malformedLines: 0, + usageMessages: 2, + }); +}); + +test("the listTurns walk stops after eight windows even when every window has rows", async () => { + const pages = []; + for (let windowIndex = 0; windowIndex < 12; windowIndex += 1) { + pages.push({ + turns: [ + apiTurn({ turnId: `t-${windowIndex}`, sessionId: `sess-${windowIndex}`, endedAt: 1_000, input: 1 }), + ], + nextCursor: null, + }); + } + const host = createFakeUsageHost(pages); + const result = await plugin.__test.readCompletedTurnUsage("/nonexistent-host", host.pi); + assert.equal(host.calls.length, 8); + assert.equal(result.events.length, 8); +}); + +test("readCompletedTurnUsage recovers through the database when the usage contract is missing or throws", async () => { + let sqlite; + try { + sqlite = require("node:sqlite"); + } catch { + return; + } + if (!sqlite?.DatabaseSync) return; + const fixture = createFixture(); + try { + const db = new sqlite.DatabaseSync(join(fixture.root, "pi.sqlite")); + db.exec(`CREATE TABLE turns ( + id TEXT PRIMARY KEY, + session_id TEXT, + status TEXT, + provider_id TEXT, + model_id TEXT, + input_tokens INTEGER, + output_tokens INTEGER, + usage_json TEXT, + started_at INTEGER, + ended_at INTEGER + )`); + const endedAt = new Date(2026, 6, 30, 12).getTime(); + db.prepare( + `INSERT INTO turns (id, session_id, status, provider_id, model_id, input_tokens, output_tokens, usage_json, started_at, ended_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + ).run("t1", "sess-fallback", "completed", "local", "alpha", 10, 5, JSON.stringify({ totalTokens: 17 }), endedAt - 1, endedAt); + db.close(); + + // A host without pi.usage at all routes straight to the database reader. + const withoutUsage = await plugin.__test.readCompletedTurnUsage(fixture.root, {}); + assert.equal(withoutUsage.events.length, 1); + assert.equal(withoutUsage.events[0].sessionId, "pi-desktop:sess-fallback"); + assert.equal(withoutUsage.events[0].tokens.total, 17); + + // A host whose listTurns throws must never cost the panel its numbers. + const broken = { + usage: { + listTurns: async () => { + throw new Error("no contract on this host"); + }, + }, + }; + const recovered = await plugin.__test.readCompletedTurnUsage(fixture.root, broken); + assert.equal(recovered.events.length, 1); + assert.equal(recovered.events[0].sessionId, "pi-desktop:sess-fallback"); + } finally { + fixture.cleanup(); + } +}); + test("adapters normalize Claude Code, Codex, and OpenCode without cumulative Codex double-counting", async () => { const fixture = createFixture(); const claude = join(fixture.root, "claude-projects", "project-a"); @@ -398,6 +625,7 @@ test("summary groups models, providers, sessions, time buckets, and streaks", () test("on-load writes a snapshot before opening the panel and the tool groups by provider", async () => { const fixture = createFixture(); const calls = { registered: [], unregistered: [], timeline: [], facts: [] }; + let scanStatus = null; const previousPi = globalThis.pi; try { mkdirSync(fixture.dataPath, { recursive: true }); @@ -422,6 +650,7 @@ test("on-load writes a snapshot before opening the panel and the tool groups by setSettings: async (value) => { calls.timeline.push("settings"); if (value.usageFacts) calls.facts.push(value.usageFacts); + if (value.scanState?.status) scanStatus = value.scanState.status; }, }, commands: { @@ -439,7 +668,7 @@ test("on-load writes a snapshot before opening the panel and the tool groups by }; await plugin.onLoad(); - await waitForBackgroundScan(); + await waitForBackgroundScan(() => scanStatus); const command = calls.registered.find((item) => item.type === "command").value; const tool = calls.registered.find((item) => item.type === "tool").value; calls.timeline.length = 0;