@iowarp/clio-coder 0.5.0 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +4 -4
- package/CHANGELOG.md +35 -0
- package/CONTRIBUTING.md +9 -0
- package/README.md +12 -9
- package/ROADMAP.md +30 -5
- package/dist/{acp-KS7ARGV6.js → acp-F6PGOFZ4.js} +2 -2
- package/dist/{agents-YOGJ2YEJ.js → agents-ZHP4R6EX.js} +22 -21
- package/dist/assets/codewiki.json +1 -1
- package/dist/{auth-CEDCO6O6.js → auth-N75U2H3Z.js} +4 -4
- package/dist/{chunk-D3KWFOUT.js → chunk-23UIXH6Z.js} +3 -3
- package/dist/{chunk-ZR5YA6ZF.js → chunk-36XVK5P5.js} +5 -5
- package/dist/{chunk-AVSR2HKI.js → chunk-3DZ4LPLS.js} +6 -6
- package/dist/{chunk-LYJJ6546.js → chunk-3GSPCWFX.js} +12 -12
- package/dist/{chunk-CRHPCPZP.js → chunk-4KC4CHKJ.js} +2 -2
- package/dist/{chunk-4VDHLCF5.js → chunk-4WJBWKMI.js} +2 -2
- package/dist/{chunk-KBUTSJSR.js → chunk-4XBYHXGK.js} +2 -2
- package/dist/{chunk-6W75D37F.js → chunk-53P775GE.js} +6 -3
- package/dist/{chunk-XZWSP67B.js → chunk-5APYAXCB.js} +2 -2
- package/dist/{chunk-D65E25JG.js → chunk-67HPYNTJ.js} +2 -2
- package/dist/{chunk-5JQR5B4D.js → chunk-6KV4KCQZ.js} +4 -4
- package/dist/{chunk-V4XJWDHR.js → chunk-6UKFRTXI.js} +2 -1
- package/dist/{chunk-NDPF2N2L.js → chunk-7DW7CJW2.js} +2 -2
- package/dist/{chunk-5KQEUIWM.js → chunk-7LUEQODV.js} +4 -4
- package/dist/{chunk-LNOXCGN2.js → chunk-7UFDIOBT.js} +3 -3
- package/dist/{chunk-DXSETN72.js → chunk-7WUGNN43.js} +2 -2
- package/dist/{chunk-V35KZDUZ.js → chunk-AA3GRZWS.js} +2 -2
- package/dist/{chunk-X4CEFOSG.js → chunk-ADO3OQIC.js} +4 -4
- package/dist/{chunk-UX2L5CUD.js → chunk-AI36V3ON.js} +3 -3
- package/dist/{chunk-B7GY6QER.js → chunk-B26OUMJW.js} +2 -2
- package/dist/{chunk-LAR7DX5E.js → chunk-B5TASTCK.js} +153 -62
- package/dist/{chunk-TGWZXJ7L.js → chunk-BEMQK5Q7.js} +92 -84
- package/dist/{chunk-RNDCROSG.js → chunk-CNXU7TR2.js} +3 -3
- package/dist/{chunk-3JLTRPN7.js → chunk-CVMUXT64.js} +35 -66
- package/dist/{chunk-733ZBT5P.js → chunk-E74MJF6G.js} +4 -4
- package/dist/{chunk-T2MDZES3.js → chunk-EBMTBE76.js} +3 -3
- package/dist/{chunk-OZ76TLGU.js → chunk-EEITU7S6.js} +2 -2
- package/dist/{chunk-GRHOXDKB.js → chunk-ELDJWJKM.js} +97 -14
- package/dist/{chunk-GCQFN7YI.js → chunk-EMYEQLWO.js} +2 -2
- package/dist/{chunk-LTWH2ACR.js → chunk-EQPFEDFV.js} +222 -127
- package/dist/{chunk-TDGYTWM6.js → chunk-F4I3H56H.js} +2 -2
- package/dist/chunk-F6KCJO3U.js +523 -0
- package/dist/{chunk-FGSQHUCK.js → chunk-F6VKWTTY.js} +5 -5
- package/dist/{chunk-UJD5MPP6.js → chunk-FLNXQQ5B.js} +2 -2
- package/dist/{chunk-EX57VVWW.js → chunk-FOSRJ2VZ.js} +4 -4
- package/dist/{chunk-ZARZ4POR.js → chunk-G5ZDQQE3.js} +3 -3
- package/dist/{chunk-HQVN7G4F.js → chunk-G62JBPXN.js} +7 -7
- package/dist/{chunk-ZJPORZTC.js → chunk-GIAI7A2K.js} +2 -2
- package/dist/chunk-GPTR6OTU.js +2551 -0
- package/dist/{chunk-EZD2BXGP.js → chunk-GUFQPTQK.js} +2 -2
- package/dist/{chunk-OPD7GL6F.js → chunk-H6FQVNZC.js} +8 -7
- package/dist/chunk-H7REJ7L5.js +80 -0
- package/dist/{chunk-EYTLV3W3.js → chunk-HTWTORF7.js} +2 -2
- package/dist/{chunk-DF6PI6GN.js → chunk-I6UKQPGS.js} +3 -3
- package/dist/{chunk-ISHUS7HC.js → chunk-JRW236JT.js} +3 -3
- package/dist/{chunk-DE2LU267.js → chunk-KKAJGT3B.js} +3 -2
- package/dist/{chunk-OV62D6KO.js → chunk-KQMYL5GR.js} +3 -3
- package/dist/{chunk-QDUUOZ3S.js → chunk-KWMFC7FN.js} +3 -3
- package/dist/{chunk-R7FGRZNF.js → chunk-LXYHCCIZ.js} +9 -9
- package/dist/{chunk-NFSK2VNU.js → chunk-LZMTVVN4.js} +2 -2
- package/dist/{chunk-62UK7AGW.js → chunk-M5PMRZSR.js} +397 -44
- package/dist/chunk-MMZMM6SW.js +131 -0
- package/dist/{chunk-FFYTC2KQ.js → chunk-MNABNEBX.js} +2 -2
- package/dist/{chunk-XMDBTYCH.js → chunk-N3CWOGVF.js} +3 -3
- package/dist/{chunk-VYRQHORQ.js → chunk-NMQSO6Z6.js} +4 -3
- package/dist/{chunk-R2UUNSNQ.js → chunk-O3ZAF7UY.js} +18 -5
- package/dist/{chunk-4SYHGVDE.js → chunk-OQNCLZI4.js} +4 -4
- package/dist/{chunk-EG4ARXPC.js → chunk-OX6QTA4N.js} +3 -3
- package/dist/{chunk-P4GNZPXO.js → chunk-OYUS3UZP.js} +2 -2
- package/dist/{chunk-OQ2EOAHA.js → chunk-PF36KOGI.js} +291 -112
- package/dist/{chunk-55RMDDFM.js → chunk-PGQG7ZMW.js} +2 -2
- package/dist/{chunk-CBAQPTDU.js → chunk-PIU6BXXW.js} +2 -2
- package/dist/{chunk-EKV4UBCM.js → chunk-PZBQUJ2F.js} +2 -2
- package/dist/{chunk-7RET77VB.js → chunk-Q2HKY32Y.js} +2 -2
- package/dist/{chunk-U6YTUVOX.js → chunk-Q4RMNWMZ.js} +4 -4
- package/dist/chunk-Q6FRR3AQ.js +48 -0
- package/dist/{chunk-6RJMYYIE.js → chunk-QJSIAJBY.js} +3 -3
- package/dist/{chunk-DOUISWEE.js → chunk-QOJ4ERMU.js} +5 -5
- package/dist/{chunk-IFPDAD7R.js → chunk-RWSRKXLT.js} +3 -3
- package/dist/{chunk-T33IYTZM.js → chunk-SAASVMIY.js} +3 -3
- package/dist/{chunk-2DTIWSAW.js → chunk-SEQDH6JC.js} +3 -3
- package/dist/{chunk-BRCOSE7O.js → chunk-SZUO5BSE.js} +2 -2
- package/dist/{chunk-HOEDA42N.js → chunk-T7HZ7KJF.js} +2 -2
- package/dist/{chunk-OJXOA6YU.js → chunk-TL6LNERH.js} +2 -2
- package/dist/{chunk-UCINIRHW.js → chunk-TPCZXWLT.js} +3 -3
- package/dist/{chunk-ZUKUCZYZ.js → chunk-TQ2KTH4A.js} +2 -2
- package/dist/{chunk-4WOOTKFD.js → chunk-U3FETNQB.js} +341 -20
- package/dist/{chunk-5B3RTYAK.js → chunk-UGDSB4AN.js} +2 -2
- package/dist/{chunk-VKHLUZNO.js → chunk-UZ7YBL43.js} +3 -3
- package/dist/{chunk-QNUQ7K7D.js → chunk-VKMSEO7Y.js} +4 -4
- package/dist/{chunk-IN7DGBVS.js → chunk-VLX5VZ35.js} +5 -5
- package/dist/{chunk-SJGS3GDI.js → chunk-WAGBMMNX.js} +5 -5
- package/dist/{chunk-BFOSV5EZ.js → chunk-WX2YCH7F.js} +2 -2
- package/dist/{chunk-3LT34CAM.js → chunk-XBIGUILU.js} +32 -17
- package/dist/{chunk-XDHUDE5K.js → chunk-XKYBFRWR.js} +5 -5
- package/dist/{chunk-U5QU5ZOD.js → chunk-XYPWFSU5.js} +3 -3
- package/dist/{chunk-YBUECLAF.js → chunk-YEJQDODA.js} +2 -2
- package/dist/{chunk-YQ6XEFVK.js → chunk-Z74OGONW.js} +39 -7
- package/dist/{chunk-I4ELN5BX.js → chunk-ZTNOEJRI.js} +2 -2
- package/dist/cli/index.js +26 -26
- package/dist/{clio-K2PBVCEM.js → clio-4IKFE25F.js} +2 -2
- package/dist/{clio-context-tools-GR4OHFSO.js → clio-context-tools-OHJXYX4R.js} +25 -23
- package/dist/{code-nav-4X3OI6DK.js → code-nav-XPM3MY5I.js} +7 -7
- package/dist/{config-Q5J7UBAB.js → config-343YS25T.js} +43 -41
- package/dist/{config-graph-P3555574.js → config-graph-WI263KDX.js} +43 -41
- package/dist/{configure-KIJ32ZO6.js → configure-GZDG4HER.js} +21 -21
- package/dist/{context-LFBQOSXT.js → context-4OFO7N7Y.js} +12 -12
- package/dist/{context-WBH3KSVR.js → context-JLK2RLOG.js} +42 -40
- package/dist/{context-NNLBES2Y.js → context-O6DIWCF6.js} +23 -21
- package/dist/{context-clear-YOHMQ6SQ.js → context-clear-MR7KOGQI.js} +42 -40
- package/dist/{context-working-set-MRAEVDNV.js → context-working-set-YZAD3KWO.js} +14 -13
- package/dist/{data-tool-JJPBGMQJ.js → data-tool-EKD7DFHG.js} +7 -7
- package/dist/{detail-RHJOTGN4.js → detail-YZPHFEVJ.js} +43 -41
- package/dist/{dispatch-runner-NC7PYNIO.js → dispatch-runner-VKVXVTWJ.js} +44 -42
- package/dist/{doctor-S5KZ2GTN.js → doctor-53AVQXAO.js} +17 -16
- package/dist/{doctor-deep-2EJQSHIZ.js → doctor-deep-BLRB2NCG.js} +5 -5
- package/dist/{eval-3D6M36N7.js → eval-P3BQSLQE.js} +21 -20
- package/dist/{evidence-XNVPPLVK.js → evidence-6LLI6SZH.js} +44 -42
- package/dist/{evidence-6IYN52ET.js → evidence-HTN3JLUO.js} +42 -40
- package/dist/{evidence-JVECPI3F.js → evidence-NJRE3R5H.js} +42 -40
- package/dist/{evolve-TLPUDJDM.js → evolve-WWZHJVJP.js} +42 -40
- package/dist/{fleet-WKVBF5SZ.js → fleet-GGZ4BVJE.js} +64 -62
- package/dist/{fleet-6RJWIOEO.js → fleet-QG3IIWWM.js} +44 -42
- package/dist/{fleet-commands-L6WFT7KZ.js → fleet-commands-J2ZIKISQ.js} +7 -7
- package/dist/{fleet-decisions-BTQMWU5A.js → fleet-decisions-Y2Q7375Q.js} +8 -8
- package/dist/{fleet-graph-553DZ67Q.js → fleet-graph-TCBCCCBP.js} +11 -11
- package/dist/{fleet-inspect-236FZPRB.js → fleet-inspect-KOLKBC56.js} +44 -42
- package/dist/{fleet-preflight-NZQHABWO.js → fleet-preflight-44UNCC4Y.js} +27 -25
- package/dist/{fleet-validate-7NLAE26R.js → fleet-validate-K2FXRZVA.js} +13 -13
- package/dist/{fleet-verify-E3RO6A5W.js → fleet-verify-AS444WDW.js} +42 -40
- package/dist/{fleet-view-KMZQY7YJ.js → fleet-view-X4JU4J6O.js} +43 -41
- package/dist/gui/ops-worker.js +9 -9
- package/dist/gui/reads-worker.js +9 -8
- package/dist/{init-DV5B6HQG.js → init-VSOWFHVL.js} +54 -52
- package/dist/{interactive-OXGYZYAS.js → interactive-JRCBENLZ.js} +1733 -577
- package/dist/{interop-J7UOBOSF.js → interop-JR6BCY7R.js} +10 -10
- package/dist/{inventory-OEGIL6PY.js → inventory-3JUGLVJI.js} +43 -41
- package/dist/{library-ROZAMUIL.js → library-7BOH3Y7N.js} +15 -15
- package/dist/{library-B3UPGY4V.js → library-7PFONXUI.js} +9 -9
- package/dist/{library-MXQA5DLT.js → library-TUYZMFIA.js} +7 -7
- package/dist/{library-import-2LTWOVBD.js → library-import-TJVXKLDK.js} +10 -10
- package/dist/{library-inventory-LJ3XHLWO.js → library-inventory-MD3AYTP4.js} +9 -9
- package/dist/{library-validation-DARHHUNW.js → library-validation-ZDLITEA3.js} +7 -7
- package/dist/{mcp-2GSPYQ54.js → mcp-EFCWUPUR.js} +3 -3
- package/dist/{memory-J3STUUIK.js → memory-Q2YVOKK5.js} +42 -40
- package/dist/{models-Y3GMFJ2V.js → models-M7BOCW7F.js} +17 -16
- package/dist/{monitor-LHNH577J.js → monitor-2HD3MC67.js} +47 -45
- package/dist/{orchestrator-DTAPXULA.js → orchestrator-OUW3ZN6Y.js} +1894 -226
- package/dist/{panes-A743ONZ5.js → panes-TTPPSYCL.js} +3 -3
- package/dist/{preload-OJBMAIXF.js → preload-A47C2NUF.js} +42 -40
- package/dist/{providers-CMETPZNS.js → providers-T43W7EL4.js} +4 -4
- package/dist/{resources-FY6XYFN2.js → resources-TP6X2V6W.js} +20 -11
- package/dist/{run-H64MEVTA.js → run-LDSBPMCS.js} +61 -59
- package/dist/{share-CAFMMQLM.js → share-URHKKMHR.js} +9 -9
- package/dist/{skills-ERKQUC7L.js → skills-E477MOOO.js} +13 -11
- package/dist/{skills-eval-CQ23MYQK.js → skills-eval-AIBR2L7H.js} +583 -93
- package/dist/{skills-inventory-TVUYJDVN.js → skills-inventory-HLMKY3R5.js} +13 -11
- package/dist/{slash-commands-ROG3KIFJ.js → slash-commands-LIIZKA5Y.js} +28 -26
- package/dist/{startup-background-KDWCQMWT.js → startup-background-JR2KDK6R.js} +43 -41
- package/dist/{steer-KKYXJSH4.js → steer-D7RTAPCM.js} +3 -3
- package/dist/{system-ZDW44ZGX.js → system-X2P5N4IY.js} +14 -13
- package/dist/{targets-AJWA6E7Z.js → targets-VFFAKUFM.js} +25 -24
- package/dist/{tasks-6L2JJRC5.js → tasks-ERAWZA22.js} +6 -6
- package/dist/{terminal-lease-YFUSDPAD.js → terminal-lease-V2N2BIKQ.js} +2 -2
- package/dist/{trace-LA5MVR4Y.js → trace-MA5BT7XY.js} +3 -3
- package/dist/{usage-XSILDHKF.js → usage-WYFY2L6M.js} +50 -48
- package/dist/{verifiers-ECA5GIHL.js → verifiers-TYIW2XKF.js} +7 -7
- package/dist/{verify-R6QQCCEB.js → verify-242UFYTP.js} +6 -6
- package/dist/{web-fetch-PVZNMBOB.js → web-fetch-LRAGYU6Y.js} +3 -3
- package/dist/{wiki-generate-RP22WP5K.js → wiki-generate-OFKOX2T6.js} +53 -51
- package/dist/worker/entry.js +31 -29
- package/docs/architecture/architecture.md +1 -0
- package/docs/architecture/context-engine.md +4 -2
- package/docs/architecture/observability.md +6 -4
- package/docs/architecture/prompt-envelope-and-tools.md +1 -1
- package/docs/architecture/session-lifecycle.md +1 -1
- package/docs/architecture/tui-design.md +12 -9
- package/docs/gui/parity/02-slash-and-surfaces.md +1 -1
- package/docs/guide/commands-and-modes.md +91 -5
- package/docs/guide/configuration-and-targets.md +2 -2
- package/docs/guide/context-continuity.md +49 -0
- package/docs/guide/proactive-memory.md +6 -3
- package/library/registry.yaml +292 -32
- package/library/skills/README.md +27 -2
- package/library/skills/context/context-handoff/SKILL.md +10 -4
- package/library/skills/meta/clio-coder-dev/SKILL.md +92 -88
- package/library/skills/meta/clio-coder-dev/evals.md +51 -46
- package/library/skills/meta/clio-coder-dev/plugin.json +2 -2
- package/library/skills/meta/clio-coder-dev/references/change-map.md +49 -0
- package/library/skills/meta/clio-coder-test/SKILL.md +89 -142
- package/library/skills/meta/clio-coder-test/evals.md +58 -62
- package/library/skills/meta/clio-coder-test/plugin.json +2 -2
- package/library/skills/meta/clio-coder-test/references/harness.md +3 -3
- package/library/skills/meta/clio-coder-test/references/lifecycle-validation.md +30 -0
- package/library/skills/meta/clio-coder-test/references/test-map.md +60 -89
- package/library/skills/registry.yaml +5 -5
- package/library/skills/skill-marketplace.json +12 -14
- package/package.json +1 -1
- package/src/cli/skills-eval.ts +1064 -54
- package/src/cli/usage.ts +2 -2
- package/src/core/clio-repo.ts +3 -0
- package/src/core/tool-names.ts +1 -0
- package/src/domains/context/budget/inspection.ts +9 -0
- package/src/domains/context/budget/live-view.ts +418 -0
- package/src/domains/context/budget/pressure.ts +393 -0
- package/src/domains/context/budget/request-fit.ts +15 -0
- package/src/domains/evidence/build.ts +24 -0
- package/src/domains/gateway/mcp/client.ts +1 -1
- package/src/domains/memory/commit-state.ts +160 -0
- package/src/domains/memory/operations.ts +18 -10
- package/src/domains/memory/prompt-cache.ts +95 -0
- package/src/domains/memory/prompt-section.ts +50 -7
- package/src/domains/memory/relevance.ts +93 -0
- package/src/domains/memory/restoration.ts +78 -0
- package/src/domains/memory/store.ts +39 -1
- package/src/domains/middleware/memory-intervention.ts +116 -24
- package/src/domains/observability/background-memory-usage.ts +1 -1
- package/src/domains/observability/cost.ts +3 -3
- package/src/domains/observability/extension.ts +1 -1
- package/src/domains/observability/metrics.ts +1 -1
- package/src/domains/prompts/compiler.ts +1 -1
- package/src/domains/prompts/extension.ts +38 -1
- package/src/domains/quota/anthropic-max-provider.ts +134 -0
- package/src/domains/quota/anthropic-usage.ts +220 -0
- package/src/domains/quota/antigravity-provider.ts +339 -0
- package/src/domains/quota/cache.ts +87 -0
- package/src/domains/quota/claude-code-provider.ts +169 -0
- package/src/domains/quota/codex-provider.ts +237 -0
- package/src/domains/quota/presentation.ts +240 -0
- package/src/domains/quota/registry.ts +23 -0
- package/src/domains/quota/service.ts +93 -0
- package/src/domains/quota/summary-feed.ts +86 -0
- package/src/domains/quota/types.ts +84 -0
- package/src/domains/resources/index.ts +11 -0
- package/src/domains/resources/skills/catalog-view.ts +571 -0
- package/src/domains/resources/skills/lexical-match.ts +136 -0
- package/src/domains/resources/skills/loader.ts +38 -0
- package/src/domains/resources/skills/promotion.ts +1 -55
- package/src/domains/resources/skills/provenance-pin.ts +50 -20
- package/src/domains/safety/action-classifier.ts +1 -0
- package/src/domains/session/compaction/branch-summary.ts +4 -1
- package/src/domains/session/compaction/compact.ts +18 -2
- package/src/domains/session/compaction/cut-point.ts +20 -1
- package/src/domains/session/compaction/tokens.ts +48 -3
- package/src/domains/session/context-accounting.ts +8 -1
- package/src/domains/session/continuity/carry.ts +59 -0
- package/src/domains/session/continuity/contract.ts +592 -0
- package/src/domains/session/continuity/evidence.ts +290 -0
- package/src/domains/session/continuity/fold.ts +1075 -0
- package/src/domains/session/continuity/note.ts +79 -0
- package/src/domains/session/continuity/operator-request.ts +104 -0
- package/src/domains/session/continuity/persistence.ts +408 -0
- package/src/domains/session/continuity/ports.ts +231 -0
- package/src/domains/session/continuity/projection.ts +538 -0
- package/src/domains/session/continuity/validate.ts +352 -0
- package/src/domains/session/entries.ts +50 -3
- package/src/domains/session/index.ts +48 -0
- package/src/domains/session/migrations/index.ts +7 -3
- package/src/domains/session/tree/fork.ts +15 -1
- package/src/domains/session/usage.ts +2 -2
- package/src/engine/acp/commands.ts +1 -1
- package/src/engine/agent.ts +133 -9
- package/src/engine/session.ts +13 -6
- package/src/entry/orchestrator.ts +116 -33
- package/src/interactive/chat-loop-messages.ts +2 -2
- package/src/interactive/chat-loop.ts +219 -63
- package/src/interactive/chat-renderer.ts +99 -8
- package/src/interactive/context-overlay.ts +24 -3
- package/src/interactive/continuity-controller.ts +526 -0
- package/src/interactive/dispatch-board.ts +21 -3
- package/src/interactive/footer/dashboard.ts +25 -1
- package/src/interactive/footer/key-hints.ts +2 -2
- package/src/interactive/footer/pages.ts +70 -27
- package/src/interactive/footer/widgets.ts +32 -7
- package/src/interactive/footer-panel.ts +1 -1
- package/src/interactive/interactive-application.ts +7 -2
- package/src/interactive/interactive-input-runtime.ts +2 -2
- package/src/interactive/interactive-presentation.ts +15 -0
- package/src/interactive/interactive-slash-runtime.ts +45 -8
- package/src/interactive/interactive-tickers.ts +7 -1
- package/src/interactive/model-session-replay.ts +130 -3
- package/src/interactive/output-reserve.ts +35 -0
- package/src/interactive/overlay-general-openers.ts +12 -8
- package/src/interactive/overlay-key-routing.ts +6 -9
- package/src/interactive/overlay-lifecycle.ts +10 -6
- package/src/interactive/overlay-session-lifecycle.ts +21 -14
- package/src/interactive/quota-view.ts +229 -0
- package/src/interactive/session-last-turn.ts +1 -1
- package/src/interactive/session-transcript.ts +2 -1
- package/src/interactive/session-usage-reseed.ts +2 -2
- package/src/interactive/side-question.ts +2 -2
- package/src/interactive/slash-commands.ts +28 -9
- package/src/interactive/turn-context.ts +686 -63
- package/src/interactive/turn-middleware.ts +39 -6
- package/src/interactive/turn-persistence.ts +15 -2
- package/src/interactive/turn-prewarm.ts +1 -1
- package/src/interactive/turn-runtime.ts +48 -5
- package/src/interactive/{cost-overlay.ts → usage-overlay.ts} +121 -33
- package/src/interactive/welcome-dashboard.ts +26 -3
- package/src/tools/agent-tools.ts +3 -1
- package/src/tools/bootstrap.ts +6 -0
- package/src/tools/builtin-tool-catalog.ts +9 -0
- package/src/tools/context/index.ts +148 -113
- package/src/tools/context/surface.ts +11 -7
- package/src/tools/core-bootstrap.ts +4 -0
- package/src/tools/observation.ts +8 -2
- package/src/tools/policy.ts +3 -0
- package/src/tools/self-compact.ts +31 -0
- package/src/tools/surface.ts +1 -0
- package/src/tools/tasks.ts +1 -1
- package/dist/chunk-CTFPFW3H.js +0 -44
- package/dist/chunk-EOJPDNUP.js +0 -1325
- package/dist/chunk-I4Y4WKZR.js +0 -386
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quota adapter for the OpenAI Codex subscription.
|
|
3
|
+
*
|
|
4
|
+
* Codex CLI stores its own OAuth record in `auth.json`, honoring
|
|
5
|
+
* `CODEX_HOME` when set the same way the `codex` command does. This
|
|
6
|
+
* adapter reads that file and never writes it, then reads current usage
|
|
7
|
+
* from the ChatGPT backend's usage endpoint.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { readFile } from "node:fs/promises";
|
|
11
|
+
import { homedir } from "node:os";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
|
|
14
|
+
import { formatPlan, type QuotaProvider, type UsageCredits, type UsageSnapshot, type UsageWindow } from "./types.js";
|
|
15
|
+
|
|
16
|
+
export const CODEX_QUOTA_PROVIDER_ID = "codex";
|
|
17
|
+
export const CODEX_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
|
|
18
|
+
|
|
19
|
+
const DISPLAY_NAME = "Codex";
|
|
20
|
+
const REQUEST_TIMEOUT_MS = 15_000;
|
|
21
|
+
const WINDOW_ORDER: Record<string, number> = { session: 0, weekly: 1 };
|
|
22
|
+
const WEEKLY_THRESHOLD_SECONDS = 86_400;
|
|
23
|
+
|
|
24
|
+
/** The subset of Codex's stored OAuth record this adapter needs. */
|
|
25
|
+
export interface CodexCredentials {
|
|
26
|
+
accessToken: string;
|
|
27
|
+
accountId: string | null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface CodexQuotaOptions {
|
|
31
|
+
/** Override the credential file location; defaults to `$CODEX_HOME/auth.json` or `~/.codex/auth.json`. */
|
|
32
|
+
credentialsPath?: string;
|
|
33
|
+
/** Replace credential reading entirely, which is how tests supply a token. */
|
|
34
|
+
readCredentials?: () => Promise<CodexCredentials | null>;
|
|
35
|
+
fetch?: typeof globalThis.fetch;
|
|
36
|
+
timeoutMs?: number;
|
|
37
|
+
now?: () => Date;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Codex's default credential file path. */
|
|
41
|
+
function defaultCodexCredentialsPath(): string {
|
|
42
|
+
const codexHome = process.env.CODEX_HOME;
|
|
43
|
+
return codexHome ? join(codexHome, "auth.json") : join(homedir(), ".codex", "auth.json");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Extract the access token and account id from a parsed credential document. */
|
|
47
|
+
function parseCodexCredentials(raw: unknown): CodexCredentials | null {
|
|
48
|
+
if (!isRecord(raw)) return null;
|
|
49
|
+
const tokens = raw.tokens;
|
|
50
|
+
if (!isRecord(tokens)) return null;
|
|
51
|
+
const accessToken = tokens.access_token;
|
|
52
|
+
if (typeof accessToken !== "string" || accessToken.length === 0) return null;
|
|
53
|
+
const accountId = tokens.account_id;
|
|
54
|
+
return {
|
|
55
|
+
accessToken,
|
|
56
|
+
accountId: typeof accountId === "string" ? accountId : null,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function readCredentialsFile(path: string): Promise<CodexCredentials | null> {
|
|
61
|
+
let text: string;
|
|
62
|
+
try {
|
|
63
|
+
text = await readFile(path, "utf8");
|
|
64
|
+
} catch {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
return parseCodexCredentials(JSON.parse(text));
|
|
69
|
+
} catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Build the read-only `codex` quota adapter. */
|
|
75
|
+
export function createCodexQuotaProvider(options: CodexQuotaOptions = {}): QuotaProvider {
|
|
76
|
+
const credentialsPath = options.credentialsPath ?? defaultCodexCredentialsPath();
|
|
77
|
+
const readCredentials = options.readCredentials ?? (() => readCredentialsFile(credentialsPath));
|
|
78
|
+
const doFetch = options.fetch ?? globalThis.fetch;
|
|
79
|
+
const timeoutMs = options.timeoutMs ?? REQUEST_TIMEOUT_MS;
|
|
80
|
+
const now = options.now ?? (() => new Date());
|
|
81
|
+
|
|
82
|
+
const snapshot = (
|
|
83
|
+
status: UsageSnapshot["status"],
|
|
84
|
+
message: string | null,
|
|
85
|
+
extra: Partial<UsageSnapshot> = {},
|
|
86
|
+
): UsageSnapshot => ({
|
|
87
|
+
providerId: CODEX_QUOTA_PROVIDER_ID,
|
|
88
|
+
displayName: DISPLAY_NAME,
|
|
89
|
+
status,
|
|
90
|
+
windows: [],
|
|
91
|
+
message,
|
|
92
|
+
fetchedAt: now().toISOString(),
|
|
93
|
+
...extra,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
id: CODEX_QUOTA_PROVIDER_ID,
|
|
98
|
+
displayName: DISPLAY_NAME,
|
|
99
|
+
|
|
100
|
+
async detect(): Promise<boolean> {
|
|
101
|
+
return (await readCredentials()) !== null;
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
async fetch(): Promise<UsageSnapshot> {
|
|
105
|
+
const credentials = await readCredentials();
|
|
106
|
+
if (credentials === null) {
|
|
107
|
+
return snapshot("no_credentials", "No Codex credentials found");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const headers: Record<string, string> = {
|
|
111
|
+
Authorization: `Bearer ${credentials.accessToken}`,
|
|
112
|
+
"User-Agent": "codex-cli",
|
|
113
|
+
};
|
|
114
|
+
if (credentials.accountId !== null) {
|
|
115
|
+
headers["ChatGPT-Account-Id"] = credentials.accountId;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const controller = new AbortController();
|
|
119
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
120
|
+
let response: Response;
|
|
121
|
+
try {
|
|
122
|
+
response = await doFetch(CODEX_USAGE_URL, {
|
|
123
|
+
method: "GET",
|
|
124
|
+
headers,
|
|
125
|
+
signal: controller.signal,
|
|
126
|
+
});
|
|
127
|
+
} catch (error) {
|
|
128
|
+
return snapshot("error", describeError(error, timeoutMs));
|
|
129
|
+
} finally {
|
|
130
|
+
clearTimeout(timer);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (response.status === 401 || response.status === 403) {
|
|
134
|
+
await discardBody(response);
|
|
135
|
+
return snapshot("expired", "Sign in with Codex again");
|
|
136
|
+
}
|
|
137
|
+
if (response.status !== 200) {
|
|
138
|
+
await discardBody(response);
|
|
139
|
+
return snapshot("error", `HTTP ${response.status}`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
let body: unknown;
|
|
143
|
+
try {
|
|
144
|
+
body = await response.json();
|
|
145
|
+
} catch {
|
|
146
|
+
return snapshot("error", "Unexpected usage response");
|
|
147
|
+
}
|
|
148
|
+
if (!isRecord(body)) {
|
|
149
|
+
return snapshot("error", "Unexpected usage response");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const planType = typeof body.plan_type === "string" ? body.plan_type : null;
|
|
153
|
+
return snapshot("ok", null, {
|
|
154
|
+
windows: usageWindows(body.rate_limit),
|
|
155
|
+
credits: usageCredits(body.credits),
|
|
156
|
+
plan: formatPlan(planType),
|
|
157
|
+
});
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Windows from the `rate_limit.primary_window` and `.secondary_window` slots. */
|
|
163
|
+
function usageWindows(raw: unknown): UsageWindow[] {
|
|
164
|
+
if (!isRecord(raw)) return [];
|
|
165
|
+
const windows: UsageWindow[] = [];
|
|
166
|
+
const primary = buildWindow(raw.primary_window, "session");
|
|
167
|
+
if (primary !== null) windows.push(primary);
|
|
168
|
+
const secondary = buildWindow(raw.secondary_window, "weekly");
|
|
169
|
+
if (secondary !== null) windows.push(secondary);
|
|
170
|
+
return windows.sort((left, right) => {
|
|
171
|
+
const order = (WINDOW_ORDER[left.key] ?? 2) - (WINDOW_ORDER[right.key] ?? 2);
|
|
172
|
+
return order !== 0 ? order : left.label.localeCompare(right.label);
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** One `rate_limit` window slot, with the key defaulting when `limit_window_seconds` is absent. */
|
|
177
|
+
function buildWindow(raw: unknown, defaultKey: "session" | "weekly"): UsageWindow | null {
|
|
178
|
+
if (!isRecord(raw)) return null;
|
|
179
|
+
const usedPct = asNumber(raw.used_percent);
|
|
180
|
+
if (usedPct === null) return null;
|
|
181
|
+
const key = windowKey(raw.limit_window_seconds, defaultKey);
|
|
182
|
+
const label = key === "weekly" ? "Weekly" : "5h";
|
|
183
|
+
const short = key === "weekly" ? "wk" : "5h";
|
|
184
|
+
return { key, label, short, usedPct, resetsAt: epochSecondsToIso(raw.reset_at) };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function windowKey(limitWindowSeconds: unknown, defaultKey: "session" | "weekly"): "session" | "weekly" {
|
|
188
|
+
const seconds = asNumber(limitWindowSeconds);
|
|
189
|
+
if (seconds === null) return defaultKey;
|
|
190
|
+
return seconds >= WEEKLY_THRESHOLD_SECONDS ? "weekly" : "session";
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Credit balance from the `credits` block, present only for credit-billed plans. */
|
|
194
|
+
function usageCredits(raw: unknown): UsageCredits | null {
|
|
195
|
+
if (!isRecord(raw)) return null;
|
|
196
|
+
if (raw.unlimited === true) return { display: "Unlimited", usedPct: null };
|
|
197
|
+
if (raw.has_credits === true && typeof raw.balance === "string" && raw.balance.length > 0) {
|
|
198
|
+
return { display: raw.balance, usedPct: null };
|
|
199
|
+
}
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
204
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function asNumber(value: unknown): number | null {
|
|
208
|
+
if (typeof value === "number") return Number.isFinite(value) ? value : null;
|
|
209
|
+
if (typeof value === "string") {
|
|
210
|
+
const parsed = Number.parseFloat(value.trim());
|
|
211
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
212
|
+
}
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Convert a UNIX epoch seconds value into an ISO 8601 instant. */
|
|
217
|
+
function epochSecondsToIso(value: unknown): string | null {
|
|
218
|
+
const seconds = asNumber(value);
|
|
219
|
+
if (seconds === null) return null;
|
|
220
|
+
const date = new Date(seconds * 1000);
|
|
221
|
+
return Number.isNaN(date.getTime()) ? null : date.toISOString();
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function describeError(error: unknown, timeoutMs: number): string {
|
|
225
|
+
if (error instanceof Error) {
|
|
226
|
+
return error.name === "AbortError" ? `timeout after ${timeoutMs}ms` : error.message;
|
|
227
|
+
}
|
|
228
|
+
return String(error);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async function discardBody(response: Response): Promise<void> {
|
|
232
|
+
try {
|
|
233
|
+
await response.body?.cancel();
|
|
234
|
+
} catch {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure presentation helpers for quota snapshots.
|
|
3
|
+
*
|
|
4
|
+
* These produce plain strings and severity words. No terminal, no theme,
|
|
5
|
+
* no component imports, so the interactive layer decides colour and the
|
|
6
|
+
* domain stays testable without a TTY.
|
|
7
|
+
*
|
|
8
|
+
* Two facts from live accounts shape everything here. A provider may report
|
|
9
|
+
* no session window at all (Codex reports weekly only), so no surface may
|
|
10
|
+
* assume a fixed row shape. And two credentials can sit on one account
|
|
11
|
+
* (anthropic-max and claude-code), so a combined view must fold them rather
|
|
12
|
+
* than present one budget twice.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { UsageSnapshot, UsageWindow } from "./types.js";
|
|
16
|
+
|
|
17
|
+
/** How close a window is to its limit, in the absence of a provider word. */
|
|
18
|
+
export type QuotaSeverity = "normal" | "caution" | "warning" | "critical";
|
|
19
|
+
|
|
20
|
+
const SEVERITY_WORDS = new Set<QuotaSeverity>(["normal", "caution", "warning", "critical"]);
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Thresholds for a window the provider did not classify.
|
|
24
|
+
*
|
|
25
|
+
* Anthropic sends its own `severity` and that is preferred; these bounds
|
|
26
|
+
* exist for Codex and Antigravity, which send none.
|
|
27
|
+
*/
|
|
28
|
+
export function severityForPct(usedPct: number): QuotaSeverity {
|
|
29
|
+
if (usedPct >= 95) return "critical";
|
|
30
|
+
if (usedPct >= 80) return "warning";
|
|
31
|
+
if (usedPct >= 60) return "caution";
|
|
32
|
+
return "normal";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** A provider's own severity word when it sent a recognized one, else the threshold. */
|
|
36
|
+
export function windowSeverity(window: UsageWindow): QuotaSeverity {
|
|
37
|
+
const reported = window.severity?.toLowerCase();
|
|
38
|
+
if (reported !== undefined && SEVERITY_WORDS.has(reported as QuotaSeverity)) {
|
|
39
|
+
return reported as QuotaSeverity;
|
|
40
|
+
}
|
|
41
|
+
return severityForPct(window.usedPct);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The snapshot's worst severity, which is what a single indicator should show. */
|
|
45
|
+
export function snapshotSeverity(snapshot: UsageSnapshot): QuotaSeverity {
|
|
46
|
+
const rank: QuotaSeverity[] = ["normal", "caution", "warning", "critical"];
|
|
47
|
+
let worst: QuotaSeverity = "normal";
|
|
48
|
+
for (const window of snapshot.windows) {
|
|
49
|
+
const severity = windowSeverity(window);
|
|
50
|
+
if (rank.indexOf(severity) > rank.indexOf(worst)) worst = severity;
|
|
51
|
+
}
|
|
52
|
+
return worst;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Whole percent, so a footer never jitters on a fractional change. */
|
|
56
|
+
export function formatPct(usedPct: number): string {
|
|
57
|
+
return `${Math.round(usedPct)}%`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** The window a compact indicator should show: the one closest to biting. */
|
|
61
|
+
export function primaryWindow(snapshot: UsageSnapshot): UsageWindow | null {
|
|
62
|
+
let best: UsageWindow | null = null;
|
|
63
|
+
for (const window of snapshot.windows) {
|
|
64
|
+
const rank = { normal: 0, caution: 1, warning: 2, critical: 3 };
|
|
65
|
+
if (
|
|
66
|
+
best === null ||
|
|
67
|
+
rank[windowSeverity(window)] > rank[windowSeverity(best)] ||
|
|
68
|
+
(rank[windowSeverity(window)] === rank[windowSeverity(best)] && window.usedPct > best.usedPct)
|
|
69
|
+
)
|
|
70
|
+
best = window;
|
|
71
|
+
}
|
|
72
|
+
return best;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** The named window when the provider reports one, for surfaces that want a fixed column. */
|
|
76
|
+
export function windowByKey(snapshot: UsageSnapshot, key: string): UsageWindow | null {
|
|
77
|
+
return snapshot.windows.find((window) => window.key === key) ?? null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Fold credentials that report the same account.
|
|
82
|
+
*
|
|
83
|
+
* anthropic-max and claude-code are separate credentials on one Anthropic
|
|
84
|
+
* subscription, so showing both doubles the apparent budget. When their
|
|
85
|
+
* windows agree, the one carrying a plan label survives.
|
|
86
|
+
*/
|
|
87
|
+
export function foldDuplicateAccounts(snapshots: ReadonlyArray<UsageSnapshot>): UsageSnapshot[] {
|
|
88
|
+
const kept: UsageSnapshot[] = [];
|
|
89
|
+
for (const snapshot of snapshots) {
|
|
90
|
+
const twin = kept.findIndex((existing) => sameAccount(existing, snapshot));
|
|
91
|
+
if (twin < 0) {
|
|
92
|
+
kept.push(snapshot);
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
const existing = kept[twin];
|
|
96
|
+
if (existing !== undefined && existing.plan == null && snapshot.plan != null) kept[twin] = snapshot;
|
|
97
|
+
}
|
|
98
|
+
return kept;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function sameAccount(left: UsageSnapshot, right: UsageSnapshot): boolean {
|
|
102
|
+
if (left.status !== "ok" || right.status !== "ok") return false;
|
|
103
|
+
const anthropic = new Set(["anthropic-max", "claude-code"]);
|
|
104
|
+
if (left.providerId === right.providerId || !anthropic.has(left.providerId) || !anthropic.has(right.providerId))
|
|
105
|
+
return false;
|
|
106
|
+
if (left.windows.length === 0 || left.windows.length !== right.windows.length) return false;
|
|
107
|
+
return left.windows.every((window, index) => {
|
|
108
|
+
const other = right.windows[index];
|
|
109
|
+
return (
|
|
110
|
+
other !== undefined &&
|
|
111
|
+
other.key === window.key &&
|
|
112
|
+
other.scope === window.scope &&
|
|
113
|
+
(other.resetsAt === window.resetsAt ||
|
|
114
|
+
(other.resetsAt !== null &&
|
|
115
|
+
window.resetsAt !== null &&
|
|
116
|
+
Math.abs(Date.parse(other.resetsAt) - Date.parse(window.resetsAt)) < 1000)) &&
|
|
117
|
+
Math.abs(other.usedPct - window.usedPct) < 0.5
|
|
118
|
+
);
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Lines for the welcome header: one per connected subscription, naming the
|
|
124
|
+
* plan and whatever windows that provider actually reports.
|
|
125
|
+
*/
|
|
126
|
+
export function welcomeQuotaLines(snapshots: ReadonlyArray<UsageSnapshot>): string[] {
|
|
127
|
+
const lines: string[] = [];
|
|
128
|
+
for (const snapshot of foldDuplicateAccounts(snapshots)) {
|
|
129
|
+
if (snapshot.status === "no_credentials") continue;
|
|
130
|
+
const head = snapshot.plan ? `${snapshot.displayName} · ${snapshot.plan}` : snapshot.displayName;
|
|
131
|
+
if (snapshot.status !== "ok") {
|
|
132
|
+
lines.push(`${head} · ${snapshot.message ?? snapshot.status}`);
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
const parts = snapshot.windows.map((window) => `${window.label} ${formatPct(window.usedPct)} used`);
|
|
136
|
+
if (snapshot.credits) parts.push(snapshot.credits.display);
|
|
137
|
+
lines.push(parts.length > 0 ? `${head} · ${parts.join(" · ")}` : `${head} · connected`);
|
|
138
|
+
}
|
|
139
|
+
return lines;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* One compact footer segment across providers.
|
|
144
|
+
*
|
|
145
|
+
* Weekly is the shared column because every provider observed so far
|
|
146
|
+
* reports one. A session window is appended only where it exists, which
|
|
147
|
+
* is what the decision log meant by 5h for Claude models.
|
|
148
|
+
*/
|
|
149
|
+
export function footerQuotaSegment(snapshots: ReadonlyArray<UsageSnapshot>): string | null {
|
|
150
|
+
const parts: string[] = [];
|
|
151
|
+
for (const snapshot of foldDuplicateAccounts(snapshots)) {
|
|
152
|
+
if (snapshot.status !== "ok") continue;
|
|
153
|
+
const weekly = windowByKey(snapshot, "weekly");
|
|
154
|
+
const session = windowByKey(snapshot, "session");
|
|
155
|
+
const shown: string[] = [];
|
|
156
|
+
if (session) shown.push(`5h ${formatPct(session.usedPct)} used`);
|
|
157
|
+
if (weekly) shown.push(`wk ${formatPct(weekly.usedPct)} used`);
|
|
158
|
+
const binding = primaryWindow(snapshot);
|
|
159
|
+
if (shown.length > 0 && binding?.scope && binding !== session && binding !== weekly) {
|
|
160
|
+
shown.push(
|
|
161
|
+
`${binding.scope}${binding.label === binding.scope ? "" : ` ${binding.label}`} ${formatPct(binding.usedPct)} used`,
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
if (shown.length === 0) {
|
|
165
|
+
const fallback = primaryWindow(snapshot);
|
|
166
|
+
if (fallback) shown.push(`${fallback.short ?? fallback.label} ${formatPct(fallback.usedPct)} used`);
|
|
167
|
+
}
|
|
168
|
+
if (shown.length > 0) parts.push(`${shortName(snapshot.displayName)} ${shown.join("/")}`);
|
|
169
|
+
}
|
|
170
|
+
return parts.length > 0 ? parts.join(" · ") : null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function shortName(displayName: string): string {
|
|
174
|
+
const first = displayName.split(" ")[0] ?? displayName;
|
|
175
|
+
return first;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* One line for the welcome banner and any other single-line surface.
|
|
180
|
+
*
|
|
181
|
+
* Paid accounts first, then the free local row, so the contrast between a
|
|
182
|
+
* consumed subscription window and free local inference is visible in one
|
|
183
|
+
* glance.
|
|
184
|
+
*/
|
|
185
|
+
export function quotaSummaryLine(snapshots: ReadonlyArray<UsageSnapshot>): string | null {
|
|
186
|
+
const paid = footerQuotaSegment(snapshots);
|
|
187
|
+
const local = snapshots.find((snapshot) => snapshot.providerId === "local");
|
|
188
|
+
const localPart = local?.credits ? `Local ${local.credits.display}` : null;
|
|
189
|
+
const parts = [paid, localPart].filter((part): part is string => part !== null);
|
|
190
|
+
return parts.length > 0 ? parts.join(" · ") : null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Rows for the expanded dashboard's Status page, one provider per row. */
|
|
194
|
+
export function statusPageQuotaRows(snapshots: ReadonlyArray<UsageSnapshot>): Array<{
|
|
195
|
+
label: string;
|
|
196
|
+
detail: string;
|
|
197
|
+
severity: QuotaSeverity;
|
|
198
|
+
}> {
|
|
199
|
+
return foldDuplicateAccounts(snapshots).map((snapshot) => {
|
|
200
|
+
const label = snapshot.plan ? `${snapshot.displayName} (${snapshot.plan})` : snapshot.displayName;
|
|
201
|
+
if (snapshot.status !== "ok") {
|
|
202
|
+
return { label, detail: snapshot.message ?? snapshot.status, severity: "normal" as QuotaSeverity };
|
|
203
|
+
}
|
|
204
|
+
const windows = snapshot.windows.map(
|
|
205
|
+
(window) => `${window.label} ${formatPct(window.usedPct)} used${resetSuffix(window)}`,
|
|
206
|
+
);
|
|
207
|
+
if (snapshot.credits) windows.push(`credits ${snapshot.credits.display}`);
|
|
208
|
+
return {
|
|
209
|
+
label,
|
|
210
|
+
detail: windows.length > 0 ? windows.join(" · ") : "connected",
|
|
211
|
+
severity: snapshotSeverity(snapshot),
|
|
212
|
+
};
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function resetSuffix(window: UsageWindow): string {
|
|
217
|
+
return window.resetsAt === null ? "" : ` (resets ${window.resetsAt.slice(0, 16).replace("T", " ")}Z)`;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* The local-inference row.
|
|
222
|
+
*
|
|
223
|
+
* A local model consumes no subscription window and no credit, so it is
|
|
224
|
+
* reported as free rather than omitted. Showing it beside the paid rows is
|
|
225
|
+
* the point: work routed locally is work that did not spend a quota.
|
|
226
|
+
* The saved-quota figure is deliberately absent here, because proving it
|
|
227
|
+
* needs per-target token attribution this slice does not collect.
|
|
228
|
+
*/
|
|
229
|
+
export function localQuotaSnapshot(options: { runtimeLabel?: string; fetchedAt?: string | null } = {}): UsageSnapshot {
|
|
230
|
+
return {
|
|
231
|
+
providerId: "local",
|
|
232
|
+
displayName: options.runtimeLabel ?? "Local AI",
|
|
233
|
+
status: "ok",
|
|
234
|
+
windows: [],
|
|
235
|
+
credits: { display: "$0.00", usedPct: null },
|
|
236
|
+
plan: "Local",
|
|
237
|
+
message: "no subscription window consumed",
|
|
238
|
+
fetchedAt: options.fetchedAt ?? null,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registry of quota adapters.
|
|
3
|
+
*
|
|
4
|
+
* Order is display order. Every adapter is read-only and reports a missing
|
|
5
|
+
* credential as a snapshot status, so listing one costs nothing when the
|
|
6
|
+
* operator does not use that product.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { createAnthropicMaxQuotaProvider } from "./anthropic-max-provider.js";
|
|
10
|
+
import { createAntigravityQuotaProvider } from "./antigravity-provider.js";
|
|
11
|
+
import { createClaudeCodeQuotaProvider } from "./claude-code-provider.js";
|
|
12
|
+
import { createCodexQuotaProvider } from "./codex-provider.js";
|
|
13
|
+
import type { QuotaProvider } from "./types.js";
|
|
14
|
+
|
|
15
|
+
export function buildQuotaProviders(): QuotaProvider[] {
|
|
16
|
+
return [
|
|
17
|
+
createAnthropicMaxQuotaProvider(),
|
|
18
|
+
createClaudeCodeQuotaProvider(),
|
|
19
|
+
createCodexQuotaProvider(),
|
|
20
|
+
createAntigravityQuotaProvider(),
|
|
21
|
+
// The copilot adapter attaches here once its credential shape is confirmed.
|
|
22
|
+
];
|
|
23
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session-facing quota reader.
|
|
3
|
+
*
|
|
4
|
+
* Refresh stays lazy, as the decision log requires: no background timer.
|
|
5
|
+
* A surface asks for snapshots, and the service either serves cached ones
|
|
6
|
+
* or spends one read per provider. Every provider is read concurrently
|
|
7
|
+
* because they are independent accounts, and a slow one must not hold up
|
|
8
|
+
* the others.
|
|
9
|
+
*
|
|
10
|
+
* Nothing here throws. A provider that fails contributes a snapshot whose
|
|
11
|
+
* status says so, which is what the surfaces render.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { createQuotaCache, DEFAULT_QUOTA_CACHE_TTL_MS, type QuotaCache } from "./cache.js";
|
|
15
|
+
import { localQuotaSnapshot } from "./presentation.js";
|
|
16
|
+
import { buildQuotaProviders } from "./registry.js";
|
|
17
|
+
import type { QuotaProvider, UsageSnapshot } from "./types.js";
|
|
18
|
+
|
|
19
|
+
export interface QuotaServiceOptions {
|
|
20
|
+
providers?: QuotaProvider[];
|
|
21
|
+
cache?: QuotaCache;
|
|
22
|
+
ttlMs?: number;
|
|
23
|
+
/** Include the free local-inference row; surfaces that list paid accounts only can opt out. */
|
|
24
|
+
includeLocal?: boolean;
|
|
25
|
+
localRuntimeLabel?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface QuotaService {
|
|
29
|
+
/** Cached snapshots without spending a read, for a first paint. */
|
|
30
|
+
peek(): UsageSnapshot[];
|
|
31
|
+
/** Snapshots, refreshing any provider whose cached value has expired. */
|
|
32
|
+
read(): Promise<UsageSnapshot[]>;
|
|
33
|
+
/** Providers holding usable credentials, without spending a usage read. */
|
|
34
|
+
detected(): Promise<QuotaProvider[]>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function createQuotaService(options: QuotaServiceOptions = {}): QuotaService {
|
|
38
|
+
const providers = options.providers ?? buildQuotaProviders();
|
|
39
|
+
const cache = options.cache ?? createQuotaCache({ ttlMs: options.ttlMs ?? DEFAULT_QUOTA_CACHE_TTL_MS });
|
|
40
|
+
const includeLocal = options.includeLocal ?? true;
|
|
41
|
+
|
|
42
|
+
const withLocal = (snapshots: UsageSnapshot[]): UsageSnapshot[] => {
|
|
43
|
+
if (!includeLocal) return snapshots;
|
|
44
|
+
const local = options.localRuntimeLabel
|
|
45
|
+
? localQuotaSnapshot({ runtimeLabel: options.localRuntimeLabel })
|
|
46
|
+
: localQuotaSnapshot();
|
|
47
|
+
return [...snapshots, local];
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
peek(): UsageSnapshot[] {
|
|
52
|
+
const cached: UsageSnapshot[] = [];
|
|
53
|
+
for (const provider of providers) {
|
|
54
|
+
const snapshot = cache.readLastGood(provider.id);
|
|
55
|
+
if (snapshot !== null) cached.push(snapshot);
|
|
56
|
+
}
|
|
57
|
+
return withLocal(cached);
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
async read(): Promise<UsageSnapshot[]> {
|
|
61
|
+
const settled = await Promise.all(
|
|
62
|
+
providers.map(async (provider) => {
|
|
63
|
+
try {
|
|
64
|
+
return await cache.resolve(provider);
|
|
65
|
+
} catch (error) {
|
|
66
|
+
return {
|
|
67
|
+
providerId: provider.id,
|
|
68
|
+
displayName: provider.displayName,
|
|
69
|
+
status: "error" as const,
|
|
70
|
+
windows: [],
|
|
71
|
+
message: error instanceof Error ? error.message : String(error),
|
|
72
|
+
fetchedAt: null,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
}),
|
|
76
|
+
);
|
|
77
|
+
return withLocal(settled.filter((snapshot) => snapshot.status !== "no_credentials"));
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
async detected(): Promise<QuotaProvider[]> {
|
|
81
|
+
const flags = await Promise.all(
|
|
82
|
+
providers.map(async (provider) => {
|
|
83
|
+
try {
|
|
84
|
+
return await provider.detect();
|
|
85
|
+
} catch {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
}),
|
|
89
|
+
);
|
|
90
|
+
return providers.filter((_, index) => flags[index] === true);
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A synchronous quota line for render paths, refreshed off the frame.
|
|
3
|
+
*
|
|
4
|
+
* The welcome banner and the footer are called on every frame and may not
|
|
5
|
+
* await anything or touch the network. This feed answers instantly from the
|
|
6
|
+
* last reading, and when that reading is missing or stale it starts one in
|
|
7
|
+
* the background and calls `onUpdate` when a newer line is ready, which is
|
|
8
|
+
* how a frame gets requested. That keeps the decision log's lazy refresh
|
|
9
|
+
* rule: a read happens because a surface was looked at, never on a timer.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { quotaSummaryLine } from "./presentation.js";
|
|
13
|
+
import { createQuotaService, type QuotaService } from "./service.js";
|
|
14
|
+
import type { UsageSnapshot } from "./types.js";
|
|
15
|
+
|
|
16
|
+
export const QUOTA_SUMMARY_TTL_MS = 5 * 60_000;
|
|
17
|
+
|
|
18
|
+
export interface QuotaSummaryFeedOptions {
|
|
19
|
+
service?: QuotaService;
|
|
20
|
+
/** Called after a background read changed any snapshot detail. */
|
|
21
|
+
onUpdate?: () => void;
|
|
22
|
+
ttlMs?: number;
|
|
23
|
+
now?: () => number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface QuotaSummaryFeed {
|
|
27
|
+
/** The current line, or null until the first reading lands. Never blocks. */
|
|
28
|
+
peek(): string | null;
|
|
29
|
+
peekSnapshots(): ReadonlyArray<UsageSnapshot>;
|
|
30
|
+
/** Stop background reads from requesting frames after teardown. */
|
|
31
|
+
dispose(): void;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function createQuotaSummaryFeed(options: QuotaSummaryFeedOptions = {}): QuotaSummaryFeed {
|
|
35
|
+
const service = options.service ?? createQuotaService();
|
|
36
|
+
const ttlMs = options.ttlMs ?? QUOTA_SUMMARY_TTL_MS;
|
|
37
|
+
const now = options.now ?? Date.now;
|
|
38
|
+
|
|
39
|
+
let line: string | null = null;
|
|
40
|
+
let snapshots: ReadonlyArray<UsageSnapshot> = [];
|
|
41
|
+
let readAt: number | null = null;
|
|
42
|
+
let inFlight = false;
|
|
43
|
+
let disposed = false;
|
|
44
|
+
|
|
45
|
+
const refresh = (): void => {
|
|
46
|
+
if (inFlight || disposed) return;
|
|
47
|
+
inFlight = true;
|
|
48
|
+
void Promise.resolve()
|
|
49
|
+
.then(() => (disposed ? snapshots : service.read()))
|
|
50
|
+
.then((nextSnapshots) => {
|
|
51
|
+
if (disposed) return;
|
|
52
|
+
const next = quotaSummaryLine(nextSnapshots);
|
|
53
|
+
const changed = JSON.stringify(nextSnapshots) !== JSON.stringify(snapshots);
|
|
54
|
+
snapshots = nextSnapshots;
|
|
55
|
+
readAt = now();
|
|
56
|
+
if (changed) {
|
|
57
|
+
line = next;
|
|
58
|
+
options.onUpdate?.();
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
.catch(() => {
|
|
62
|
+
// A failed read leaves the previous line standing. Provider failures
|
|
63
|
+
// already arrive as snapshot statuses, so reaching here means
|
|
64
|
+
// something outside the adapters broke, and the banner should not
|
|
65
|
+
// start flickering because of it.
|
|
66
|
+
readAt = now();
|
|
67
|
+
})
|
|
68
|
+
.finally(() => {
|
|
69
|
+
inFlight = false;
|
|
70
|
+
});
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
peek(): string | null {
|
|
75
|
+
if (!disposed && (readAt === null || now() - readAt >= ttlMs)) refresh();
|
|
76
|
+
return line;
|
|
77
|
+
},
|
|
78
|
+
peekSnapshots(): ReadonlyArray<UsageSnapshot> {
|
|
79
|
+
if (!disposed && (readAt === null || now() - readAt >= ttlMs)) refresh();
|
|
80
|
+
return snapshots;
|
|
81
|
+
},
|
|
82
|
+
dispose(): void {
|
|
83
|
+
disposed = true;
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|