cartethyia 2.4.5 → 2.4.6
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/.env.example +4 -1
- package/README.md +18 -0
- package/dist/dashboard/assets/AccountCooldown-ChpdzxRH.js.gz +0 -0
- package/dist/dashboard/assets/{BannedPage-DUrXpp-X.js → BannedPage-QhvbJB9o.js} +1 -1
- package/dist/dashboard/assets/BannedPage-QhvbJB9o.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolDetailPage-BzGevuql.js → CliToolDetailPage-Bcy150Jo.js} +1 -1
- package/dist/dashboard/assets/CliToolDetailPage-Bcy150Jo.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolsPage-BaRM_7We.js → CliToolsPage-DyNXefph.js} +1 -1
- package/dist/dashboard/assets/CliToolsPage-DyNXefph.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-Cak0ljnO.js +1 -0
- package/dist/dashboard/assets/CombosPage-Cak0ljnO.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-CATpAEOz.js +1 -0
- package/dist/dashboard/assets/ConsoleLogPage-CATpAEOz.js.gz +0 -0
- package/dist/dashboard/assets/{CustomizationPage-DqUGcoH0.js → CustomizationPage-D6jEU7nL.js} +1 -1
- package/dist/dashboard/assets/CustomizationPage-D6jEU7nL.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-CZkpn0a7.js +1 -0
- package/dist/dashboard/assets/HealthEventsModal-CZkpn0a7.js.gz +0 -0
- package/dist/dashboard/assets/{LoginPage-DbhTGfhb.js → LoginPage-BJlMVN4U.js} +1 -1
- package/dist/dashboard/assets/LoginPage-BJlMVN4U.js.gz +0 -0
- package/dist/dashboard/assets/ModelPicker-Br3CXEau.js +1 -0
- package/dist/dashboard/assets/ModelPicker-Br3CXEau.js.gz +0 -0
- package/dist/dashboard/assets/{OverviewPage-C3NIzsDx.js → OverviewPage-CQxXbo2g.js} +1 -1
- package/dist/dashboard/assets/OverviewPage-CQxXbo2g.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage--0RBNh99.js +3 -0
- package/dist/dashboard/assets/ProviderDetailPage--0RBNh99.js.gz +0 -0
- package/dist/dashboard/assets/ProviderIcon-paOCq8I0.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-1M-C3mbu.js +1 -0
- package/dist/dashboard/assets/ProvidersPage-1M-C3mbu.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-CXiumA53.js +7 -0
- package/dist/dashboard/assets/ProxyPage-CXiumA53.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-BGDIfIYP.js +1 -0
- package/dist/dashboard/assets/QuotaPage-BGDIfIYP.js.gz +0 -0
- package/dist/dashboard/assets/{SettingsPage-YxRml8AT.js → SettingsPage-BHW4Zt06.js} +1 -1
- package/dist/dashboard/assets/SettingsPage-BHW4Zt06.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-VOYC_lIa.js +1 -0
- package/dist/dashboard/assets/SetupPage-VOYC_lIa.js.gz +0 -0
- package/dist/dashboard/assets/SortableList-DpOTnGuH.js +1 -0
- package/dist/dashboard/assets/SortableList-DpOTnGuH.js.gz +0 -0
- package/dist/dashboard/assets/StudioPage-BNIDLiU6.js +31 -0
- package/dist/dashboard/assets/StudioPage-BNIDLiU6.js.gz +0 -0
- package/dist/dashboard/assets/ToolIcon-K1ckcDKg.js.gz +0 -0
- package/dist/dashboard/assets/{UsagePage-Djes-Epy.js → UsagePage-BAFaSDQV.js} +14 -14
- package/dist/dashboard/assets/UsagePage-BAFaSDQV.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-BETfaMYZ.js +1 -0
- package/dist/dashboard/assets/api-keys-BETfaMYZ.js.gz +0 -0
- package/dist/dashboard/assets/app-BLCvGdzb.css +2 -0
- package/dist/dashboard/assets/app-BLCvGdzb.css.gz +0 -0
- package/dist/dashboard/assets/{app-BDJWMThu.js → app-Cjq7nrwu.js} +3 -3
- package/dist/dashboard/assets/app-Cjq7nrwu.js.gz +0 -0
- package/dist/dashboard/assets/arrow-left-CpBxkoxI.js.gz +0 -0
- package/dist/dashboard/assets/arrow-right-Dl5yKnp8.js.gz +0 -0
- package/dist/dashboard/assets/{assert-Bwh6DJTK.js → assert-7J6_LVD3.js} +1 -1
- package/dist/dashboard/assets/assert-7J6_LVD3.js.gz +0 -0
- package/dist/dashboard/assets/backup-file-gdKIlg8U.js.gz +0 -0
- package/dist/dashboard/assets/{badge-BwmwEOgA.js → badge-BTyGk_DL.js} +1 -1
- package/dist/dashboard/assets/badge-BTyGk_DL.js.gz +0 -0
- package/dist/dashboard/assets/{banner-BKBfKTqn.js → banner-bujTWYM4.js} +1 -1
- package/dist/dashboard/assets/banner-bujTWYM4.js.gz +0 -0
- package/dist/dashboard/assets/brain-Cwed_72z.js.gz +0 -0
- package/dist/dashboard/assets/{card-BTdbRFFg.js → card-B0Bnx5CA.js} +1 -1
- package/dist/dashboard/assets/card-B0Bnx5CA.js.gz +0 -0
- package/dist/dashboard/assets/check-D69FzdvC.js.gz +0 -0
- package/dist/dashboard/assets/chevron-right-BXIy5l3h.js.gz +0 -0
- package/dist/dashboard/assets/circle-alert-CntYHV5v.js.gz +0 -0
- package/dist/dashboard/assets/circle-check-BU3v_mFW.js.gz +0 -0
- package/dist/dashboard/assets/cli-tools-DE2ug812.js +1 -0
- package/dist/dashboard/assets/cli-tools-DE2ug812.js.gz +0 -0
- package/dist/dashboard/assets/clipboard-button-R_rArH8S.js +1 -0
- package/dist/dashboard/assets/clipboard-button-R_rArH8S.js.gz +0 -0
- package/dist/dashboard/assets/cog-CdWhsajs.js.gz +0 -0
- package/dist/dashboard/assets/copy-ngmQ9zcZ.js.gz +0 -0
- package/dist/dashboard/assets/{dialog-iocKA3jr.js → dialog-BWIP_EvW.js} +1 -1
- package/dist/dashboard/assets/dialog-BWIP_EvW.js.gz +0 -0
- package/dist/dashboard/assets/dialog-actions-B1axpEg7.js.gz +0 -0
- package/dist/dashboard/assets/download-A0SW9OCW.js.gz +0 -0
- package/dist/dashboard/assets/download-dRFo5_Wu.js.gz +0 -0
- package/dist/dashboard/assets/{drawer-CDKOTZdY.js → drawer-BKcgVugF.js} +1 -1
- package/dist/dashboard/assets/drawer-BKcgVugF.js.gz +0 -0
- package/dist/dashboard/assets/entry-7dpgvpJ3.js +4 -0
- package/dist/dashboard/assets/entry-7dpgvpJ3.js.gz +0 -0
- package/dist/dashboard/assets/entry-CcWkGR4a.js +1 -0
- package/dist/dashboard/assets/entry-CcWkGR4a.js.gz +0 -0
- package/dist/dashboard/assets/entry-kTiTFrvh.js +1 -0
- package/dist/dashboard/assets/entry-kTiTFrvh.js.gz +0 -0
- package/dist/dashboard/assets/eye-C_hawfH4.js.gz +0 -0
- package/dist/dashboard/assets/eye-off-Bk5g2luF.js.gz +0 -0
- package/dist/dashboard/assets/fingerprint-pattern-BWuHtbW4.js.gz +0 -0
- package/dist/dashboard/assets/gauge-BbIMy7MB.js.gz +0 -0
- package/dist/dashboard/assets/github-badge-Booc5mNe.js +1 -0
- package/dist/dashboard/assets/github-badge-Booc5mNe.js.gz +0 -0
- package/dist/dashboard/assets/image-BPaouViG.js.gz +0 -0
- package/dist/dashboard/assets/{index-C4hXfAMs.js → index-D70pChfn.js} +2 -2
- package/dist/dashboard/assets/index-D70pChfn.js.gz +0 -0
- package/dist/dashboard/assets/{inline-DCejSLlA.js → inline-D1TCqM2D.js} +1 -1
- package/dist/dashboard/assets/inline-D1TCqM2D.js.gz +0 -0
- package/dist/dashboard/assets/{input-Bn4_kNav.js → input-B9oyU93y.js} +1 -1
- package/dist/dashboard/assets/input-B9oyU93y.js.gz +0 -0
- package/dist/dashboard/assets/jsx-runtime-CXUMngI_.js.gz +0 -0
- package/dist/dashboard/assets/key-round-UkavsGxp.js.gz +0 -0
- package/dist/dashboard/assets/layers-2-glBbjl.js.gz +0 -0
- package/dist/dashboard/assets/{layout-Bv-NxqZ5.js → layout-BArzXirQ.js} +1 -1
- package/dist/dashboard/assets/layout-BArzXirQ.js.gz +0 -0
- package/dist/dashboard/assets/{list-row-BLA0sldD.js → list-row-BVZzaoHF.js} +1 -1
- package/dist/dashboard/assets/list-row-BVZzaoHF.js.gz +0 -0
- package/dist/dashboard/assets/{live-YlifPYox.js → live-EmuIayqp.js} +1 -1
- package/dist/dashboard/assets/live-EmuIayqp.js.gz +0 -0
- package/dist/dashboard/assets/log-in-BmRjy60c.js.gz +0 -0
- package/dist/dashboard/assets/message-circle-CCxGwRvv.js.gz +0 -0
- package/dist/dashboard/assets/{model-bans-CVvHZ-qz.js → model-bans-CSPQ9xB0.js} +1 -1
- package/dist/dashboard/assets/model-bans-CSPQ9xB0.js.gz +0 -0
- package/dist/dashboard/assets/mutation-BkoLM5Ba.js +1 -0
- package/dist/dashboard/assets/mutation-BkoLM5Ba.js.gz +0 -0
- package/dist/dashboard/assets/numbers-CFL4_Dqc.js.gz +0 -0
- package/dist/dashboard/assets/pencil-768ysRAr.js.gz +0 -0
- package/dist/dashboard/assets/plus-DIdX_g-x.js.gz +0 -0
- package/dist/dashboard/assets/power-off-Yjw9GMJh.js +1 -0
- package/dist/dashboard/assets/power-off-Yjw9GMJh.js.gz +0 -0
- package/dist/dashboard/assets/privacy-mode-GM8bej85.js +1 -0
- package/dist/dashboard/assets/privacy-mode-GM8bej85.js.gz +0 -0
- package/dist/dashboard/assets/providers/agentrouter.svg.gz +0 -0
- package/dist/dashboard/assets/providers/aihubmix.svg.gz +0 -0
- package/dist/dashboard/assets/providers/alibaba-coding-plan.svg.gz +0 -0
- package/dist/dashboard/assets/providers/alibaba.svg.gz +0 -0
- package/dist/dashboard/assets/providers/antigravity.svg.gz +0 -0
- package/dist/dashboard/assets/providers/bai.svg.gz +0 -0
- package/dist/dashboard/assets/providers/blackbox.svg.gz +0 -0
- package/dist/dashboard/assets/providers/claude-code.svg.gz +0 -0
- package/dist/dashboard/assets/providers/exa.svg.gz +0 -0
- package/dist/dashboard/assets/providers/gmi-cloud.svg.gz +0 -0
- package/dist/dashboard/assets/providers/inferhub.svg.gz +0 -0
- package/dist/dashboard/assets/providers/muse.svg.gz +0 -0
- package/dist/dashboard/assets/providers/nous-research.svg.gz +0 -0
- package/dist/dashboard/assets/providers/openai-light.svg.gz +0 -0
- package/dist/dashboard/assets/providers/parallel.svg.gz +0 -0
- package/dist/dashboard/assets/providers/tokenharbor.svg.gz +0 -0
- package/dist/dashboard/assets/providers/zai-coding-plan.svg.gz +0 -0
- package/dist/dashboard/assets/{query-keys-ROf-g1Wi.js → query-keys-D7wX2vM5.js} +1 -1
- package/dist/dashboard/assets/query-keys-D7wX2vM5.js.gz +0 -0
- package/dist/dashboard/assets/quota-DZGyp0N5.js.gz +0 -0
- package/dist/dashboard/assets/refresh-cw-BqJFk4sU.js.gz +0 -0
- package/dist/dashboard/assets/rotate-ccw-CufcqITi.js.gz +0 -0
- package/dist/dashboard/assets/route-CNSNj9Zm.js.gz +0 -0
- package/dist/dashboard/assets/{routing-jntw9-Ui.js → routing-BExgxxBU.js} +1 -1
- package/dist/dashboard/assets/routing-BExgxxBU.js.gz +0 -0
- package/dist/dashboard/assets/scaling-nWE4Cx8f.js.gz +0 -0
- package/dist/dashboard/assets/search-JOKus9rb.js.gz +0 -0
- package/dist/dashboard/assets/{select-CGUuTQmj.js → select-C9mMoEpJ.js} +1 -1
- package/dist/dashboard/assets/select-C9mMoEpJ.js.gz +0 -0
- package/dist/dashboard/assets/settings-C4_Gn_wq.js +1 -0
- package/dist/dashboard/assets/settings-C4_Gn_wq.js.gz +0 -0
- package/dist/dashboard/assets/shield-check-DVS0Foqh.js.gz +0 -0
- package/dist/dashboard/assets/sliders-horizontal-ClG7dBeJ.js.gz +0 -0
- package/dist/dashboard/assets/sparkles-BRaRUxAU.js.gz +0 -0
- package/dist/dashboard/assets/{stack-C1kTcOmP.js → stack-Ca5HxI5i.js} +1 -1
- package/dist/dashboard/assets/stack-Ca5HxI5i.js.gz +0 -0
- package/dist/dashboard/assets/{state-DHKdg-pm.js → state-CzUPfLXo.js} +1 -1
- package/dist/dashboard/assets/state-CzUPfLXo.js.gz +0 -0
- package/dist/dashboard/assets/stream-retry-5QnVuSSo.js.gz +0 -0
- package/dist/dashboard/assets/{switch-DIDeAiz9.js → switch-DOUIky_i.js} +1 -1
- package/dist/dashboard/assets/switch-DOUIky_i.js.gz +0 -0
- package/dist/dashboard/assets/terminal-Bd4ee8TA.js +1 -0
- package/dist/dashboard/assets/terminal-Bd4ee8TA.js.gz +0 -0
- package/dist/dashboard/assets/toast-C76WrMhc.js +2 -0
- package/dist/dashboard/assets/toast-C76WrMhc.js.gz +0 -0
- package/dist/dashboard/assets/tokens-DDxMbSRL.js.gz +0 -0
- package/dist/dashboard/assets/trash-Cppn5c3R.js.gz +0 -0
- package/dist/dashboard/assets/use-clipboard-B0Sy03fE.js.gz +0 -0
- package/dist/dashboard/assets/use-presence-BSZEBx7U.js.gz +0 -0
- package/dist/dashboard/assets/use-reduced-motion-C4B0R6Sz.js.gz +0 -0
- package/dist/dashboard/assets/use-timeout-BwcBBRrT.js.gz +0 -0
- package/dist/dashboard/assets/{useMutation-DVmwOKU-.js → useMutation-D87FZwH0.js} +1 -1
- package/dist/dashboard/assets/useMutation-D87FZwH0.js.gz +0 -0
- package/dist/dashboard/assets/wrench-B6E1oEuY.js.gz +0 -0
- package/dist/dashboard/index.html +1 -1
- package/dist/dashboard/index.html.gz +0 -0
- package/dist/dashboard/precompressed-assets.json +123 -0
- package/migrations/full-postgres/0013_add_providers_tenant_id_index.sql +22 -0
- package/migrations/full-postgres/0014_add_provider_accounts_tenant_id_index.sql +21 -0
- package/migrations/lite-sqlite3/0000_baseline.sql +4 -2
- package/migrations/lite-sqlite3/0013_add_providers_tenant_id_index.sql +4 -0
- package/migrations/lite-sqlite3/0014_add_provider_accounts_tenant_id_index.sql +4 -0
- package/package.json +25 -30
- package/scripts/cli/{carte-launcher.cjs → bin/carte.cjs} +8 -8
- package/scripts/cli/{cli-launcher.cjs → bin/cartethyia.cjs} +167 -134
- package/scripts/cli/command/admin.ts +65 -0
- package/scripts/cli/command/console-menu.ts +227 -0
- package/scripts/cli/{doctor.ts → command/doctor.ts} +257 -258
- package/scripts/cli/{setup.ts → command/setup.ts} +8 -12
- package/scripts/cli/command/start.ts +13 -0
- package/scripts/cli/command/tray.ts +165 -0
- package/scripts/cli/command/update.ts +23 -0
- package/scripts/cli/{runtime-state.ts → gateway/drain-token.ts} +78 -78
- package/scripts/cli/gateway/spawn.ts +101 -0
- package/scripts/cli/gateway/stop.ts +51 -0
- package/scripts/cli/main.ts +94 -0
- package/scripts/cli/support/bun.ts +27 -0
- package/scripts/cli/support/env.ts +99 -0
- package/scripts/cli/support/platform.ts +64 -0
- package/scripts/cli/support/services.ts +88 -0
- package/scripts/cli/support/startup-signal.ts +23 -0
- package/scripts/cli/{version.ts → support/version.ts} +1 -1
- package/scripts/cli/tray/hidden-window.vbs +28 -0
- package/scripts/cli/{tray.ts → tray/index.ts} +3 -3
- package/scripts/cli/{tray.ps1 → tray/windows.ps1} +107 -98
- package/scripts/cli/ui/background.ts +49 -0
- package/src/boot/app.ts +3 -1
- package/src/boot/dependencies.ts +36 -13
- package/src/config/providers.ts +27 -1
- package/src/config/spec.ts +11 -0
- package/src/console/backup/routes.ts +8 -0
- package/src/console/cli-tools/contracts.ts +6 -6
- package/src/console/console-router.ts +5 -3
- package/src/console/dashboard-assets.ts +81 -11
- package/src/console/observability/contracts.ts +54 -22
- package/src/console/observability/routes.ts +31 -6
- package/src/console/observability/store.ts +68 -28
- package/src/console/providers/catalog/public-model-store.ts +91 -60
- package/src/console/providers/catalog/store.ts +5 -1
- package/src/console/quota/account-quota-tenant.ts +9 -6
- package/src/console/response-compression.ts +69 -2
- package/src/console/routing/pools/probe-verdict.ts +72 -0
- package/src/console/share/share-router.ts +7 -4
- package/src/console/share/share-stats.ts +12 -5
- package/src/console/shared/read-through-cache.ts +133 -7
- package/src/console/updates/repository-updates.ts +39 -34
- package/src/network/outbound-fetch.ts +113 -25
- package/src/network/tls/bun-client-hello.ts +112 -0
- package/src/observability/event-log.ts +29 -8
- package/src/observability/metrics.ts +40 -13
- package/src/observability/runtime-metrics.ts +7 -0
- package/src/persistence/schema.ts +40 -2
- package/src/protocol/response/codex.ts +9 -75
- package/src/protocol/tool-identity.ts +43 -9
- package/src/providers/authentication/jwt-validator.ts +130 -187
- package/src/providers/authentication/oauth-flow-store.ts +16 -9
- package/src/providers/default-registry.ts +4 -0
- package/src/providers/discovery/catalog/catalog-format.ts +20 -0
- package/src/providers/discovery/models-dev-catalog.ts +39 -22
- package/src/providers/discovery/probe-wire.ts +29 -5
- package/src/providers/integrations/buddy/buddy-catalog-shared.ts +3 -2
- package/src/providers/integrations/buddy/buddy-cli-headers.ts +88 -0
- package/src/providers/integrations/buddy/buddy-conversation-headers.ts +4 -0
- package/src/providers/integrations/buddy/buddy-discovery-shared.ts +33 -1
- package/src/providers/integrations/buddy/buddy-identity.ts +46 -0
- package/src/providers/integrations/buddy/buddy-oauth-shared.ts +37 -1
- package/src/providers/integrations/buddy/buddy-usage-store.ts +40 -5
- package/src/providers/integrations/buddy/codebuddy/provider.ts +2 -2
- package/src/providers/integrations/buddy/codebuddy/shared.ts +106 -73
- package/src/providers/integrations/buddy/workbuddy/cn-provider.ts +5 -2
- package/src/providers/integrations/buddy/workbuddy/quota.ts +172 -159
- package/src/providers/integrations/buddy/workbuddy/shared.ts +200 -171
- package/src/providers/integrations/codex/codex.ts +5 -8
- package/src/providers/integrations/factory/factory-oauth.ts +7 -11
- package/src/providers/integrations/opencode/opencode-go-quota.ts +130 -0
- package/src/providers/integrations/opencode/provider.ts +42 -3
- package/src/providers/integrations/qoder/qoder-oauth.ts +3 -9
- package/src/providers/integrations/shared/install-id.ts +29 -2
- package/src/providers/operations/client-versions.ts +11 -1
- package/src/providers/operations/model-catalog-cache.ts +18 -3
- package/src/providers/operations/provider-catalog-service.ts +76 -0
- package/src/transport/dispatch/attempt-finalize.ts +7 -1
- package/src/transport/dispatch/attempt-loop.ts +9 -7
- package/src/transport/dispatch/empty-stream.ts +17 -26
- package/src/transport/failure-policy.ts +20 -0
- package/src/transport/routing/route-catalog.ts +42 -3
- package/src/transport/surface/chat/encode.ts +18 -31
- package/src/transport/surface/messages/stream.ts +3 -2
- package/src/transport/translation/tool-repair.ts +37 -16
- package/src/workers/account-identity-sweep.ts +14 -12
- package/src/workers/daily-checkin.ts +62 -0
- package/src/workers/quota-refresh-sweep.ts +27 -2
- package/dist/dashboard/assets/CombosPage-B7ETkfVH.js +0 -1
- package/dist/dashboard/assets/ConsoleLogPage-AisQzCI1.js +0 -1
- package/dist/dashboard/assets/HealthEventsModal-BNflDm_f.js +0 -1
- package/dist/dashboard/assets/ModelPicker-CAiPR5Vd.js +0 -1
- package/dist/dashboard/assets/ProviderDetailPage-BK8i5B5X.js +0 -3
- package/dist/dashboard/assets/ProvidersPage-CvCVgm1e.js +0 -1
- package/dist/dashboard/assets/ProxyPage-DjabkuzP.js +0 -7
- package/dist/dashboard/assets/QuotaPage-Dxe0B0iu.js +0 -1
- package/dist/dashboard/assets/SetupPage-BaD4P_jW.js +0 -1
- package/dist/dashboard/assets/SortableList-Bol1ASmI.js +0 -1
- package/dist/dashboard/assets/StudioPage-CLP4le3U.js +0 -31
- package/dist/dashboard/assets/api-keys-CjIGQhe7.js +0 -1
- package/dist/dashboard/assets/app-Cjgo4-Lf.css +0 -2
- package/dist/dashboard/assets/cli-tools-DccaOfhX.js +0 -1
- package/dist/dashboard/assets/clipboard-button-DQmMikNQ.js +0 -1
- package/dist/dashboard/assets/entry-Cpi0rph1.js +0 -4
- package/dist/dashboard/assets/entry-Dm3PpDCO.js +0 -1
- package/dist/dashboard/assets/entry-keNDZ_Wz.js +0 -1
- package/dist/dashboard/assets/github-badge-BuAJr8oU.js +0 -1
- package/dist/dashboard/assets/mutation-BIizFZav.js +0 -1
- package/dist/dashboard/assets/privacy-mode-CQjbZ3xk.js +0 -1
- package/dist/dashboard/assets/settings-BAS6QbmJ.js +0 -1
- package/dist/dashboard/assets/terminal-C9mZ4Ugm.js +0 -1
- package/dist/dashboard/assets/toast-DKAdP2mo.js +0 -2
- package/scripts/cli/cli.ts +0 -771
- package/scripts/operations/probe-egress.ts +0 -335
- package/scripts/operations/restart.ts +0 -120
- package/scripts/shared/env.ts +0 -269
- /package/{tray-assets → scripts/cli/tray/assets}/cartethyia.ico +0 -0
- /package/{tray-assets → scripts/cli/tray/assets}/cartethyia.png +0 -0
- /package/scripts/cli/{menu.ts → ui/prompt.ts} +0 -0
- /package/scripts/cli/{wordmark.ts → ui/wordmark.ts} +0 -0
|
@@ -1,258 +1,257 @@
|
|
|
1
|
-
import { accessSync, constants, existsSync, mkdirSync } from "node:fs";
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
.
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
const required
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
redisParsed.host
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
);
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
console.log(
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
});
|
|
1
|
+
import { accessSync, constants, existsSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { ENV_PATH, hasPlaceholderSecrets, readEnvFile } from "../support/env";
|
|
3
|
+
import { getLocalPlatform, getOsHints } from "../support/platform";
|
|
4
|
+
import { parseServiceUrl, probeTcpService } from "../support/services";
|
|
5
|
+
import type { ProbeResult } from "../support/services";
|
|
6
|
+
import { resolveDataDir, resolveDbMode, type DbMode } from "../../../src/persistence/db-mode";
|
|
7
|
+
|
|
8
|
+
interface DoctorConfig {
|
|
9
|
+
envPath: string;
|
|
10
|
+
probeTimeoutMs: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function loadConfig(): DoctorConfig {
|
|
14
|
+
return {
|
|
15
|
+
envPath: ENV_PATH,
|
|
16
|
+
probeTimeoutMs: 3000,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Operator escape hatch: clears the console login lockout bucket.
|
|
22
|
+
*
|
|
23
|
+
* The lockout is keyed by `<username>:<clientIp>` and lasts an hour, and there
|
|
24
|
+
* is no console surface to clear it — a locked-out operator has no way back in
|
|
25
|
+
* short of editing the database by hand. This is the documented reset.
|
|
26
|
+
*
|
|
27
|
+
* Usage:
|
|
28
|
+
* bun run doctor --reset-lockout <username> <clientIp>
|
|
29
|
+
* bun run doctor --reset-lockout --all
|
|
30
|
+
*/
|
|
31
|
+
async function resetLockout(args: readonly string[]): Promise<void> {
|
|
32
|
+
const all = args.includes("--all");
|
|
33
|
+
const positional = args.filter((arg) => !arg.startsWith("--"));
|
|
34
|
+
if (!all && positional.length < 2) {
|
|
35
|
+
console.error("Usage: bun run doctor --reset-lockout <username> <clientIp>");
|
|
36
|
+
console.error(" bun run doctor --reset-lockout --all");
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const env = await readEnvFile(ENV_PATH);
|
|
41
|
+
const databaseUrl = env.DATABASE_URL ?? process.env.DATABASE_URL;
|
|
42
|
+
if (!databaseUrl) {
|
|
43
|
+
console.error("✗ DATABASE_URL is not set; cannot reach the lockout table.");
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
process.env.DATABASE_URL = databaseUrl;
|
|
47
|
+
|
|
48
|
+
const { getDb, closeDb } = await import("../../../src/persistence/boot");
|
|
49
|
+
const { consoleLockouts } = await import("../../../src/persistence/schema");
|
|
50
|
+
const { eq, or } = await import("drizzle-orm");
|
|
51
|
+
|
|
52
|
+
const db = getDb();
|
|
53
|
+
try {
|
|
54
|
+
if (all) {
|
|
55
|
+
const cleared = await db.delete(consoleLockouts).returning({ ip: consoleLockouts.ip });
|
|
56
|
+
console.log(`✓ Cleared ${cleared.length} lockout row(s).`);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const username = positional[0]!.trim().toLowerCase();
|
|
60
|
+
const clientIp = positional[1]!;
|
|
61
|
+
// The bucket key is `<identifier>:<ip>`; match either exact spelling so
|
|
62
|
+
// an operator does not have to guess how the identifier was normalized.
|
|
63
|
+
const cleared = await db
|
|
64
|
+
.delete(consoleLockouts)
|
|
65
|
+
.where(or(eq(consoleLockouts.ip, `${username}:${clientIp}`), eq(consoleLockouts.ip, clientIp)))
|
|
66
|
+
.returning({ ip: consoleLockouts.ip });
|
|
67
|
+
if (cleared.length === 0) {
|
|
68
|
+
console.log(`• No lockout row for ${username}:${clientIp} (already clear).`);
|
|
69
|
+
} else {
|
|
70
|
+
console.log(`✓ Cleared lockout for ${cleared.map((row) => row.ip).join(", ")}.`);
|
|
71
|
+
}
|
|
72
|
+
} finally {
|
|
73
|
+
await closeDb();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
async function probeReadiness(port: number, timeoutMs: number): Promise<ProbeResult> {
|
|
79
|
+
const controller = new AbortController();
|
|
80
|
+
const timeoutHandle = setTimeout(() => controller.abort(), timeoutMs);
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const response = await fetch(`http://localhost:${port}/health/ready`, {
|
|
84
|
+
signal: controller.signal,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
clearTimeout(timeoutHandle);
|
|
88
|
+
|
|
89
|
+
if (response.ok) {
|
|
90
|
+
return { success: true };
|
|
91
|
+
}
|
|
92
|
+
return { success: false, error: `HTTP ${response.status}` };
|
|
93
|
+
} catch (err) {
|
|
94
|
+
clearTimeout(timeoutHandle);
|
|
95
|
+
return { success: false, error: String(err) };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Runs the health check and the lockout reset; the CLI and `bun run doctor` both call this. */
|
|
100
|
+
export async function doctor(): Promise<void> {
|
|
101
|
+
const config = loadConfig();
|
|
102
|
+
|
|
103
|
+
const resetIndex = process.argv.indexOf("--reset-lockout");
|
|
104
|
+
if (resetIndex !== -1) {
|
|
105
|
+
await resetLockout(process.argv.slice(resetIndex + 1));
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
console.log("🩺 Cartethyia Health Check\n");
|
|
110
|
+
console.log(`🖥️ Host platform: ${getLocalPlatform()}`);
|
|
111
|
+
|
|
112
|
+
// Step 1: Check .env exists
|
|
113
|
+
console.log("📋 Environment Configuration");
|
|
114
|
+
if (!existsSync(config.envPath)) {
|
|
115
|
+
console.log(" ✗ .env file not found");
|
|
116
|
+
console.log(" Run: bun setup\n");
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
console.log(" ✓ .env file exists");
|
|
120
|
+
|
|
121
|
+
// Step 2: Check required environment variables. DATABASE_URL is required in
|
|
122
|
+
// full mode only; lite keeps its database in the data dir. Redis is never
|
|
123
|
+
// required — unset means the in-memory backend.
|
|
124
|
+
const envVars = await readEnvFile(config.envPath);
|
|
125
|
+
// `CARTETHYIA_DB_MODE` has no default and throws when absent, which is the
|
|
126
|
+
// right answer at boot but the wrong one here: this command exists to report
|
|
127
|
+
// a broken environment readably, so the missing key joins the checklist
|
|
128
|
+
// instead of crashing before it prints.
|
|
129
|
+
const missing: string[] = [];
|
|
130
|
+
let dbMode: DbMode | null = null;
|
|
131
|
+
try {
|
|
132
|
+
dbMode = resolveDbMode({ ...process.env, ...envVars });
|
|
133
|
+
} catch {
|
|
134
|
+
console.log(" ✗ CARTETHYIA_DB_MODE is not set — run: bun setup");
|
|
135
|
+
// Counted as missing so this command exits non-zero. A boot would throw on
|
|
136
|
+
// the same key, so a green doctor here would be a false all-clear.
|
|
137
|
+
missing.push("CARTETHYIA_DB_MODE");
|
|
138
|
+
}
|
|
139
|
+
const required =
|
|
140
|
+
dbMode === "full"
|
|
141
|
+
? ["DATABASE_URL", "CARTETHYIA_ENCRYPTION_KEY"]
|
|
142
|
+
: ["CARTETHYIA_ENCRYPTION_KEY"];
|
|
143
|
+
|
|
144
|
+
for (const key of required) {
|
|
145
|
+
const value = envVars[key] ?? process.env[key];
|
|
146
|
+
if (!value) {
|
|
147
|
+
missing.push(key);
|
|
148
|
+
console.log(` ✗ ${key} is not set`);
|
|
149
|
+
} else if (hasPlaceholderSecrets(value)) {
|
|
150
|
+
console.log(` ⚠️ ${key} has placeholder value — update before production`);
|
|
151
|
+
} else {
|
|
152
|
+
console.log(` ✓ ${key} is set`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (missing.length > 0) {
|
|
157
|
+
console.log(
|
|
158
|
+
`\nFailed: ${missing.length} required environment variable(s) missing or using placeholders\n`,
|
|
159
|
+
);
|
|
160
|
+
process.exit(1);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
console.log();
|
|
164
|
+
|
|
165
|
+
// Step 3: Check database. Full mode probes the TCP service; lite verifies
|
|
166
|
+
// the data dir is writable (the SQLite backend creates the file at boot too).
|
|
167
|
+
if (dbMode === "lite") {
|
|
168
|
+
const dataDir = resolveDataDir({ ...process.env, ...envVars });
|
|
169
|
+
try {
|
|
170
|
+
mkdirSync(dataDir, { recursive: true });
|
|
171
|
+
accessSync(dataDir, constants.W_OK);
|
|
172
|
+
console.log(` ✓ Lite data dir is writable (${dataDir})`);
|
|
173
|
+
} catch {
|
|
174
|
+
console.log(` ✗ Lite data dir is not writable (${dataDir})`);
|
|
175
|
+
process.exit(1);
|
|
176
|
+
}
|
|
177
|
+
} else {
|
|
178
|
+
const dbUrl = envVars.DATABASE_URL ?? process.env.DATABASE_URL;
|
|
179
|
+
const dbParsed = parseServiceUrl(dbUrl ?? "");
|
|
180
|
+
|
|
181
|
+
if (!dbParsed) {
|
|
182
|
+
console.log(" ✗ Invalid DATABASE_URL format");
|
|
183
|
+
process.exit(1);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const dbResult = await probeTcpService(dbParsed.host, dbParsed.port, config.probeTimeoutMs);
|
|
187
|
+
if (dbResult.success) {
|
|
188
|
+
console.log(` ✓ Reachable at ${dbParsed.host}:${dbParsed.port}`);
|
|
189
|
+
} else {
|
|
190
|
+
console.log(` ✗ Not reachable at ${dbParsed.host}:${dbParsed.port}`);
|
|
191
|
+
console.log(` Error: ${dbResult.error || "connection refused"}`);
|
|
192
|
+
for (const hint of getOsHints("postgres")) console.log(` ${hint}`);
|
|
193
|
+
process.exit(1);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
console.log();
|
|
198
|
+
|
|
199
|
+
// Step 4: Check Redis connectivity — informational only. A configured URL
|
|
200
|
+
// is probed; unset means the in-memory backend, which is always up.
|
|
201
|
+
console.log("🔴 Redis");
|
|
202
|
+
const redisUrl = envVars.REDIS_URL ?? process.env.REDIS_URL;
|
|
203
|
+
if (!redisUrl) {
|
|
204
|
+
console.log(" ✓ in-memory backend (REDIS_URL not set)");
|
|
205
|
+
} else {
|
|
206
|
+
const redisParsed = parseServiceUrl(redisUrl);
|
|
207
|
+
if (!redisParsed) {
|
|
208
|
+
console.log(" ✗ REDIS_URL is invalid");
|
|
209
|
+
process.exit(1);
|
|
210
|
+
}
|
|
211
|
+
const redisResult = await probeTcpService(
|
|
212
|
+
redisParsed.host,
|
|
213
|
+
redisParsed.port,
|
|
214
|
+
config.probeTimeoutMs,
|
|
215
|
+
);
|
|
216
|
+
if (redisResult.success) {
|
|
217
|
+
console.log(` ✓ Reachable at ${redisParsed.host}:${redisParsed.port}`);
|
|
218
|
+
} else {
|
|
219
|
+
console.log(` ✗ Not reachable at ${redisParsed.host}:${redisParsed.port}`);
|
|
220
|
+
console.log(` Error: ${redisResult.error || "connection refused"}`);
|
|
221
|
+
for (const hint of getOsHints("redis")) console.log(` ${hint}`);
|
|
222
|
+
process.exit(1);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
console.log();
|
|
227
|
+
|
|
228
|
+
// Step 5: Check application readiness
|
|
229
|
+
console.log("🏥 Application Readiness");
|
|
230
|
+
// Use the same explicit PORT resolution as the application.
|
|
231
|
+
const port = Number(envVars.PORT ?? process.env.PORT ?? "12800");
|
|
232
|
+
console.log(` Checking http://localhost:${port}/health/ready...`);
|
|
233
|
+
|
|
234
|
+
const readinessResult = await probeReadiness(port, config.probeTimeoutMs);
|
|
235
|
+
if (readinessResult.success) {
|
|
236
|
+
console.log(` ✓ Application is ready`);
|
|
237
|
+
} else {
|
|
238
|
+
console.log(` ✗ Application is not ready`);
|
|
239
|
+
console.log(` Error: ${readinessResult.error || "connection refused"}`);
|
|
240
|
+
console.log("");
|
|
241
|
+
console.log("Make sure the application is running:");
|
|
242
|
+
console.log(` bun run dev`);
|
|
243
|
+
process.exit(1);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
console.log();
|
|
247
|
+
console.log("✅ All systems operational!");
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// `bun run doctor` still works: the module is a script when run directly and a
|
|
251
|
+
// command when imported by the CLI.
|
|
252
|
+
if (import.meta.main) {
|
|
253
|
+
doctor().catch((err) => {
|
|
254
|
+
console.error("❌ Health check failed:", err);
|
|
255
|
+
process.exit(1);
|
|
256
|
+
});
|
|
257
|
+
}
|
|
@@ -4,23 +4,19 @@ import { existsSync, mkdirSync } from "node:fs";
|
|
|
4
4
|
import { readFile, writeFile } from "node:fs/promises";
|
|
5
5
|
import { dirname, resolve } from "node:path";
|
|
6
6
|
import { createServer } from "node:net";
|
|
7
|
+
import { MINIMUM_BUN_VERSION, bunVersionAtLeast } from "../support/bun";
|
|
7
8
|
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
detectLocalPostgres,
|
|
9
|
+
ENV_PATH,
|
|
10
|
+
PROJECT_ROOT,
|
|
11
11
|
generateCartethyiaEncryptionKey,
|
|
12
|
-
getLocalPlatform,
|
|
13
|
-
getOsHints,
|
|
14
12
|
hasPlaceholderSecrets,
|
|
15
13
|
mandatoryEnvBody,
|
|
16
|
-
parseServiceUrl,
|
|
17
|
-
probeTcpService,
|
|
18
14
|
readEnvFile,
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
} from "../
|
|
22
|
-
import type { ProbeResult } from "../
|
|
23
|
-
import { resolveDataDir, resolveDbMode } from "
|
|
15
|
+
} from "../support/env";
|
|
16
|
+
import { getLocalPlatform, getOsHints } from "../support/platform";
|
|
17
|
+
import { detectLocalPostgres, parseServiceUrl, probeTcpService } from "../support/services";
|
|
18
|
+
import type { ProbeResult } from "../support/services";
|
|
19
|
+
import { resolveDataDir, resolveDbMode } from "../../../src/persistence/db-mode";
|
|
24
20
|
|
|
25
21
|
async function ask(question: string, defaultValue = ""): Promise<string> {
|
|
26
22
|
const rl = createInterface({ input, output });
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { ensureDrainToken } from "../gateway/drain-token";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Runs the gateway in the foreground, in this process.
|
|
5
|
+
*
|
|
6
|
+
* A foreground run still needs a drain token on disk: the gateway registers
|
|
7
|
+
* `/admin/drain` only when one is set, and "Hide to tray" hands this run to a
|
|
8
|
+
* tray that drains it through that route.
|
|
9
|
+
*/
|
|
10
|
+
export async function startForeground(): Promise<void> {
|
|
11
|
+
process.env.CARTETHYIA_DRAIN_TOKEN ??= ensureDrainToken();
|
|
12
|
+
await import("../../../src/main");
|
|
13
|
+
}
|