@repo-os/repoos 0.5.45
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/LICENSE.md +110 -0
- package/README.md +272 -0
- package/dist/.build-info.json +4 -0
- package/dist/.build-stamp.json +3 -0
- package/dist/cli/colors.d.ts +13 -0
- package/dist/cli/colors.js +37 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +221 -0
- package/dist/commands/check.d.ts +170 -0
- package/dist/commands/check.js +1227 -0
- package/dist/commands/docs.d.ts +2 -0
- package/dist/commands/docs.js +70 -0
- package/dist/commands/gc.d.ts +1 -0
- package/dist/commands/gc.js +57 -0
- package/dist/commands/init.d.ts +9 -0
- package/dist/commands/init.js +699 -0
- package/dist/commands/screenshots.d.ts +7 -0
- package/dist/commands/screenshots.js +77 -0
- package/dist/commands/serve.d.ts +55 -0
- package/dist/commands/serve.js +204 -0
- package/dist/commands/status.d.ts +140 -0
- package/dist/commands/status.js +569 -0
- package/dist/commands/stop.d.ts +1 -0
- package/dist/commands/stop.js +59 -0
- package/dist/commands/tasks.d.ts +47 -0
- package/dist/commands/tasks.js +334 -0
- package/dist/commands/tunnel.d.ts +1 -0
- package/dist/commands/tunnel.js +1271 -0
- package/dist/commands/ui-harness.d.ts +53 -0
- package/dist/commands/ui-harness.js +62 -0
- package/dist/commands/ui-smoke.d.ts +6 -0
- package/dist/commands/ui-smoke.js +220 -0
- package/dist/commands/upgrade.d.ts +15 -0
- package/dist/commands/upgrade.js +188 -0
- package/dist/core/auth-store.d.ts +81 -0
- package/dist/core/auth-store.js +530 -0
- package/dist/core/auth.d.ts +75 -0
- package/dist/core/auth.js +124 -0
- package/dist/core/bootstrap.d.ts +41 -0
- package/dist/core/bootstrap.js +236 -0
- package/dist/core/build.d.ts +92 -0
- package/dist/core/build.js +249 -0
- package/dist/core/canary.d.ts +10 -0
- package/dist/core/canary.js +14 -0
- package/dist/core/check-failure-summary.d.ts +10 -0
- package/dist/core/check-failure-summary.js +34 -0
- package/dist/core/config.d.ts +160 -0
- package/dist/core/config.js +1288 -0
- package/dist/core/context-pack.d.ts +74 -0
- package/dist/core/context-pack.js +778 -0
- package/dist/core/db.d.ts +236 -0
- package/dist/core/db.js +747 -0
- package/dist/core/detect.d.ts +69 -0
- package/dist/core/detect.js +251 -0
- package/dist/core/docs.d.ts +76 -0
- package/dist/core/docs.js +225 -0
- package/dist/core/frontmatter.d.ts +29 -0
- package/dist/core/frontmatter.js +215 -0
- package/dist/core/git.d.ts +505 -0
- package/dist/core/git.js +1453 -0
- package/dist/core/indexer.d.ts +33 -0
- package/dist/core/indexer.js +282 -0
- package/dist/core/input.d.ts +66 -0
- package/dist/core/input.js +222 -0
- package/dist/core/logger.d.ts +70 -0
- package/dist/core/logger.js +138 -0
- package/dist/core/models.d.ts +45 -0
- package/dist/core/models.js +291 -0
- package/dist/core/net-probe.d.ts +4 -0
- package/dist/core/net-probe.js +20 -0
- package/dist/core/providers/deepinfra.d.ts +23 -0
- package/dist/core/providers/deepinfra.js +44 -0
- package/dist/core/providers/index.d.ts +24 -0
- package/dist/core/providers/index.js +59 -0
- package/dist/core/providers/openrouter.d.ts +28 -0
- package/dist/core/providers/openrouter.js +82 -0
- package/dist/core/providers/spend.d.ts +86 -0
- package/dist/core/providers/spend.js +286 -0
- package/dist/core/providers/types.d.ts +56 -0
- package/dist/core/providers/types.js +39 -0
- package/dist/core/repoos.d.ts +59 -0
- package/dist/core/repoos.js +221 -0
- package/dist/core/runtime.d.ts +35 -0
- package/dist/core/runtime.js +167 -0
- package/dist/core/skills-registry.d.ts +39 -0
- package/dist/core/skills-registry.js +148 -0
- package/dist/core/skills-registry.test.d.ts +1 -0
- package/dist/core/skills-registry.test.js +52 -0
- package/dist/core/sysmem.d.ts +7 -0
- package/dist/core/sysmem.js +55 -0
- package/dist/core/tailscale.d.ts +2 -0
- package/dist/core/tailscale.js +19 -0
- package/dist/core/task.d.ts +53 -0
- package/dist/core/task.js +310 -0
- package/dist/core/toml-line.d.ts +20 -0
- package/dist/core/toml-line.js +52 -0
- package/dist/core/toml-validate.d.ts +44 -0
- package/dist/core/toml-validate.js +335 -0
- package/dist/core/tunnel-assistant.d.ts +30 -0
- package/dist/core/tunnel-assistant.js +65 -0
- package/dist/core/tunnel-registry.d.ts +96 -0
- package/dist/core/tunnel-registry.js +267 -0
- package/dist/core/tunnel-registry.test.d.ts +1 -0
- package/dist/core/tunnel-registry.test.js +309 -0
- package/dist/core/tunnel.d.ts +103 -0
- package/dist/core/tunnel.js +320 -0
- package/dist/core/types.d.ts +811 -0
- package/dist/core/types.js +27 -0
- package/dist/core/worktree-gc.d.ts +57 -0
- package/dist/core/worktree-gc.js +194 -0
- package/dist/server/__tests__/integration-job.test.d.ts +1 -0
- package/dist/server/__tests__/integration-job.test.js +129 -0
- package/dist/server/agents.d.ts +1020 -0
- package/dist/server/agents.js +4070 -0
- package/dist/server/attachments.d.ts +51 -0
- package/dist/server/attachments.js +130 -0
- package/dist/server/auto-engineering.d.ts +45 -0
- package/dist/server/auto-engineering.js +275 -0
- package/dist/server/branchless-release.d.ts +22 -0
- package/dist/server/branchless-release.js +67 -0
- package/dist/server/built-in-agents.d.ts +305 -0
- package/dist/server/built-in-agents.js +1838 -0
- package/dist/server/cto-monitor.d.ts +35 -0
- package/dist/server/cto-monitor.js +281 -0
- package/dist/server/cto.d.ts +125 -0
- package/dist/server/cto.js +590 -0
- package/dist/server/deployments.d.ts +119 -0
- package/dist/server/deployments.js +325 -0
- package/dist/server/diff-snapshot.d.ts +20 -0
- package/dist/server/diff-snapshot.js +73 -0
- package/dist/server/done.d.ts +160 -0
- package/dist/server/done.js +488 -0
- package/dist/server/freeform.d.ts +55 -0
- package/dist/server/freeform.js +179 -0
- package/dist/server/handoff.d.ts +99 -0
- package/dist/server/handoff.js +575 -0
- package/dist/server/hetzner.d.ts +45 -0
- package/dist/server/hetzner.js +117 -0
- package/dist/server/icons.d.ts +8 -0
- package/dist/server/icons.js +195 -0
- package/dist/server/integration-job.d.ts +75 -0
- package/dist/server/integration-job.js +144 -0
- package/dist/server/integration-orchestrator.d.ts +211 -0
- package/dist/server/integration-orchestrator.js +1299 -0
- package/dist/server/integration-status.d.ts +48 -0
- package/dist/server/integration-status.js +71 -0
- package/dist/server/live-index.d.ts +295 -0
- package/dist/server/live-index.js +452 -0
- package/dist/server/model-test.d.ts +20 -0
- package/dist/server/model-test.js +100 -0
- package/dist/server/ntfy.d.ts +57 -0
- package/dist/server/ntfy.js +112 -0
- package/dist/server/pm-attachments.d.ts +35 -0
- package/dist/server/pm-attachments.js +181 -0
- package/dist/server/pm-runs.d.ts +58 -0
- package/dist/server/pm-runs.js +80 -0
- package/dist/server/preview.d.ts +174 -0
- package/dist/server/preview.js +641 -0
- package/dist/server/release.d.ts +88 -0
- package/dist/server/release.js +449 -0
- package/dist/server/reload.d.ts +194 -0
- package/dist/server/reload.js +586 -0
- package/dist/server/remote-validation.d.ts +132 -0
- package/dist/server/remote-validation.js +478 -0
- package/dist/server/repo-lock.d.ts +36 -0
- package/dist/server/repo-lock.js +141 -0
- package/dist/server/review-guard.d.ts +14 -0
- package/dist/server/review-guard.js +159 -0
- package/dist/server/review.d.ts +243 -0
- package/dist/server/review.js +1211 -0
- package/dist/server/routes/agents.d.ts +6 -0
- package/dist/server/routes/agents.js +28 -0
- package/dist/server/routes/auth.d.ts +60 -0
- package/dist/server/routes/auth.js +755 -0
- package/dist/server/routes/config.d.ts +20 -0
- package/dist/server/routes/config.js +347 -0
- package/dist/server/routes/debugger.d.ts +33 -0
- package/dist/server/routes/debugger.js +360 -0
- package/dist/server/routes/deployments.d.ts +3 -0
- package/dist/server/routes/deployments.js +16 -0
- package/dist/server/routes/docs.d.ts +8 -0
- package/dist/server/routes/docs.js +135 -0
- package/dist/server/routes/helpers.d.ts +21 -0
- package/dist/server/routes/helpers.js +171 -0
- package/dist/server/routes/index.d.ts +19 -0
- package/dist/server/routes/index.js +18 -0
- package/dist/server/routes/info.d.ts +17 -0
- package/dist/server/routes/info.js +177 -0
- package/dist/server/routes/inputs.d.ts +7 -0
- package/dist/server/routes/inputs.js +125 -0
- package/dist/server/routes/model-providers.d.ts +27 -0
- package/dist/server/routes/model-providers.js +94 -0
- package/dist/server/routes/models.d.ts +3 -0
- package/dist/server/routes/models.js +36 -0
- package/dist/server/routes/notify.d.ts +2 -0
- package/dist/server/routes/notify.js +17 -0
- package/dist/server/routes/playground.d.ts +29 -0
- package/dist/server/routes/playground.js +81 -0
- package/dist/server/routes/release.d.ts +20 -0
- package/dist/server/routes/release.js +86 -0
- package/dist/server/routes/router.d.ts +12 -0
- package/dist/server/routes/router.js +32 -0
- package/dist/server/routes/skill-registry.d.ts +5 -0
- package/dist/server/routes/skill-registry.js +51 -0
- package/dist/server/routes/tasks.d.ts +58 -0
- package/dist/server/routes/tasks.js +1442 -0
- package/dist/server/routes/transcribe.d.ts +2 -0
- package/dist/server/routes/transcribe.js +124 -0
- package/dist/server/routes/types.d.ts +68 -0
- package/dist/server/routes/types.js +1 -0
- package/dist/server/routes/ui.d.ts +5 -0
- package/dist/server/routes/ui.js +81 -0
- package/dist/server/routes/utils.d.ts +3 -0
- package/dist/server/routes/utils.js +23 -0
- package/dist/server/serve-reaper.d.ts +135 -0
- package/dist/server/serve-reaper.js +491 -0
- package/dist/server/server.d.ts +49 -0
- package/dist/server/server.js +2171 -0
- package/dist/server/supervisor.d.ts +89 -0
- package/dist/server/supervisor.js +231 -0
- package/dist/server/system.d.ts +198 -0
- package/dist/server/system.js +480 -0
- package/dist/server/task-check.d.ts +45 -0
- package/dist/server/task-check.js +71 -0
- package/dist/server/task-transitions.d.ts +51 -0
- package/dist/server/task-transitions.js +78 -0
- package/dist/server/task-watchdog.d.ts +158 -0
- package/dist/server/task-watchdog.js +681 -0
- package/dist/server/test-run.d.ts +28 -0
- package/dist/server/test-run.js +81 -0
- package/dist/server/watcher.d.ts +21 -0
- package/dist/server/watcher.js +194 -0
- package/dist/server/write.d.ts +94 -0
- package/dist/server/write.js +345 -0
- package/dist/ui/assets/AgentsView-CF4wRvHh.css +1 -0
- package/dist/ui/assets/AgentsView-D-myL-kA.js +1 -0
- package/dist/ui/assets/ContextView-D3TC-RSp.css +1 -0
- package/dist/ui/assets/ContextView-DsOqAdyE.js +4 -0
- package/dist/ui/assets/DashboardView-BOhsIJqO.js +1 -0
- package/dist/ui/assets/DashboardView-Ck9o0RP2.css +1 -0
- package/dist/ui/assets/DeploymentsView--iIUVfEH.css +1 -0
- package/dist/ui/assets/DeploymentsView-DXu_FYR2.js +1 -0
- package/dist/ui/assets/InputsView-24HVbVPk.js +1 -0
- package/dist/ui/assets/InputsView-BLb7GHtx.css +1 -0
- package/dist/ui/assets/LoginView-6QlGg6cx.css +1 -0
- package/dist/ui/assets/LoginView-BqupI0Lt.js +1 -0
- package/dist/ui/assets/ReleasesView-BEG1A0YH.css +1 -0
- package/dist/ui/assets/ReleasesView-CAgMuxpe.js +3 -0
- package/dist/ui/assets/SettingsView-CA-NYSMK.css +1 -0
- package/dist/ui/assets/SettingsView-O3iZSnjg.js +3 -0
- package/dist/ui/assets/WorkView-D8Tl6qwy.js +1 -0
- package/dist/ui/assets/WorkView-DMYL2s3W.css +1 -0
- package/dist/ui/assets/abnfDiagram-VCTEODGH-J5lkSnEE.js +1 -0
- package/dist/ui/assets/arc-BHZnmhhM.js +1 -0
- package/dist/ui/assets/architecture-7GRP2DOG-pLdFt-xS.js +1 -0
- package/dist/ui/assets/architectureDiagram-5GKGNRK7-C3k89uKX.js +36 -0
- package/dist/ui/assets/array-BifhSqXX.js +1 -0
- package/dist/ui/assets/blockDiagram-I7D4REHJ-AWNoBzop.js +129 -0
- package/dist/ui/assets/button-lbikqSph.js +1 -0
- package/dist/ui/assets/c4Diagram-7LVT6UL2-DKG1D5gd.js +38 -0
- package/dist/ui/assets/channel-gA4qBv3A.js +1 -0
- package/dist/ui/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
- package/dist/ui/assets/chunk-4HAMMTFA-D1YR8lF_.js +62 -0
- package/dist/ui/assets/chunk-5VM5RSS4-ZNzvKenW.js +15 -0
- package/dist/ui/assets/chunk-75Z2AOVW-5GuBxfsL.js +2 -0
- package/dist/ui/assets/chunk-DU6HZSFF-DfdNNqwa.js +127 -0
- package/dist/ui/assets/chunk-F27PBJKO-BPZzinQ4.js +1 -0
- package/dist/ui/assets/chunk-FOHPRMQF-l2z4K2jD.js +161 -0
- package/dist/ui/assets/chunk-GMAD6QVW-DGWvSKj5.js +72 -0
- package/dist/ui/assets/chunk-GVQU2GXP-D6Aw75wJ.js +1 -0
- package/dist/ui/assets/chunk-IMKFNOWR-B8mMBKCW.js +231 -0
- package/dist/ui/assets/chunk-JWPE2WC7-DVXcaiue.js +1 -0
- package/dist/ui/assets/chunk-L3NEJ4N5-D5OJuXYE.js +1 -0
- package/dist/ui/assets/chunk-OSK3NFVY-B7r9w2UW.js +10 -0
- package/dist/ui/assets/chunk-P2QGCYS3-C74X4AQX.js +1 -0
- package/dist/ui/assets/chunk-POPQ4Y6H-C5_u1gUS.js +1 -0
- package/dist/ui/assets/chunk-PWAF6VOD-DfoTLtxn.js +1 -0
- package/dist/ui/assets/chunk-SHT3W25Y-D40m2eqz.js +168 -0
- package/dist/ui/assets/chunk-SVP7TREG-euxi0Kap.js +88 -0
- package/dist/ui/assets/chunk-TICWLB2K-fkdxngzN.js +206 -0
- package/dist/ui/assets/chunk-XXDRQBXY-DFBUG-OT.js +1 -0
- package/dist/ui/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
- package/dist/ui/assets/classDiagram-ZZMXUADV-BzW8bq2P.js +1 -0
- package/dist/ui/assets/classDiagram-v2-VYDZK3BY-BzW8bq2P.js +1 -0
- package/dist/ui/assets/cose-bilkent-JH36ORCC-BTcgMon1.js +1 -0
- package/dist/ui/assets/cynefin-OW5HDTMX-B5m_COsS.js +1 -0
- package/dist/ui/assets/cynefinDiagram-5FMLGOSQ-C-AG6CGe.js +62 -0
- package/dist/ui/assets/cytoscape.esm-FBntH-9t.js +321 -0
- package/dist/ui/assets/dagre-GXQ25YYZ-DDfDnp8L.js +4 -0
- package/dist/ui/assets/dagre-PrKaheQc.js +1 -0
- package/dist/ui/assets/defaultLocale-BFoDCU3G.js +1 -0
- package/dist/ui/assets/diagram-S7CK7UJ4-DjQZAlEY.js +30 -0
- package/dist/ui/assets/diagram-UQ7AKVKN-DAZ9yp1B.js +41 -0
- package/dist/ui/assets/diagram-VSXAHHWV-BWepUKJD.js +3 -0
- package/dist/ui/assets/diagram-VX7I27RA-EcV1J81b.js +24 -0
- package/dist/ui/assets/diagram-Z3DM3KII-Dbz7XU_K.js +24 -0
- package/dist/ui/assets/dist-DTg6UBE_.js +1 -0
- package/dist/ui/assets/ebnfDiagram-PWID7BFC-BUyQw8Mm.js +1 -0
- package/dist/ui/assets/erDiagram-RLTQ6QDP-WVf8ukKt.js +99 -0
- package/dist/ui/assets/eventmodeling-NTZA5JFV-DHqyLOAn.js +1 -0
- package/dist/ui/assets/flowDiagram-HODETNUW-CYk_ze5K.js +1 -0
- package/dist/ui/assets/ganttDiagram-EL5Y4UJY-rB4MZuB-.js +292 -0
- package/dist/ui/assets/gitGraph-4MIJSDKK-BOTWM8dY.js +1 -0
- package/dist/ui/assets/gitGraphDiagram-WWUBYQGX-eYscuhtP.js +106 -0
- package/dist/ui/assets/graphlib-DS17s2tU.js +1 -0
- package/dist/ui/assets/index-BACBfnga.css +1 -0
- package/dist/ui/assets/index-BOQ-ust6.js +26 -0
- package/dist/ui/assets/info-A6RAGUB7-_67wOORe.js +1 -0
- package/dist/ui/assets/infoDiagram-27XIBGKW-XVXbddTA.js +2 -0
- package/dist/ui/assets/init-C-OQMol4.js +1 -0
- package/dist/ui/assets/ishikawaDiagram-5VMMS53U-ClLNiEru.js +70 -0
- package/dist/ui/assets/journeyDiagram-3NMN7TZE-NRawjR6i.js +139 -0
- package/dist/ui/assets/kanban-definition-UXKFOSKX-BgSzBfIx.js +89 -0
- package/dist/ui/assets/katex-DolUETbr.js +257 -0
- package/dist/ui/assets/line-KAyfuul4.js +1 -0
- package/dist/ui/assets/linear-cCCPOpu7.js +1 -0
- package/dist/ui/assets/mermaid-parser.core-CgXZk1iN.js +7 -0
- package/dist/ui/assets/mermaid.core-BUUkChHW.js +44 -0
- package/dist/ui/assets/mindmap-definition-YA3MSWOX-Cpn7vW4r.js +96 -0
- package/dist/ui/assets/ordinal-BDEzSJ7C.js +1 -0
- package/dist/ui/assets/packet-AYTQ26CC-0lZwM-d6.js +1 -0
- package/dist/ui/assets/path-COt_16Va.js +1 -0
- package/dist/ui/assets/pegDiagram-XKGWAZYB-impy0inn.js +1 -0
- package/dist/ui/assets/pie-WAS4IAKB-CqfdGHYg.js +1 -0
- package/dist/ui/assets/pieDiagram-E7YTZNPT-CxjL4I1h.js +39 -0
- package/dist/ui/assets/quadrantDiagram-AXDQQJYC-Cjd1QMIe.js +7 -0
- package/dist/ui/assets/radar-RG4KPBEZ-CRkQIwiv.js +1 -0
- package/dist/ui/assets/railroad-74A4TZTK-i67XSWTB.js +1 -0
- package/dist/ui/assets/railroad-abnf-HS5TGJTU-Cv9m5aS3.js +1 -0
- package/dist/ui/assets/railroad-ebnf-LZEXJU2U-C5ZIn-Jh.js +1 -0
- package/dist/ui/assets/railroad-peg-WCYAUIDC-CGra9Oq-.js +1 -0
- package/dist/ui/assets/railroadDiagram-O6MQD6OU-_pK0qyaY.js +1 -0
- package/dist/ui/assets/releases-B0WNV-SY.js +1 -0
- package/dist/ui/assets/repoos-cto-square.webp +0 -0
- package/dist/ui/assets/repoos-helpful-pm-square.webp +0 -0
- package/dist/ui/assets/repoos-orchestrator-square.webp +0 -0
- package/dist/ui/assets/repoos-performance-agent-square.webp +0 -0
- package/dist/ui/assets/repoos-ross-from-friends-square.webp +0 -0
- package/dist/ui/assets/repoos-tech-debt-agent-square.webp +0 -0
- package/dist/ui/assets/requirementDiagram-BXWQKSXE-CRrFX-yw.js +84 -0
- package/dist/ui/assets/rough.esm-By172zw-.js +1 -0
- package/dist/ui/assets/sankeyDiagram-P5KCCOFB-X_xFRj9E.js +40 -0
- package/dist/ui/assets/sequenceDiagram-WJ2MYXX4-Byb49lKf.js +162 -0
- package/dist/ui/assets/sizeCapture-INFHLROL-B0uUizjq.js +1 -0
- package/dist/ui/assets/src-BH-TyZbA.js +1 -0
- package/dist/ui/assets/stateDiagram-D77RDMKH-CzaPrqYF.js +1 -0
- package/dist/ui/assets/stateDiagram-v2-MP3YSRHH-BCXckz7K.js +1 -0
- package/dist/ui/assets/swimlanes-42K2YHIH-Cx_1lnqr.js +1 -0
- package/dist/ui/assets/swimlanesDiagram-VR7AAH4N-54qsKi6A.js +8 -0
- package/dist/ui/assets/timeline-definition-24CTP7MA-B1IbZPQ-.js +120 -0
- package/dist/ui/assets/treeView-Q6P3EWNA-DFWhVE3d.js +1 -0
- package/dist/ui/assets/treemap-WGGIJYW6-BmbYPThn.js +1 -0
- package/dist/ui/assets/vendor-ui-DPm7zLxi.js +3 -0
- package/dist/ui/assets/vendor-vue-CobgyEJp.js +1 -0
- package/dist/ui/assets/vennDiagram-4TSXK5OY-Ae2AtRpt.js +34 -0
- package/dist/ui/assets/wardley-WFR3VGLG-BtD4X4tL.js +1 -0
- package/dist/ui/assets/wardleyDiagram-VM6X3IG4-B3YN2fTd.js +78 -0
- package/dist/ui/assets/xychartDiagram-S5SC5T6Z-CIzJfMKA.js +7 -0
- package/dist/ui/favicon.svg +11 -0
- package/dist/ui/index.html +49 -0
- package/dist/ui/sw.js +48 -0
- package/package.json +91 -0
package/dist/core/db.js
ADDED
|
@@ -0,0 +1,747 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQLite database layer for RepoOS.
|
|
3
|
+
*
|
|
4
|
+
* Provides durable storage for session stats (time, tokens, cost) across all
|
|
5
|
+
* agent types. Uses bun:sqlite (Bun) or node:sqlite (Node >= 22), with graceful
|
|
6
|
+
* degradation if neither is available.
|
|
7
|
+
*
|
|
8
|
+
* Zero runtime dependencies — sqlite is a platform builtin.
|
|
9
|
+
*/
|
|
10
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
11
|
+
import { createRequire } from "node:module";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
let Database;
|
|
14
|
+
let dbAvailable = false;
|
|
15
|
+
let sqliteLoadAttempted = false;
|
|
16
|
+
// RepoOS is ESM, so the CommonJS global `require` is not available. Create a
|
|
17
|
+
// local resolver for optional runtime builtins instead; otherwise telemetry
|
|
18
|
+
// silently degrades even on Node versions that provide `node:sqlite`.
|
|
19
|
+
const runtimeRequire = createRequire(import.meta.url);
|
|
20
|
+
/**
|
|
21
|
+
* Load SQLite on first actual use, not at module import time. Node prints an
|
|
22
|
+
* ExperimentalWarning the moment `node:sqlite` is required on some Node
|
|
23
|
+
* versions — since this module is reachable from nearly every command's
|
|
24
|
+
* import graph, requiring it eagerly at the top of the file would fire that
|
|
25
|
+
* warning before the CLI even dispatches to a command, before anything has
|
|
26
|
+
* a chance to suppress it. Both Bun and Node 22+ have builtin sqlite support;
|
|
27
|
+
* we try Bun first, then fall back to node:sqlite, and gracefully degrade if
|
|
28
|
+
* neither is available.
|
|
29
|
+
*/
|
|
30
|
+
function loadSqlite() {
|
|
31
|
+
if (sqliteLoadAttempted)
|
|
32
|
+
return;
|
|
33
|
+
sqliteLoadAttempted = true;
|
|
34
|
+
try {
|
|
35
|
+
const global = globalThis;
|
|
36
|
+
if (global.Bun && typeof global.Bun === "object") {
|
|
37
|
+
// Running in Bun — use bun:sqlite if available
|
|
38
|
+
try {
|
|
39
|
+
const sqlite = runtimeRequire("bun:sqlite");
|
|
40
|
+
Database = sqlite.Database;
|
|
41
|
+
dbAvailable = true;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// bun:sqlite not available, try node:sqlite below
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
// Fallback to node:sqlite (Node 22+)
|
|
48
|
+
if (!dbAvailable) {
|
|
49
|
+
try {
|
|
50
|
+
const sqlite = runtimeRequire("node:sqlite");
|
|
51
|
+
Database = sqlite.DatabaseSync;
|
|
52
|
+
dbAvailable = true;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// node:sqlite not available — degrade gracefully
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// Any error during initialization — degrade gracefully
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/** Schema versions and their migrations. */
|
|
64
|
+
const MIGRATIONS = [
|
|
65
|
+
{
|
|
66
|
+
version: 1,
|
|
67
|
+
up: `
|
|
68
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
69
|
+
sessionId TEXT PRIMARY KEY,
|
|
70
|
+
sessionType TEXT NOT NULL,
|
|
71
|
+
taskId TEXT,
|
|
72
|
+
agent TEXT NOT NULL,
|
|
73
|
+
model TEXT NOT NULL,
|
|
74
|
+
codingAgent TEXT NOT NULL,
|
|
75
|
+
startedAt TEXT NOT NULL,
|
|
76
|
+
endedAt TEXT,
|
|
77
|
+
elapsedMs INTEGER NOT NULL DEFAULT 0,
|
|
78
|
+
inputTokens INTEGER,
|
|
79
|
+
outputTokens INTEGER,
|
|
80
|
+
totalTokens INTEGER,
|
|
81
|
+
costUsd REAL,
|
|
82
|
+
costSource TEXT NOT NULL DEFAULT 'none',
|
|
83
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
84
|
+
lastActivityAt TEXT NOT NULL,
|
|
85
|
+
createdAt TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
86
|
+
updatedAt TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_taskId ON sessions(taskId);
|
|
90
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_sessionType ON sessions(sessionType);
|
|
91
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_agent ON sessions(agent);
|
|
92
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_model ON sessions(model);
|
|
93
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_startedAt ON sessions(startedAt);
|
|
94
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status);
|
|
95
|
+
|
|
96
|
+
CREATE TABLE IF NOT EXISTS schema_version (
|
|
97
|
+
version INTEGER PRIMARY KEY,
|
|
98
|
+
appliedAt TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
99
|
+
);
|
|
100
|
+
`,
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
version: 2,
|
|
104
|
+
up: `
|
|
105
|
+
-- Allowed users with roles
|
|
106
|
+
CREATE TABLE IF NOT EXISTS auth_users (
|
|
107
|
+
email TEXT PRIMARY KEY,
|
|
108
|
+
role TEXT NOT NULL DEFAULT 'member',
|
|
109
|
+
display_name TEXT,
|
|
110
|
+
auth_source TEXT NOT NULL DEFAULT 'otp',
|
|
111
|
+
added_by TEXT,
|
|
112
|
+
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
113
|
+
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
-- Server-side sessions (hashed tokens)
|
|
117
|
+
CREATE TABLE IF NOT EXISTS auth_sessions (
|
|
118
|
+
session_id TEXT PRIMARY KEY,
|
|
119
|
+
token_hash TEXT NOT NULL UNIQUE,
|
|
120
|
+
email TEXT NOT NULL,
|
|
121
|
+
role TEXT NOT NULL,
|
|
122
|
+
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
123
|
+
expires_at TEXT NOT NULL,
|
|
124
|
+
revoked_at TEXT
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
CREATE INDEX IF NOT EXISTS idx_auth_sessions_token_hash ON auth_sessions(token_hash);
|
|
128
|
+
CREATE INDEX IF NOT EXISTS idx_auth_sessions_email ON auth_sessions(email);
|
|
129
|
+
|
|
130
|
+
-- OTP challenges (hashed codes)
|
|
131
|
+
CREATE TABLE IF NOT EXISTS auth_otp (
|
|
132
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
133
|
+
email TEXT NOT NULL,
|
|
134
|
+
code_hash TEXT NOT NULL,
|
|
135
|
+
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
136
|
+
expires_at TEXT NOT NULL,
|
|
137
|
+
used_at TEXT,
|
|
138
|
+
source_ip TEXT
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
CREATE INDEX IF NOT EXISTS idx_auth_otp_email ON auth_otp(email);
|
|
142
|
+
|
|
143
|
+
-- Audit log for membership/role changes
|
|
144
|
+
CREATE TABLE IF NOT EXISTS auth_audit_log (
|
|
145
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
146
|
+
action TEXT NOT NULL,
|
|
147
|
+
target_email TEXT,
|
|
148
|
+
actor_email TEXT,
|
|
149
|
+
details TEXT,
|
|
150
|
+
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
151
|
+
);
|
|
152
|
+
`,
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
// Prompt-cache token visibility (Phase 0). Nullable — left NULL for any
|
|
156
|
+
// session whose CLI reported no cache figures, and for every row that
|
|
157
|
+
// predates this migration.
|
|
158
|
+
version: 3,
|
|
159
|
+
up: `
|
|
160
|
+
ALTER TABLE sessions ADD COLUMN cacheReadTokens INTEGER;
|
|
161
|
+
ALTER TABLE sessions ADD COLUMN cacheCreationTokens INTEGER;
|
|
162
|
+
`,
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
// Agent-turn (model round-trip) count per session. Nullable — NULL for a
|
|
166
|
+
// CLI that reports nothing countable, and for every pre-migration row.
|
|
167
|
+
version: 4,
|
|
168
|
+
up: `ALTER TABLE sessions ADD COLUMN turns INTEGER;`,
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
// Raw per-event usage capture. One row per usage-bearing CLI event
|
|
172
|
+
// (opencode `step_finish`, claude `result`/`assistant`, codex `usage`),
|
|
173
|
+
// recorded BEFORE the lossy fold into `sessions`. `sessions` is a derived
|
|
174
|
+
// rollup; this table is the ground truth you can rebuild it from and diff
|
|
175
|
+
// extraction bugs against. Append-only, best-effort.
|
|
176
|
+
version: 5,
|
|
177
|
+
up: `
|
|
178
|
+
CREATE TABLE IF NOT EXISTS session_usage_events (
|
|
179
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
180
|
+
sessionId TEXT NOT NULL,
|
|
181
|
+
taskId TEXT,
|
|
182
|
+
agent TEXT,
|
|
183
|
+
model TEXT,
|
|
184
|
+
codingAgent TEXT,
|
|
185
|
+
eventType TEXT,
|
|
186
|
+
ts TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
187
|
+
inputTokens INTEGER,
|
|
188
|
+
outputTokens INTEGER,
|
|
189
|
+
totalTokens INTEGER,
|
|
190
|
+
cacheReadTokens INTEGER,
|
|
191
|
+
cacheCreationTokens INTEGER,
|
|
192
|
+
costUsd REAL,
|
|
193
|
+
turns INTEGER,
|
|
194
|
+
raw TEXT NOT NULL,
|
|
195
|
+
-- sessionId + hash(raw): the durable agent log is replayed verbatim on
|
|
196
|
+
-- server restart/adopt, so the same event line can arrive twice. A
|
|
197
|
+
-- UNIQUE key + INSERT OR IGNORE keeps this table idempotent under replay.
|
|
198
|
+
dedupeKey TEXT NOT NULL DEFAULT ''
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
CREATE INDEX IF NOT EXISTS idx_usage_events_sessionId ON session_usage_events(sessionId);
|
|
202
|
+
CREATE INDEX IF NOT EXISTS idx_usage_events_taskId ON session_usage_events(taskId);
|
|
203
|
+
CREATE INDEX IF NOT EXISTS idx_usage_events_ts ON session_usage_events(ts);
|
|
204
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_usage_events_dedupe ON session_usage_events(dedupeKey);
|
|
205
|
+
`,
|
|
206
|
+
},
|
|
207
|
+
];
|
|
208
|
+
/** Singleton database instance. */
|
|
209
|
+
let dbInstance = null;
|
|
210
|
+
/** Trailing-window duration in ms for each finite usage range. */
|
|
211
|
+
const USAGE_RANGE_MS = {
|
|
212
|
+
"1d": 24 * 60 * 60 * 1000,
|
|
213
|
+
"7d": 7 * 24 * 60 * 60 * 1000,
|
|
214
|
+
"30d": 30 * 24 * 60 * 60 * 1000,
|
|
215
|
+
};
|
|
216
|
+
/** Database wrapper providing high-level operations. */
|
|
217
|
+
export class RepoOSDb {
|
|
218
|
+
db;
|
|
219
|
+
available;
|
|
220
|
+
/**
|
|
221
|
+
* Open or create the database. If SQLite is not available, sets available=false
|
|
222
|
+
* and all operations become no-ops (fail-soft).
|
|
223
|
+
*/
|
|
224
|
+
constructor(repoRoot) {
|
|
225
|
+
this.available = false;
|
|
226
|
+
loadSqlite();
|
|
227
|
+
if (!dbAvailable || !Database)
|
|
228
|
+
return;
|
|
229
|
+
try {
|
|
230
|
+
const cacheDir = join(repoRoot, ".repoos");
|
|
231
|
+
if (!existsSync(cacheDir)) {
|
|
232
|
+
mkdirSync(cacheDir, { recursive: true });
|
|
233
|
+
}
|
|
234
|
+
const dbPath = join(cacheDir, "repoos.db");
|
|
235
|
+
this.db = new Database(dbPath);
|
|
236
|
+
// Enable WAL mode for better concurrency
|
|
237
|
+
this.db.exec("PRAGMA journal_mode=WAL");
|
|
238
|
+
this.db.exec("PRAGMA synchronous=NORMAL");
|
|
239
|
+
// Only consider the store available once the schema is actually in place;
|
|
240
|
+
// a failed migration must degrade gracefully (available=false) rather than
|
|
241
|
+
// leave a half-initialized DB that silently no-ops every read (0230).
|
|
242
|
+
this.available = this.runMigrations();
|
|
243
|
+
}
|
|
244
|
+
catch {
|
|
245
|
+
// Initialization failed — graceful degradation
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
/** Apply pending migrations. Returns true only if the schema is in place. */
|
|
249
|
+
runMigrations() {
|
|
250
|
+
if (!this.db)
|
|
251
|
+
return false;
|
|
252
|
+
try {
|
|
253
|
+
let currentVersion = 0;
|
|
254
|
+
try {
|
|
255
|
+
const result = this.db
|
|
256
|
+
.prepare("SELECT version FROM schema_version ORDER BY version DESC LIMIT 1")
|
|
257
|
+
.all();
|
|
258
|
+
currentVersion = result.length > 0 ? result[0].version : 0;
|
|
259
|
+
}
|
|
260
|
+
catch {
|
|
261
|
+
// No schema_version yet — run all migrations below.
|
|
262
|
+
currentVersion = 0;
|
|
263
|
+
}
|
|
264
|
+
for (const migration of MIGRATIONS) {
|
|
265
|
+
if (migration.version > currentVersion) {
|
|
266
|
+
this.db.exec(migration.up);
|
|
267
|
+
this.db.exec(`INSERT INTO schema_version (version) VALUES (${migration.version})`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return true;
|
|
271
|
+
}
|
|
272
|
+
catch {
|
|
273
|
+
// A migration failed — the schema is incomplete, so report unavailable.
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
isAvailable() {
|
|
278
|
+
return this.available;
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Create or update a session record. The sessionId is unique; multiple turns
|
|
282
|
+
* update the same record (idempotent).
|
|
283
|
+
*/
|
|
284
|
+
upsertSession(session) {
|
|
285
|
+
if (!this.available || !this.db)
|
|
286
|
+
return;
|
|
287
|
+
try {
|
|
288
|
+
const stmt = this.db.prepare(`
|
|
289
|
+
INSERT INTO sessions (
|
|
290
|
+
sessionId, sessionType, taskId, agent, model, codingAgent,
|
|
291
|
+
startedAt, endedAt, elapsedMs, inputTokens, outputTokens, totalTokens,
|
|
292
|
+
cacheReadTokens, cacheCreationTokens, turns,
|
|
293
|
+
costUsd, costSource, status, lastActivityAt, updatedAt
|
|
294
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))
|
|
295
|
+
ON CONFLICT(sessionId) DO UPDATE SET
|
|
296
|
+
-- A per-task cli/model override (or an agent switch mid-session) is
|
|
297
|
+
-- resolved by the time the agent actually runs, so a later upsert
|
|
298
|
+
-- carries the accurate CLI/model — the first insert may have used the
|
|
299
|
+
-- base config agent. Keep an existing real value only when the new
|
|
300
|
+
-- one is a bare fallback ("unknown"/"default"/"").
|
|
301
|
+
agent = CASE WHEN excluded.agent IN ('', 'unknown') THEN agent ELSE excluded.agent END,
|
|
302
|
+
model = CASE WHEN excluded.model IN ('', 'default') THEN model ELSE excluded.model END,
|
|
303
|
+
codingAgent = CASE WHEN excluded.codingAgent IN ('', 'unknown') THEN codingAgent ELSE excluded.codingAgent END,
|
|
304
|
+
endedAt = excluded.endedAt,
|
|
305
|
+
elapsedMs = excluded.elapsedMs,
|
|
306
|
+
inputTokens = COALESCE(excluded.inputTokens, inputTokens),
|
|
307
|
+
outputTokens = COALESCE(excluded.outputTokens, outputTokens),
|
|
308
|
+
totalTokens = COALESCE(excluded.totalTokens, totalTokens),
|
|
309
|
+
cacheReadTokens = COALESCE(excluded.cacheReadTokens, cacheReadTokens),
|
|
310
|
+
cacheCreationTokens = COALESCE(excluded.cacheCreationTokens, cacheCreationTokens),
|
|
311
|
+
turns = COALESCE(excluded.turns, turns),
|
|
312
|
+
costUsd = COALESCE(excluded.costUsd, costUsd),
|
|
313
|
+
costSource = excluded.costSource,
|
|
314
|
+
status = excluded.status,
|
|
315
|
+
lastActivityAt = excluded.lastActivityAt,
|
|
316
|
+
updatedAt = datetime('now')
|
|
317
|
+
`);
|
|
318
|
+
stmt.run(session.sessionId, session.sessionType, session.taskId ?? null, session.agent, session.model, session.codingAgent, session.startedAt, session.endedAt ?? null, session.elapsedMs, session.inputTokens ?? null, session.outputTokens ?? null, session.totalTokens ?? null, session.cacheReadTokens ?? null, session.cacheCreationTokens ?? null, session.turns ?? null, session.costUsd ?? null, session.costSource ?? "none", session.status ?? "active", session.lastActivityAt);
|
|
319
|
+
}
|
|
320
|
+
catch {
|
|
321
|
+
// Operation failed — continue gracefully
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Append one raw usage-bearing CLI event, exactly as reported, before it is
|
|
326
|
+
* folded into the `sessions` rollup. Best-effort and append-only: a failure
|
|
327
|
+
* here never disturbs the session record or the request.
|
|
328
|
+
*/
|
|
329
|
+
recordUsageEvent(ev) {
|
|
330
|
+
if (!this.available || !this.db)
|
|
331
|
+
return;
|
|
332
|
+
try {
|
|
333
|
+
const raw = ev.raw.length > 20_000 ? `${ev.raw.slice(0, 20_000)}…[truncated]` : ev.raw;
|
|
334
|
+
// djb2 hash — small, dependency-free, only needs to be stable, not secure.
|
|
335
|
+
let h = 5381;
|
|
336
|
+
for (let i = 0; i < ev.raw.length; i++)
|
|
337
|
+
h = ((h << 5) + h + ev.raw.charCodeAt(i)) | 0;
|
|
338
|
+
const dedupeKey = `${ev.sessionId}:${h >>> 0}`;
|
|
339
|
+
const stmt = this.db.prepare(`
|
|
340
|
+
INSERT OR IGNORE INTO session_usage_events (
|
|
341
|
+
sessionId, taskId, agent, model, codingAgent, eventType,
|
|
342
|
+
inputTokens, outputTokens, totalTokens,
|
|
343
|
+
cacheReadTokens, cacheCreationTokens, costUsd, turns, raw, dedupeKey
|
|
344
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
345
|
+
`);
|
|
346
|
+
stmt.run(ev.sessionId, ev.taskId ?? null, ev.agent ?? null, ev.model ?? null, ev.codingAgent ?? null, ev.eventType ?? null, ev.inputTokens ?? null, ev.outputTokens ?? null, ev.totalTokens ?? null, ev.cacheReadTokens ?? null, ev.cacheCreationTokens ?? null, ev.costUsd ?? null, ev.turns ?? null, raw, dedupeKey);
|
|
347
|
+
}
|
|
348
|
+
catch {
|
|
349
|
+
// Append is best-effort — continue gracefully.
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
/** Retrieve a session record by ID. */
|
|
353
|
+
getSession(sessionId) {
|
|
354
|
+
if (!this.available || !this.db)
|
|
355
|
+
return null;
|
|
356
|
+
try {
|
|
357
|
+
const result = this.db.prepare("SELECT * FROM sessions WHERE sessionId = ?").all(sessionId);
|
|
358
|
+
return result.length > 0 ? result[0] : null;
|
|
359
|
+
}
|
|
360
|
+
catch {
|
|
361
|
+
return null;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
/** Retrieve all sessions for a task. */
|
|
365
|
+
getTaskSessions(taskId) {
|
|
366
|
+
if (!this.available || !this.db)
|
|
367
|
+
return [];
|
|
368
|
+
try {
|
|
369
|
+
return this.db
|
|
370
|
+
.prepare("SELECT * FROM sessions WHERE taskId = ? ORDER BY startedAt DESC")
|
|
371
|
+
.all(taskId);
|
|
372
|
+
}
|
|
373
|
+
catch {
|
|
374
|
+
return [];
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
/** Aggregate stats for a task (all sessions and review rounds). */
|
|
378
|
+
getTaskStats(taskId) {
|
|
379
|
+
if (!this.available || !this.db)
|
|
380
|
+
return null;
|
|
381
|
+
try {
|
|
382
|
+
const rows = this.getTaskSessions(taskId);
|
|
383
|
+
if (rows.length === 0)
|
|
384
|
+
return null;
|
|
385
|
+
const agg = this.aggregateRows(rows);
|
|
386
|
+
return {
|
|
387
|
+
taskId,
|
|
388
|
+
totalSessions: agg.totalSessions,
|
|
389
|
+
totalElapsedMs: agg.totalElapsedMs,
|
|
390
|
+
totalInputTokens: agg.totalInputTokens,
|
|
391
|
+
totalOutputTokens: agg.totalOutputTokens,
|
|
392
|
+
totalTokens: agg.totalTokens,
|
|
393
|
+
totalCacheReadTokens: agg.totalCacheReadTokens,
|
|
394
|
+
totalCacheCreationTokens: agg.totalCacheCreationTokens,
|
|
395
|
+
totalTurns: agg.totalTurns,
|
|
396
|
+
totalCostUsd: agg.totalCostUsd,
|
|
397
|
+
costSource: agg.costSource,
|
|
398
|
+
roles: this.groupRows(rows, (r) => r.sessionType)
|
|
399
|
+
.map(([role, group]) => this.toRoleStats(role, group))
|
|
400
|
+
.sort((a, b) => b.totalElapsedMs - a.totalElapsedMs),
|
|
401
|
+
sessions: rows,
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
catch {
|
|
405
|
+
return null;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Cutoff ISO timestamp for a usage range's trailing window, or null for
|
|
410
|
+
* "all". `startedAt` is NOT NULL ISO text, so a lexicographic `>= ?`
|
|
411
|
+
* comparison against an ISO cutoff is correct — and `idx_sessions_startedAt`
|
|
412
|
+
* covers it.
|
|
413
|
+
*/
|
|
414
|
+
usageRangeSince(range) {
|
|
415
|
+
if (range === "all")
|
|
416
|
+
return null;
|
|
417
|
+
return new Date(Date.now() - USAGE_RANGE_MS[range]).toISOString();
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Aggregate totals broken down by day, using the server's LOCAL time zone
|
|
421
|
+
* (the day a session's `endedAt` falls into on this machine). Done in JS so
|
|
422
|
+
* the grouping genuinely reflects local wall-clock days rather than SQLite's
|
|
423
|
+
* UTC `date()`.
|
|
424
|
+
*
|
|
425
|
+
* The optional range filters on `startedAt` (0334) — the same predicate the
|
|
426
|
+
* headline totals use — so the per-day table always agrees with the board
|
|
427
|
+
* summary for the selected window.
|
|
428
|
+
*/
|
|
429
|
+
getDailyTotals(range = "all") {
|
|
430
|
+
if (!this.available || !this.db)
|
|
431
|
+
return [];
|
|
432
|
+
try {
|
|
433
|
+
const since = this.usageRangeSince(range);
|
|
434
|
+
const rows = (since
|
|
435
|
+
? this.db
|
|
436
|
+
.prepare("SELECT * FROM sessions WHERE endedAt IS NOT NULL AND startedAt >= ?")
|
|
437
|
+
.all(since)
|
|
438
|
+
: this.db.prepare("SELECT * FROM sessions WHERE endedAt IS NOT NULL").all());
|
|
439
|
+
const byDay = new Map();
|
|
440
|
+
for (const r of rows) {
|
|
441
|
+
const d = new Date(r.endedAt);
|
|
442
|
+
if (Number.isNaN(d.getTime()))
|
|
443
|
+
continue;
|
|
444
|
+
const day = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
445
|
+
const list = byDay.get(day);
|
|
446
|
+
if (list)
|
|
447
|
+
list.push(r);
|
|
448
|
+
else
|
|
449
|
+
byDay.set(day, [r]);
|
|
450
|
+
}
|
|
451
|
+
return [...byDay.entries()]
|
|
452
|
+
.map(([day, group]) => {
|
|
453
|
+
const agg = this.aggregateRows(group);
|
|
454
|
+
return {
|
|
455
|
+
day,
|
|
456
|
+
totalSessions: agg.totalSessions,
|
|
457
|
+
totalElapsedMs: agg.totalElapsedMs,
|
|
458
|
+
totalInputTokens: agg.totalInputTokens,
|
|
459
|
+
totalOutputTokens: agg.totalOutputTokens,
|
|
460
|
+
totalTokens: agg.totalTokens,
|
|
461
|
+
totalCostUsd: agg.totalCostUsd,
|
|
462
|
+
costSource: agg.costSource,
|
|
463
|
+
};
|
|
464
|
+
})
|
|
465
|
+
.sort((a, b) => (a.day < b.day ? 1 : -1));
|
|
466
|
+
}
|
|
467
|
+
catch {
|
|
468
|
+
return [];
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
/** Aggregate stats grouped by session type, optionally scoped to a usage range (0334). */
|
|
472
|
+
getSessionTypeStats(range = "all") {
|
|
473
|
+
if (!this.available || !this.db)
|
|
474
|
+
return [];
|
|
475
|
+
try {
|
|
476
|
+
const since = this.usageRangeSince(range);
|
|
477
|
+
const rows = (since
|
|
478
|
+
? this.db.prepare("SELECT * FROM sessions WHERE startedAt >= ?").all(since)
|
|
479
|
+
: this.db.prepare("SELECT * FROM sessions").all());
|
|
480
|
+
return this.groupRows(rows, (r) => r.sessionType)
|
|
481
|
+
.map(([sessionType, group]) => {
|
|
482
|
+
const agg = this.aggregateRows(group);
|
|
483
|
+
return {
|
|
484
|
+
sessionType,
|
|
485
|
+
role: sessionType,
|
|
486
|
+
totalSessions: agg.totalSessions,
|
|
487
|
+
totalElapsedMs: agg.totalElapsedMs,
|
|
488
|
+
totalInputTokens: agg.totalInputTokens,
|
|
489
|
+
totalOutputTokens: agg.totalOutputTokens,
|
|
490
|
+
totalTokens: agg.totalTokens,
|
|
491
|
+
totalCacheReadTokens: agg.totalCacheReadTokens,
|
|
492
|
+
totalCacheCreationTokens: agg.totalCacheCreationTokens,
|
|
493
|
+
totalTurns: agg.totalTurns,
|
|
494
|
+
totalCostUsd: agg.totalCostUsd,
|
|
495
|
+
costSource: agg.costSource,
|
|
496
|
+
};
|
|
497
|
+
})
|
|
498
|
+
.sort((a, b) => (b.totalCostUsd ?? 0) - (a.totalCostUsd ?? 0));
|
|
499
|
+
}
|
|
500
|
+
catch {
|
|
501
|
+
return [];
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
/** Group sessions by a key, preserving insertion order of keys. */
|
|
505
|
+
groupRows(rows, key) {
|
|
506
|
+
const map = new Map();
|
|
507
|
+
for (const r of rows) {
|
|
508
|
+
const k = key(r);
|
|
509
|
+
const list = map.get(k);
|
|
510
|
+
if (list)
|
|
511
|
+
list.push(r);
|
|
512
|
+
else
|
|
513
|
+
map.set(k, [r]);
|
|
514
|
+
}
|
|
515
|
+
return [...map.entries()];
|
|
516
|
+
}
|
|
517
|
+
/** Build a single role's totals from a group of sessions. */
|
|
518
|
+
toRoleStats(role, rows) {
|
|
519
|
+
const agg = this.aggregateRows(rows);
|
|
520
|
+
return {
|
|
521
|
+
role,
|
|
522
|
+
totalSessions: agg.totalSessions,
|
|
523
|
+
totalElapsedMs: agg.totalElapsedMs,
|
|
524
|
+
totalInputTokens: agg.totalInputTokens,
|
|
525
|
+
totalOutputTokens: agg.totalOutputTokens,
|
|
526
|
+
totalTokens: agg.totalTokens,
|
|
527
|
+
totalCacheReadTokens: agg.totalCacheReadTokens,
|
|
528
|
+
totalCacheCreationTokens: agg.totalCacheCreationTokens,
|
|
529
|
+
totalTurns: agg.totalTurns,
|
|
530
|
+
totalCostUsd: agg.totalCostUsd,
|
|
531
|
+
costSource: agg.costSource,
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Sum usage across a group of sessions. costSource is classified as "mixed"
|
|
536
|
+
* when the group's spend comes from more than one source, otherwise the
|
|
537
|
+
* single non-"none" source (or "none"). This drives honest labeling in the UI
|
|
538
|
+
* (estimates and Kiro credits are never silently shown as firm USD, 0230).
|
|
539
|
+
*/
|
|
540
|
+
aggregateRows(rows) {
|
|
541
|
+
let totalSessions = 0;
|
|
542
|
+
let totalElapsedMs = 0;
|
|
543
|
+
let totalInputTokens = null;
|
|
544
|
+
let totalOutputTokens = null;
|
|
545
|
+
let totalTokens = null;
|
|
546
|
+
let totalCacheReadTokens = null;
|
|
547
|
+
let totalCacheCreationTokens = null;
|
|
548
|
+
let totalTurns = null;
|
|
549
|
+
let totalCostUsd = null;
|
|
550
|
+
const sources = new Set();
|
|
551
|
+
for (const r of rows) {
|
|
552
|
+
totalSessions += 1;
|
|
553
|
+
totalElapsedMs += r.elapsedMs || 0;
|
|
554
|
+
if (r.inputTokens != null)
|
|
555
|
+
totalInputTokens = (totalInputTokens ?? 0) + r.inputTokens;
|
|
556
|
+
if (r.outputTokens != null)
|
|
557
|
+
totalOutputTokens = (totalOutputTokens ?? 0) + r.outputTokens;
|
|
558
|
+
if (r.totalTokens != null)
|
|
559
|
+
totalTokens = (totalTokens ?? 0) + r.totalTokens;
|
|
560
|
+
if (r.cacheReadTokens != null)
|
|
561
|
+
totalCacheReadTokens = (totalCacheReadTokens ?? 0) + r.cacheReadTokens;
|
|
562
|
+
if (r.cacheCreationTokens != null)
|
|
563
|
+
totalCacheCreationTokens = (totalCacheCreationTokens ?? 0) + r.cacheCreationTokens;
|
|
564
|
+
if (r.turns != null)
|
|
565
|
+
totalTurns = (totalTurns ?? 0) + r.turns;
|
|
566
|
+
if (r.costUsd != null) {
|
|
567
|
+
totalCostUsd = (totalCostUsd ?? 0) + r.costUsd;
|
|
568
|
+
sources.add(r.costSource || "extractUsage");
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
let costSource = "none";
|
|
572
|
+
if (sources.size > 1)
|
|
573
|
+
costSource = "mixed";
|
|
574
|
+
else if (sources.size === 1)
|
|
575
|
+
costSource = [...sources][0];
|
|
576
|
+
return {
|
|
577
|
+
totalSessions,
|
|
578
|
+
totalElapsedMs,
|
|
579
|
+
totalInputTokens,
|
|
580
|
+
totalOutputTokens,
|
|
581
|
+
totalTokens,
|
|
582
|
+
totalCacheReadTokens,
|
|
583
|
+
totalCacheCreationTokens,
|
|
584
|
+
totalTurns,
|
|
585
|
+
totalCostUsd,
|
|
586
|
+
costSource,
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
/**
|
|
590
|
+
* Board-level summary: total spend, tokens, time, most expensive session/task.
|
|
591
|
+
* The optional range (0334) scopes every figure — headline totals, cost
|
|
592
|
+
* source, most-expensive lookups, roles, days — to sessions started within
|
|
593
|
+
* the trailing window, so all parts of the panel agree; "all" (the default)
|
|
594
|
+
* is the unfiltered pre-range behavior.
|
|
595
|
+
*/
|
|
596
|
+
getBoardStats(range = "all") {
|
|
597
|
+
if (!this.available || !this.db) {
|
|
598
|
+
return {
|
|
599
|
+
totalSessions: 0,
|
|
600
|
+
totalElapsedMs: 0,
|
|
601
|
+
totalTokens: null,
|
|
602
|
+
totalCostUsd: null,
|
|
603
|
+
costSource: "none",
|
|
604
|
+
mostExpensiveSession: null,
|
|
605
|
+
mostExpensiveTask: null,
|
|
606
|
+
roles: [],
|
|
607
|
+
days: [],
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
try {
|
|
611
|
+
const since = this.usageRangeSince(range);
|
|
612
|
+
const startedFilter = since ? "WHERE startedAt >= ?" : "";
|
|
613
|
+
const startedFilterAnd = since ? "AND startedAt >= ?" : "";
|
|
614
|
+
const startedArgs = since ? [since] : [];
|
|
615
|
+
const summary = this.db
|
|
616
|
+
.prepare(`
|
|
617
|
+
SELECT
|
|
618
|
+
COUNT(*) as totalSessions,
|
|
619
|
+
COALESCE(SUM(elapsedMs), 0) as totalElapsedMs,
|
|
620
|
+
SUM(CASE WHEN totalTokens IS NOT NULL THEN totalTokens ELSE 0 END) as totalTokens,
|
|
621
|
+
SUM(CASE WHEN costUsd IS NOT NULL THEN costUsd ELSE 0 END) as totalCostUsd
|
|
622
|
+
FROM sessions
|
|
623
|
+
${startedFilter}
|
|
624
|
+
`)
|
|
625
|
+
.all(...startedArgs)[0];
|
|
626
|
+
const mostExpensive = this.db
|
|
627
|
+
.prepare(`
|
|
628
|
+
SELECT * FROM sessions
|
|
629
|
+
WHERE costUsd IS NOT NULL
|
|
630
|
+
${startedFilterAnd}
|
|
631
|
+
ORDER BY costUsd DESC
|
|
632
|
+
LIMIT 1
|
|
633
|
+
`)
|
|
634
|
+
.all(...startedArgs);
|
|
635
|
+
const mostExpensiveSession = mostExpensive.length > 0 ? mostExpensive[0] : null;
|
|
636
|
+
const mostExpensiveTaskResult = this.db
|
|
637
|
+
.prepare(`
|
|
638
|
+
SELECT
|
|
639
|
+
taskId,
|
|
640
|
+
SUM(CASE WHEN costUsd IS NOT NULL THEN costUsd ELSE 0 END) as costUsd
|
|
641
|
+
FROM sessions
|
|
642
|
+
WHERE taskId IS NOT NULL
|
|
643
|
+
${startedFilterAnd}
|
|
644
|
+
GROUP BY taskId
|
|
645
|
+
ORDER BY costUsd DESC
|
|
646
|
+
LIMIT 1
|
|
647
|
+
`)
|
|
648
|
+
.all(...startedArgs);
|
|
649
|
+
const mostExpensiveTask = mostExpensiveTaskResult.length > 0
|
|
650
|
+
? {
|
|
651
|
+
taskId: mostExpensiveTaskResult[0].taskId,
|
|
652
|
+
costUsd: mostExpensiveTaskResult[0].costUsd,
|
|
653
|
+
}
|
|
654
|
+
: null;
|
|
655
|
+
// Representative cost source across the (range-scoped) sessions — mirrors
|
|
656
|
+
// the per-role / per-day classification in aggregateRows so estimates and
|
|
657
|
+
// Kiro credits are never silently shown as firm USD at the board level
|
|
658
|
+
// (0230).
|
|
659
|
+
const sourceRows = this.db
|
|
660
|
+
.prepare(`SELECT DISTINCT COALESCE(costSource, 'extractUsage') as costSource FROM sessions WHERE costUsd IS NOT NULL ${startedFilterAnd}`)
|
|
661
|
+
.all(...startedArgs);
|
|
662
|
+
const costSource = sourceRows.length > 1
|
|
663
|
+
? "mixed"
|
|
664
|
+
: sourceRows.length === 1
|
|
665
|
+
? sourceRows[0].costSource
|
|
666
|
+
: "none";
|
|
667
|
+
return {
|
|
668
|
+
totalSessions: summary.totalSessions || 0,
|
|
669
|
+
totalElapsedMs: summary.totalElapsedMs || 0,
|
|
670
|
+
totalTokens: summary.totalTokens || null,
|
|
671
|
+
totalCostUsd: summary.totalCostUsd || null,
|
|
672
|
+
costSource,
|
|
673
|
+
mostExpensiveSession,
|
|
674
|
+
mostExpensiveTask,
|
|
675
|
+
roles: this.getSessionTypeStats(range),
|
|
676
|
+
days: this.getDailyTotals(range),
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
catch {
|
|
680
|
+
return {
|
|
681
|
+
totalSessions: 0,
|
|
682
|
+
totalElapsedMs: 0,
|
|
683
|
+
totalTokens: null,
|
|
684
|
+
totalCostUsd: null,
|
|
685
|
+
costSource: "none",
|
|
686
|
+
mostExpensiveSession: null,
|
|
687
|
+
mostExpensiveTask: null,
|
|
688
|
+
roles: [],
|
|
689
|
+
days: [],
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
/** Close the database connection. */
|
|
694
|
+
close() {
|
|
695
|
+
if (this.db) {
|
|
696
|
+
try {
|
|
697
|
+
this.db.close();
|
|
698
|
+
}
|
|
699
|
+
catch {
|
|
700
|
+
// Already closed or error during close
|
|
701
|
+
}
|
|
702
|
+
this.db = null;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
/**
|
|
707
|
+
* Get or create the singleton database instance for a repo root.
|
|
708
|
+
* Returns null if SQLite is not available (graceful degradation).
|
|
709
|
+
*/
|
|
710
|
+
export function getRepoOSDb(repoRoot) {
|
|
711
|
+
if (!dbInstance) {
|
|
712
|
+
dbInstance = new RepoOSDb(repoRoot);
|
|
713
|
+
}
|
|
714
|
+
return dbInstance.isAvailable() ? dbInstance : null;
|
|
715
|
+
}
|
|
716
|
+
/** Reset the singleton (for tests). */
|
|
717
|
+
export function resetDbInstance() {
|
|
718
|
+
if (dbInstance) {
|
|
719
|
+
dbInstance.close();
|
|
720
|
+
}
|
|
721
|
+
dbInstance = null;
|
|
722
|
+
}
|
|
723
|
+
/** Convenience function to get task stats from the singleton database. */
|
|
724
|
+
export function getTaskStats(repoRoot, taskId) {
|
|
725
|
+
const db = getRepoOSDb(repoRoot);
|
|
726
|
+
return db?.getTaskStats(taskId) ?? null;
|
|
727
|
+
}
|
|
728
|
+
/** Convenience function to get session type stats from the singleton database. */
|
|
729
|
+
export function getSessionTypeStats(repoRoot) {
|
|
730
|
+
const db = getRepoOSDb(repoRoot);
|
|
731
|
+
return db?.getSessionTypeStats() ?? [];
|
|
732
|
+
}
|
|
733
|
+
/** Convenience function to get board stats from the singleton database. */
|
|
734
|
+
export function getBoardStats(repoRoot) {
|
|
735
|
+
const db = getRepoOSDb(repoRoot);
|
|
736
|
+
return (db?.getBoardStats() ?? {
|
|
737
|
+
totalSessions: 0,
|
|
738
|
+
totalElapsedMs: 0,
|
|
739
|
+
totalTokens: null,
|
|
740
|
+
totalCostUsd: null,
|
|
741
|
+
costSource: "none",
|
|
742
|
+
mostExpensiveSession: null,
|
|
743
|
+
mostExpensiveTask: null,
|
|
744
|
+
roles: [],
|
|
745
|
+
days: [],
|
|
746
|
+
});
|
|
747
|
+
}
|