toolroll 0.5.0
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 +21 -0
- package/README.md +1113 -0
- package/THIRD_PARTY_NOTICES.md +449 -0
- package/dist/accent-colors.d.ts +48 -0
- package/dist/accent-colors.js +122 -0
- package/dist/action-ledger.d.ts +26 -0
- package/dist/action-ledger.js +75 -0
- package/dist/agent-fence.d.ts +42 -0
- package/dist/agent-fence.js +183 -0
- package/dist/agentconfig.d.ts +216 -0
- package/dist/agentconfig.js +454 -0
- package/dist/api-tokens.d.ts +16 -0
- package/dist/api-tokens.js +27 -0
- package/dist/approval-policy.d.ts +69 -0
- package/dist/approval-policy.js +137 -0
- package/dist/approval-rules-ui.d.ts +25 -0
- package/dist/approval-rules-ui.js +39 -0
- package/dist/assignment-adapters.d.ts +180 -0
- package/dist/assignment-adapters.js +239 -0
- package/dist/assignment-brief.d.ts +71 -0
- package/dist/assignment-brief.js +129 -0
- package/dist/assignment-delivery.d.ts +56 -0
- package/dist/assignment-delivery.js +160 -0
- package/dist/assignment-presentation.d.ts +26 -0
- package/dist/assignment-presentation.js +80 -0
- package/dist/assignment-status.d.ts +62 -0
- package/dist/assignment-status.js +152 -0
- package/dist/assignment-ui.d.ts +71 -0
- package/dist/assignment-ui.js +103 -0
- package/dist/assignment.d.ts +222 -0
- package/dist/assignment.js +399 -0
- package/dist/attest.d.ts +56 -0
- package/dist/attest.js +153 -0
- package/dist/backend.d.ts +97 -0
- package/dist/backend.js +166 -0
- package/dist/backup-ui.d.ts +22 -0
- package/dist/backup-ui.js +59 -0
- package/dist/backup.d.ts +84 -0
- package/dist/backup.js +421 -0
- package/dist/beads.d.ts +34 -0
- package/dist/beads.js +135 -0
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +22 -0
- package/dist/board.d.ts +150 -0
- package/dist/board.js +210 -0
- package/dist/boot-identity.d.ts +63 -0
- package/dist/boot-identity.js +99 -0
- package/dist/browser/THIRD_PARTY_NOTICES.txt +2295 -0
- package/dist/browser/workspace.css +4 -0
- package/dist/browser/workspace.js +225 -0
- package/dist/browser-crew.d.ts +11 -0
- package/dist/browser-crew.js +50 -0
- package/dist/browser-shell.d.ts +8 -0
- package/dist/browser-shell.js +60 -0
- package/dist/browser-workspace.d.ts +780 -0
- package/dist/browser-workspace.js +47 -0
- package/dist/budget-alerts.d.ts +19 -0
- package/dist/budget-alerts.js +46 -0
- package/dist/builder.d.ts +370 -0
- package/dist/builder.js +3262 -0
- package/dist/capscan.d.ts +35 -0
- package/dist/capscan.js +113 -0
- package/dist/chat-acceptance.d.ts +71 -0
- package/dist/chat-acceptance.js +120 -0
- package/dist/chat-actions.d.ts +265 -0
- package/dist/chat-actions.js +1273 -0
- package/dist/chat-channel.d.ts +109 -0
- package/dist/chat-channel.js +652 -0
- package/dist/chat-continuity.d.ts +32 -0
- package/dist/chat-continuity.js +317 -0
- package/dist/chat-controls.d.ts +113 -0
- package/dist/chat-controls.js +53 -0
- package/dist/chat-delivery-state.d.ts +159 -0
- package/dist/chat-delivery-state.js +309 -0
- package/dist/chat-delivery.d.ts +38 -0
- package/dist/chat-delivery.js +693 -0
- package/dist/chat-display.d.ts +1 -0
- package/dist/chat-display.js +18 -0
- package/dist/chat-evidence.d.ts +123 -0
- package/dist/chat-evidence.js +207 -0
- package/dist/chat-flow.d.ts +44 -0
- package/dist/chat-flow.js +215 -0
- package/dist/chat-inbox.d.ts +57 -0
- package/dist/chat-inbox.js +123 -0
- package/dist/chat-polish.d.ts +16 -0
- package/dist/chat-polish.js +135 -0
- package/dist/chat-review.d.ts +46 -0
- package/dist/chat-review.js +98 -0
- package/dist/chat-rooms.d.ts +123 -0
- package/dist/chat-rooms.js +216 -0
- package/dist/chat-task-actions.d.ts +44 -0
- package/dist/chat-task-actions.js +79 -0
- package/dist/check-progress.d.ts +51 -0
- package/dist/check-progress.js +290 -0
- package/dist/child-database.d.ts +12 -0
- package/dist/child-database.js +30 -0
- package/dist/claim.d.ts +688 -0
- package/dist/claim.js +1740 -0
- package/dist/cli.d.ts +137 -0
- package/dist/cli.js +1461 -0
- package/dist/codex-limits.d.ts +15 -0
- package/dist/codex-limits.js +145 -0
- package/dist/coding-context.d.ts +55 -0
- package/dist/coding-context.js +92 -0
- package/dist/coding-handoff.d.ts +57 -0
- package/dist/coding-handoff.js +277 -0
- package/dist/coding-provider.d.ts +72 -0
- package/dist/coding-provider.js +424 -0
- package/dist/coding-shipping-ui.d.ts +4 -0
- package/dist/coding-shipping-ui.js +12 -0
- package/dist/coding-types.d.ts +62 -0
- package/dist/coding-types.js +1 -0
- package/dist/coding-ui.d.ts +17 -0
- package/dist/coding-ui.js +364 -0
- package/dist/coding-update.d.ts +15 -0
- package/dist/coding-update.js +149 -0
- package/dist/coding-workspace.d.ts +124 -0
- package/dist/coding-workspace.js +768 -0
- package/dist/container-state.d.ts +5 -0
- package/dist/container-state.js +62 -0
- package/dist/containment.d.ts +197 -0
- package/dist/containment.js +559 -0
- package/dist/contest.d.ts +318 -0
- package/dist/contest.js +753 -0
- package/dist/control-setup.d.ts +35 -0
- package/dist/control-setup.js +40 -0
- package/dist/control-ui.d.ts +29 -0
- package/dist/control-ui.js +53 -0
- package/dist/controller-service.d.ts +1 -0
- package/dist/controller-service.js +19 -0
- package/dist/controller-supervisor.d.ts +30 -0
- package/dist/controller-supervisor.js +104 -0
- package/dist/converse.d.ts +304 -0
- package/dist/converse.js +849 -0
- package/dist/coordinator-proposals.d.ts +29 -0
- package/dist/coordinator-proposals.js +137 -0
- package/dist/coordinator.d.ts +186 -0
- package/dist/coordinator.js +447 -0
- package/dist/credentials-ui.d.ts +25 -0
- package/dist/credentials-ui.js +48 -0
- package/dist/daemon.d.ts +209 -0
- package/dist/daemon.js +604 -0
- package/dist/decision.d.ts +120 -0
- package/dist/decision.js +388 -0
- package/dist/demo.d.ts +55 -0
- package/dist/demo.js +992 -0
- package/dist/desktop-access.d.ts +28 -0
- package/dist/desktop-access.js +88 -0
- package/dist/desktop-bundle.d.ts +16 -0
- package/dist/desktop-bundle.js +74 -0
- package/dist/desktop-host.d.ts +54 -0
- package/dist/desktop-host.js +508 -0
- package/dist/desktop-update-gate.d.ts +12 -0
- package/dist/desktop-update-gate.js +91 -0
- package/dist/desktop-update-recovery.d.ts +14 -0
- package/dist/desktop-update-recovery.js +243 -0
- package/dist/desktop-update.d.ts +110 -0
- package/dist/desktop-update.js +740 -0
- package/dist/discord-api.d.ts +20 -0
- package/dist/discord-api.js +138 -0
- package/dist/discord-chat.d.ts +16 -0
- package/dist/discord-chat.js +381 -0
- package/dist/discord-settings.d.ts +7 -0
- package/dist/discord-settings.js +75 -0
- package/dist/discord.d.ts +10 -0
- package/dist/discord.js +169 -0
- package/dist/discover.d.ts +75 -0
- package/dist/discover.js +150 -0
- package/dist/dispatch.d.ts +100 -0
- package/dist/dispatch.js +311 -0
- package/dist/dispose.d.ts +161 -0
- package/dist/dispose.js +803 -0
- package/dist/email-settings.d.ts +34 -0
- package/dist/email-settings.js +50 -0
- package/dist/envelope.d.ts +47 -0
- package/dist/envelope.js +105 -0
- package/dist/evidence-pack.d.ts +185 -0
- package/dist/evidence-pack.js +258 -0
- package/dist/evidence.d.ts +345 -0
- package/dist/evidence.js +782 -0
- package/dist/exec.d.ts +285 -0
- package/dist/exec.js +1841 -0
- package/dist/exhaustion.d.ts +85 -0
- package/dist/exhaustion.js +141 -0
- package/dist/export-ui.d.ts +4 -0
- package/dist/export-ui.js +17 -0
- package/dist/export.d.ts +48 -0
- package/dist/export.js +305 -0
- package/dist/flow-actions.d.ts +77 -0
- package/dist/flow-actions.js +203 -0
- package/dist/flow-code.d.ts +50 -0
- package/dist/flow-code.js +159 -0
- package/dist/flow-draft.d.ts +38 -0
- package/dist/flow-draft.js +80 -0
- package/dist/flow-engine.d.ts +66 -0
- package/dist/flow-engine.js +416 -0
- package/dist/flow-insights.d.ts +95 -0
- package/dist/flow-insights.js +149 -0
- package/dist/flow-live.d.ts +24 -0
- package/dist/flow-live.js +150 -0
- package/dist/flow-people.d.ts +24 -0
- package/dist/flow-people.js +91 -0
- package/dist/flow-replies.d.ts +48 -0
- package/dist/flow-replies.js +113 -0
- package/dist/flow-scripts.d.ts +38 -0
- package/dist/flow-scripts.js +73 -0
- package/dist/flow-secrets.d.ts +13 -0
- package/dist/flow-secrets.js +45 -0
- package/dist/flow-sort.d.ts +82 -0
- package/dist/flow-sort.js +153 -0
- package/dist/flow-steps.d.ts +40 -0
- package/dist/flow-steps.js +408 -0
- package/dist/flow-triggers.d.ts +244 -0
- package/dist/flow-triggers.js +959 -0
- package/dist/flows-ui.d.ts +20 -0
- package/dist/flows-ui.js +175 -0
- package/dist/flows.d.ts +229 -0
- package/dist/flows.js +968 -0
- package/dist/fonts.d.ts +19 -0
- package/dist/fonts.js +19 -0
- package/dist/gaps.d.ts +35 -0
- package/dist/gaps.js +101 -0
- package/dist/gate-failure.d.ts +40 -0
- package/dist/gate-failure.js +66 -0
- package/dist/git.d.ts +54 -0
- package/dist/git.js +94 -0
- package/dist/google-mail.d.ts +58 -0
- package/dist/google-mail.js +151 -0
- package/dist/grant.d.ts +136 -0
- package/dist/grant.js +238 -0
- package/dist/graph.d.ts +164 -0
- package/dist/graph.js +383 -0
- package/dist/guides.d.ts +22 -0
- package/dist/guides.js +363 -0
- package/dist/held.d.ts +150 -0
- package/dist/held.js +799 -0
- package/dist/invoke.d.ts +112 -0
- package/dist/invoke.js +780 -0
- package/dist/issues.d.ts +41 -0
- package/dist/issues.js +131 -0
- package/dist/job-object-helper.ps1 +252 -0
- package/dist/jsonl-discriminants.d.ts +11 -0
- package/dist/jsonl-discriminants.js +93 -0
- package/dist/keys.d.ts +104 -0
- package/dist/keys.js +229 -0
- package/dist/kits-ui.d.ts +17 -0
- package/dist/kits-ui.js +46 -0
- package/dist/kits.d.ts +86 -0
- package/dist/kits.js +215 -0
- package/dist/knowledge-cli.d.ts +101 -0
- package/dist/knowledge-cli.js +92 -0
- package/dist/knowledge-ui.d.ts +24 -0
- package/dist/knowledge-ui.js +40 -0
- package/dist/lead-context.d.ts +8 -0
- package/dist/lead-context.js +56 -0
- package/dist/lead-follow.d.ts +22 -0
- package/dist/lead-follow.js +167 -0
- package/dist/lead-status.d.ts +75 -0
- package/dist/lead-status.js +285 -0
- package/dist/ledger-chain.d.ts +46 -0
- package/dist/ledger-chain.js +187 -0
- package/dist/ledger-csv.d.ts +4 -0
- package/dist/ledger-csv.js +11 -0
- package/dist/ledger-view.d.ts +24 -0
- package/dist/ledger-view.js +58 -0
- package/dist/limits-ui.d.ts +26 -0
- package/dist/limits-ui.js +67 -0
- package/dist/link.d.ts +72 -0
- package/dist/link.js +217 -0
- package/dist/live.d.ts +96 -0
- package/dist/live.js +366 -0
- package/dist/liveness.d.ts +30 -0
- package/dist/liveness.js +42 -0
- package/dist/log.d.ts +7 -0
- package/dist/log.js +25 -0
- package/dist/mailbox.d.ts +57 -0
- package/dist/mailbox.js +150 -0
- package/dist/maintenance.d.ts +11 -0
- package/dist/maintenance.js +35 -0
- package/dist/mate-cli.d.ts +52 -0
- package/dist/mate-cli.js +345 -0
- package/dist/mate-contract.d.ts +10 -0
- package/dist/mate-contract.js +30 -0
- package/dist/mate-doors.d.ts +69 -0
- package/dist/mate-doors.js +548 -0
- package/dist/mate-progress.d.ts +29 -0
- package/dist/mate-progress.js +121 -0
- package/dist/mate-tools.d.ts +94 -0
- package/dist/mate-tools.js +1878 -0
- package/dist/mate.d.ts +92 -0
- package/dist/mate.js +435 -0
- package/dist/mcp-connect.d.ts +103 -0
- package/dist/mcp-connect.js +252 -0
- package/dist/mcp.d.ts +27 -0
- package/dist/mcp.js +651 -0
- package/dist/memory-cli.d.ts +466 -0
- package/dist/memory-cli.js +111 -0
- package/dist/memory-pass.d.ts +160 -0
- package/dist/memory-pass.js +409 -0
- package/dist/metrics.d.ts +3 -0
- package/dist/metrics.js +56 -0
- package/dist/mobile-viewport.d.ts +3 -0
- package/dist/mobile-viewport.js +37 -0
- package/dist/model-catalog.d.ts +118 -0
- package/dist/model-catalog.js +376 -0
- package/dist/models-cli.d.ts +102 -0
- package/dist/models-cli.js +66 -0
- package/dist/models-ui.d.ts +34 -0
- package/dist/models-ui.js +68 -0
- package/dist/modes.d.ts +80 -0
- package/dist/modes.js +182 -0
- package/dist/monitoring-settings.d.ts +44 -0
- package/dist/monitoring-settings.js +173 -0
- package/dist/monitoring-ui.d.ts +20 -0
- package/dist/monitoring-ui.js +51 -0
- package/dist/monitoring.d.ts +34 -0
- package/dist/monitoring.js +221 -0
- package/dist/names.d.ts +67 -0
- package/dist/names.js +102 -0
- package/dist/observations.d.ts +40 -0
- package/dist/observations.js +211 -0
- package/dist/oidc.d.ts +71 -0
- package/dist/oidc.js +182 -0
- package/dist/onboard.d.ts +108 -0
- package/dist/onboard.js +325 -0
- package/dist/openrouter-models.d.ts +30 -0
- package/dist/openrouter-models.js +120 -0
- package/dist/operate.d.ts +118 -0
- package/dist/operate.js +11321 -0
- package/dist/peek-cli.d.ts +97 -0
- package/dist/peek-cli.js +215 -0
- package/dist/peek.d.ts +134 -0
- package/dist/peek.js +578 -0
- package/dist/phase-routing.d.ts +307 -0
- package/dist/phase-routing.js +658 -0
- package/dist/plan-auto.d.ts +14 -0
- package/dist/plan-auto.js +92 -0
- package/dist/plan.d.ts +186 -0
- package/dist/plan.js +401 -0
- package/dist/planner-source.d.ts +251 -0
- package/dist/planner-source.js +460 -0
- package/dist/planner.d.ts +75 -0
- package/dist/planner.js +992 -0
- package/dist/policy-ui.d.ts +20 -0
- package/dist/policy-ui.js +41 -0
- package/dist/policy.d.ts +111 -0
- package/dist/policy.js +231 -0
- package/dist/prepared-evidence.d.ts +16 -0
- package/dist/prepared-evidence.js +96 -0
- package/dist/pricing.d.ts +45 -0
- package/dist/pricing.js +77 -0
- package/dist/principal.d.ts +42 -0
- package/dist/principal.js +82 -0
- package/dist/probe.d.ts +46 -0
- package/dist/probe.js +79 -0
- package/dist/process-custody.d.ts +17 -0
- package/dist/process-custody.js +91 -0
- package/dist/process-liveness.d.ts +10 -0
- package/dist/process-liveness.js +81 -0
- package/dist/process-recovery-anchor.d.ts +75 -0
- package/dist/process-recovery-anchor.js +188 -0
- package/dist/process-recovery-coalition.d.ts +25 -0
- package/dist/process-recovery-coalition.js +105 -0
- package/dist/process-recovery-eligibility.d.ts +178 -0
- package/dist/process-recovery-eligibility.js +170 -0
- package/dist/process-recovery-native.d.ts +172 -0
- package/dist/process-recovery-native.js +592 -0
- package/dist/process-recovery-provenance.d.ts +163 -0
- package/dist/process-recovery-provenance.js +292 -0
- package/dist/process-recovery-services.d.ts +56 -0
- package/dist/process-recovery-services.js +191 -0
- package/dist/process-recovery-settlement.d.ts +32 -0
- package/dist/process-recovery-settlement.js +132 -0
- package/dist/process-recovery.d.ts +41 -0
- package/dist/process-recovery.js +91 -0
- package/dist/process-tree.d.ts +41 -0
- package/dist/process-tree.js +264 -0
- package/dist/project-access.d.ts +14 -0
- package/dist/project-access.js +34 -0
- package/dist/project-cli.d.ts +18 -0
- package/dist/project-cli.js +104 -0
- package/dist/project-delete-ui.d.ts +22 -0
- package/dist/project-delete-ui.js +48 -0
- package/dist/project-delete.d.ts +55 -0
- package/dist/project-delete.js +413 -0
- package/dist/project-knowledge.d.ts +111 -0
- package/dist/project-knowledge.js +241 -0
- package/dist/project-learning.d.ts +100 -0
- package/dist/project-learning.js +438 -0
- package/dist/project-memory.d.ts +81 -0
- package/dist/project-memory.js +264 -0
- package/dist/project-skills.d.ts +152 -0
- package/dist/project-skills.js +660 -0
- package/dist/project-tools.d.ts +219 -0
- package/dist/project-tools.js +796 -0
- package/dist/project.d.ts +61 -0
- package/dist/project.js +125 -0
- package/dist/prompt.d.ts +22 -0
- package/dist/prompt.js +72 -0
- package/dist/proof.d.ts +513 -0
- package/dist/proof.js +1140 -0
- package/dist/proposal.d.ts +82 -0
- package/dist/proposal.js +210 -0
- package/dist/provider-connection.d.ts +22 -0
- package/dist/provider-connection.js +98 -0
- package/dist/provider-limits.d.ts +35 -0
- package/dist/provider-limits.js +95 -0
- package/dist/provider.d.ts +279 -0
- package/dist/provider.js +935 -0
- package/dist/publish.d.ts +107 -0
- package/dist/publish.js +650 -0
- package/dist/pulls.d.ts +118 -0
- package/dist/pulls.js +240 -0
- package/dist/push.d.ts +95 -0
- package/dist/push.js +353 -0
- package/dist/quality.d.ts +8 -0
- package/dist/quality.js +6 -0
- package/dist/recipe-ui.d.ts +18 -0
- package/dist/recipe-ui.js +152 -0
- package/dist/recipes.d.ts +81 -0
- package/dist/recipes.js +332 -0
- package/dist/remote.d.ts +67 -0
- package/dist/remote.js +122 -0
- package/dist/render.d.ts +64 -0
- package/dist/render.js +587 -0
- package/dist/report-summary.d.ts +13 -0
- package/dist/report-summary.js +15 -0
- package/dist/repos.d.ts +65 -0
- package/dist/repos.js +277 -0
- package/dist/repository-context-ui.d.ts +2 -0
- package/dist/repository-context-ui.js +10 -0
- package/dist/repository-context.d.ts +67 -0
- package/dist/repository-context.js +376 -0
- package/dist/restart-certification.d.ts +134 -0
- package/dist/restart-certification.js +226 -0
- package/dist/result-actions.d.ts +28 -0
- package/dist/result-actions.js +270 -0
- package/dist/result-completion.d.ts +18 -0
- package/dist/result-completion.js +66 -0
- package/dist/result-review.d.ts +213 -0
- package/dist/result-review.js +430 -0
- package/dist/retention-ui.d.ts +12 -0
- package/dist/retention-ui.js +39 -0
- package/dist/retention.d.ts +72 -0
- package/dist/retention.js +288 -0
- package/dist/review-context.d.ts +243 -0
- package/dist/review-context.js +1043 -0
- package/dist/review-evidence.d.ts +33 -0
- package/dist/review-evidence.js +56 -0
- package/dist/reviewer.d.ts +97 -0
- package/dist/reviewer.js +202 -0
- package/dist/routine.d.ts +228 -0
- package/dist/routine.js +764 -0
- package/dist/runner.d.ts +266 -0
- package/dist/runner.js +399 -0
- package/dist/scan.d.ts +32 -0
- package/dist/scan.js +92 -0
- package/dist/scope.d.ts +695 -0
- package/dist/scope.js +1176 -0
- package/dist/scout-report.d.ts +42 -0
- package/dist/scout-report.js +108 -0
- package/dist/scout.d.ts +69 -0
- package/dist/scout.js +351 -0
- package/dist/serve.d.ts +313 -0
- package/dist/serve.js +21776 -0
- package/dist/session-brief.d.ts +8 -0
- package/dist/session-brief.js +24 -0
- package/dist/session-cli.d.ts +13 -0
- package/dist/session-cli.js +284 -0
- package/dist/session-contract.d.ts +183 -0
- package/dist/session-contract.js +122 -0
- package/dist/session-http.d.ts +19 -0
- package/dist/session-http.js +154 -0
- package/dist/session-server.d.ts +12 -0
- package/dist/session-server.js +37 -0
- package/dist/session-service.d.ts +22 -0
- package/dist/session-service.js +157 -0
- package/dist/setup-guide.d.ts +39 -0
- package/dist/setup-guide.js +93 -0
- package/dist/sign-in-guard.d.ts +45 -0
- package/dist/sign-in-guard.js +74 -0
- package/dist/skills-ui.d.ts +11 -0
- package/dist/skills-ui.js +58 -0
- package/dist/skills.d.ts +97 -0
- package/dist/skills.js +276 -0
- package/dist/slack-api.d.ts +55 -0
- package/dist/slack-api.js +229 -0
- package/dist/slack-chat.d.ts +28 -0
- package/dist/slack-chat.js +413 -0
- package/dist/slack-settings.d.ts +7 -0
- package/dist/slack-settings.js +75 -0
- package/dist/slack-state.d.ts +13 -0
- package/dist/slack-state.js +9 -0
- package/dist/slack.d.ts +13 -0
- package/dist/slack.js +167 -0
- package/dist/spend-ui.d.ts +28 -0
- package/dist/spend-ui.js +95 -0
- package/dist/spend.d.ts +142 -0
- package/dist/spend.js +313 -0
- package/dist/sqlite-runtime.d.ts +3 -0
- package/dist/sqlite-runtime.js +6 -0
- package/dist/sso-settings.d.ts +33 -0
- package/dist/sso-settings.js +103 -0
- package/dist/sso-ui.d.ts +22 -0
- package/dist/sso-ui.js +39 -0
- package/dist/storage.d.ts +15 -0
- package/dist/storage.js +84 -0
- package/dist/store.d.ts +7754 -0
- package/dist/store.js +23279 -0
- package/dist/structured-output.d.ts +58 -0
- package/dist/structured-output.js +103 -0
- package/dist/style-asset.d.ts +7 -0
- package/dist/style-asset.js +41 -0
- package/dist/subscription-chat.d.ts +46 -0
- package/dist/subscription-chat.js +275 -0
- package/dist/summary.d.ts +33 -0
- package/dist/summary.js +71 -0
- package/dist/supervisor.mjs +297 -0
- package/dist/surface.d.ts +63 -0
- package/dist/surface.js +352 -0
- package/dist/sync.d.ts +94 -0
- package/dist/sync.js +279 -0
- package/dist/task-composer.d.ts +13 -0
- package/dist/task-composer.js +52 -0
- package/dist/task-control.d.ts +165 -0
- package/dist/task-control.js +214 -0
- package/dist/task-outcome-cli.d.ts +10 -0
- package/dist/task-outcome-cli.js +92 -0
- package/dist/task-text.d.ts +30 -0
- package/dist/task-text.js +41 -0
- package/dist/team-cli.d.ts +15 -0
- package/dist/team-cli.js +618 -0
- package/dist/team-contract.d.ts +92 -0
- package/dist/team-contract.js +1 -0
- package/dist/team-http.d.ts +17 -0
- package/dist/team-http.js +144 -0
- package/dist/team-leads.d.ts +81 -0
- package/dist/team-leads.js +565 -0
- package/dist/team-runtime.d.ts +27 -0
- package/dist/team-runtime.js +264 -0
- package/dist/team-ui.d.ts +3 -0
- package/dist/team-ui.js +9 -0
- package/dist/team-updates.d.ts +8 -0
- package/dist/team-updates.js +57 -0
- package/dist/teammate-admin.d.ts +53 -0
- package/dist/teammate-admin.js +111 -0
- package/dist/teammate-desk.d.ts +60 -0
- package/dist/teammate-desk.js +154 -0
- package/dist/teammate-memory.d.ts +64 -0
- package/dist/teammate-memory.js +196 -0
- package/dist/teammate-question.d.ts +59 -0
- package/dist/teammate-question.js +175 -0
- package/dist/teammate-tools.d.ts +108 -0
- package/dist/teammate-tools.js +271 -0
- package/dist/teammate-week.d.ts +68 -0
- package/dist/teammate-week.js +132 -0
- package/dist/teammate-work.d.ts +76 -0
- package/dist/teammate-work.js +280 -0
- package/dist/teammates-ui.d.ts +20 -0
- package/dist/teammates-ui.js +166 -0
- package/dist/teammates.d.ts +180 -0
- package/dist/teammates.js +253 -0
- package/dist/teams-api.d.ts +33 -0
- package/dist/teams-api.js +204 -0
- package/dist/teams-chat.d.ts +26 -0
- package/dist/teams-chat.js +215 -0
- package/dist/teams-settings.d.ts +12 -0
- package/dist/teams-settings.js +57 -0
- package/dist/teams.d.ts +21 -0
- package/dist/teams.js +124 -0
- package/dist/telegram-flow.d.ts +49 -0
- package/dist/telegram-flow.js +110 -0
- package/dist/telegram-mate.d.ts +137 -0
- package/dist/telegram-mate.js +707 -0
- package/dist/telegram-progress.d.ts +14 -0
- package/dist/telegram-progress.js +129 -0
- package/dist/telegram-settings.d.ts +9 -0
- package/dist/telegram-settings.js +36 -0
- package/dist/telegram-status.d.ts +75 -0
- package/dist/telegram-status.js +265 -0
- package/dist/telegram-team.d.ts +85 -0
- package/dist/telegram-team.js +359 -0
- package/dist/telegram.d.ts +230 -0
- package/dist/telegram.js +1931 -0
- package/dist/templates.d.ts +65 -0
- package/dist/templates.js +118 -0
- package/dist/tool-launcher.d.ts +1 -0
- package/dist/tool-launcher.js +101 -0
- package/dist/tools-ui.d.ts +30 -0
- package/dist/tools-ui.js +40 -0
- package/dist/transitions-recipes.d.ts +1 -0
- package/dist/transitions-recipes.js +203 -0
- package/dist/tree-proof.d.ts +34 -0
- package/dist/tree-proof.js +58 -0
- package/dist/verification-evidence.d.ts +44 -0
- package/dist/verification-evidence.js +238 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +10 -0
- package/dist/webhooks.d.ts +91 -0
- package/dist/webhooks.js +283 -0
- package/dist/work-index.d.ts +68 -0
- package/dist/work-index.js +384 -0
- package/dist/work-summary.d.ts +57 -0
- package/dist/work-summary.js +116 -0
- package/dist/workspace-motion.d.ts +4 -0
- package/dist/workspace-motion.js +133 -0
- package/dist/workspace-revision.d.ts +19 -0
- package/dist/workspace-revision.js +92 -0
- package/dist/workspace-ui.d.ts +204 -0
- package/dist/workspace-ui.js +411 -0
- package/dist/worktree-notices.d.ts +13 -0
- package/dist/worktree-notices.js +17 -0
- package/dist/worktree.d.ts +249 -0
- package/dist/worktree.js +720 -0
- package/package.json +122 -0
- package/scripts/canary-assertions.mjs +42 -0
- package/scripts/crash-canary.mjs +253 -0
- package/scripts/fixtures/crash-process.mjs +74 -0
- package/scripts/fixtures/pilot-scenarios.mjs +85 -0
- package/scripts/fixtures/restart-service.mjs +27 -0
- package/scripts/launchd-certification.mjs +143 -0
- package/scripts/pilot.mjs +121 -0
- package/scripts/proof-preflight.mjs +140 -0
- package/scripts/provider-canary.mjs +379 -0
- package/scripts/recovery-canary.mjs +135 -0
- package/scripts/restart-certification.mjs +98 -0
package/dist/spend.js
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import { SUBSCRIPTION_CAPABLE, readAuthMode } from "./keys.js";
|
|
2
|
+
/** How a provider's work is billed right now: an API key when Toolroll is set to use one, or when the CLI
|
|
3
|
+
* itself was last seen billing a key (a key helper, a Console login, Bedrock or Vertex, `codex login --with-api-key`);
|
|
4
|
+
* otherwise its plan. `db` supplies what was seen (provider_account); without it, the setting alone. */
|
|
5
|
+
export function billingOf(provider, db) {
|
|
6
|
+
const base = provider.replace(/-(subscription|api)$/, "");
|
|
7
|
+
if (provider.endsWith("-api"))
|
|
8
|
+
return "api-key";
|
|
9
|
+
const setting = provider.endsWith("-subscription") ? "subscription"
|
|
10
|
+
: Object.hasOwn(SUBSCRIPTION_CAPABLE, provider) ? readAuthMode(provider) : "api-key";
|
|
11
|
+
if (setting === "api-key" || db === undefined)
|
|
12
|
+
return setting;
|
|
13
|
+
return seenBilling(db, base) ?? setting;
|
|
14
|
+
}
|
|
15
|
+
/** How a provider's CLI was last seen billing (provider_account), or null. */
|
|
16
|
+
export function seenBilling(db, provider) {
|
|
17
|
+
const row = db.prepare("SELECT billing FROM provider_account WHERE provider = ?").get(provider);
|
|
18
|
+
return row?.["billing"] === "api-key" || row?.["billing"] === "subscription" ? row["billing"] : null;
|
|
19
|
+
}
|
|
20
|
+
/** How a Claude turn was really billed, from its own stream. Only a plan reports its usage windows (Claude Code 2.1
|
|
21
|
+
* does on every turn), so that is the one sign of the plan; a named key source or a cloud model id (Bedrock, Vertex)
|
|
22
|
+
* is a key, and so is "none" without windows (a gateway's bearer token or a cloud provider says "none" too). Null when
|
|
23
|
+
* the stream said nothing. */
|
|
24
|
+
export function claudeBillingFrom(seen) {
|
|
25
|
+
if (seen.keySource !== null && seen.keySource !== "none")
|
|
26
|
+
return "api-key";
|
|
27
|
+
if (seen.model !== null && /^arn:|(^|\.)anthropic\.|@/.test(seen.model))
|
|
28
|
+
return "api-key";
|
|
29
|
+
if (seen.planWindows)
|
|
30
|
+
return "subscription";
|
|
31
|
+
return seen.keySource === "none" ? "api-key" : null;
|
|
32
|
+
}
|
|
33
|
+
/** How Claude bills on this computer when Toolroll gives it no key (teammate turns, drafts): as its keyless runs
|
|
34
|
+
* were last seen, and its plan until one has been. */
|
|
35
|
+
export function claudeMachineBilling(db) {
|
|
36
|
+
return seenBilling(db, "claude") ?? "subscription";
|
|
37
|
+
}
|
|
38
|
+
/** Providers whose own output says what a turn cost (Claude's total_cost_usd; OpenRouter's usage.cost). */
|
|
39
|
+
const REPORTS_COST = new Set(["claude", "openrouter", "anthropic-api", "openrouter-api"]);
|
|
40
|
+
/** Whether API work on this provider can be priced: it reports its cost, or the catalogue has its prices. */
|
|
41
|
+
export function canPrice(db, provider) {
|
|
42
|
+
return REPORTS_COST.has(provider) || ceilingPriceFor(db, provider) !== null;
|
|
43
|
+
}
|
|
44
|
+
const CLAUDE_FAMILIES = ["opus", "sonnet", "haiku", "fable"];
|
|
45
|
+
const catalogSource = (provider) => provider === "claude" || provider === "claude-subscription" || provider === "anthropic-api" ? "claude"
|
|
46
|
+
: provider === "codex" || provider === "codex-subscription" ? "codex"
|
|
47
|
+
: provider === "gemini" ? "gemini"
|
|
48
|
+
: provider === "openrouter" || provider === "openrouter-api" ? "openrouter" : null;
|
|
49
|
+
/** A model's catalogue price (US dollars per million tokens), or null when the catalogue doesn't list one. A
|
|
50
|
+
* Claude short name ("sonnet") is priced as the newest of its family, as the Claude CLI runs it. */
|
|
51
|
+
export function priceFor(db, provider, model) {
|
|
52
|
+
const source = catalogSource(provider);
|
|
53
|
+
if (source === null || model === null || model === "" || model === "default")
|
|
54
|
+
return null;
|
|
55
|
+
const priced = (row) => row === undefined || row["input_usd"] == null || row["output_usd"] == null ? null
|
|
56
|
+
: { inputUsd: Number(row["input_usd"]), outputUsd: Number(row["output_usd"]), model: String(row["id"]) };
|
|
57
|
+
const exact = priced(db.prepare("SELECT id, input_usd, output_usd FROM model_seen WHERE source = ? AND id = ?").get(source, model));
|
|
58
|
+
if (exact !== null || source !== "claude" || !CLAUDE_FAMILIES.includes(model))
|
|
59
|
+
return exact;
|
|
60
|
+
return priced(db.prepare(`SELECT id, input_usd, output_usd FROM model_seen WHERE source = 'claude' AND id LIKE ? AND last_seen_at = (SELECT MAX(last_seen_at) FROM model_seen WHERE source = 'claude')
|
|
61
|
+
ORDER BY COALESCE(released_at, '') DESC, id DESC LIMIT 1`).get(`claude-${model}-%`));
|
|
62
|
+
}
|
|
63
|
+
/** The provider's highest listed price, for API work whose model the catalogue doesn't list (or that ran the CLI's
|
|
64
|
+
* default): a budget that stops work must never see it as free. */
|
|
65
|
+
export function ceilingPriceFor(db, provider) {
|
|
66
|
+
const source = catalogSource(provider);
|
|
67
|
+
if (source === null)
|
|
68
|
+
return null;
|
|
69
|
+
const row = db.prepare(`SELECT MAX(input_usd) AS input_usd, MAX(output_usd) AS output_usd FROM model_seen WHERE source = ? AND input_usd IS NOT NULL AND output_usd IS NOT NULL
|
|
70
|
+
AND last_seen_at = (SELECT MAX(last_seen_at) FROM model_seen WHERE source = ?)`).get(source, source);
|
|
71
|
+
return row === undefined || row["input_usd"] == null || row["output_usd"] == null ? null
|
|
72
|
+
: { inputUsd: Number(row["input_usd"]), outputUsd: Number(row["output_usd"]), model: "highest listed price" };
|
|
73
|
+
}
|
|
74
|
+
/** What a piece of work cost, in micro-dollars: nothing on a subscription; otherwise reported, or tokens at the
|
|
75
|
+
* catalogue price (usd per million tokens times tokens is micro-dollars), or unpriced. */
|
|
76
|
+
export function priceWork(db, work) {
|
|
77
|
+
if (work.billing === "subscription")
|
|
78
|
+
return { microusd: 0, source: "subscription", price: null };
|
|
79
|
+
if (work.costUsd !== null && Number.isFinite(work.costUsd))
|
|
80
|
+
return { microusd: Math.round(work.costUsd * 1_000_000), source: "reported", price: null };
|
|
81
|
+
if (work.tokensIn === null && work.tokensOut === null)
|
|
82
|
+
return { microusd: null, source: "unpriced", price: null };
|
|
83
|
+
const price = priceFor(db, work.provider, work.model) ?? ceilingPriceFor(db, work.provider);
|
|
84
|
+
if (price === null)
|
|
85
|
+
return { microusd: null, source: "unpriced", price: null };
|
|
86
|
+
return { microusd: Math.round((work.tokensIn ?? 0) * price.inputUsd + (work.tokensOut ?? 0) * price.outputUsd), source: "estimated", price };
|
|
87
|
+
}
|
|
88
|
+
export const SPEND_SCHEMA = `
|
|
89
|
+
CREATE TABLE IF NOT EXISTS run_spend (
|
|
90
|
+
run INTEGER PRIMARY KEY,
|
|
91
|
+
microusd INTEGER,
|
|
92
|
+
source TEXT NOT NULL CHECK (source IN ('reported', 'estimated', 'unpriced', 'subscription')),
|
|
93
|
+
billing TEXT NOT NULL CHECK (billing IN ('subscription', 'api-key')),
|
|
94
|
+
-- 1 once the run's own evidence set it (a key it was given, what its CLI said); a later turn can only make it a key.
|
|
95
|
+
billing_fixed INTEGER NOT NULL DEFAULT 0,
|
|
96
|
+
price_model TEXT,
|
|
97
|
+
input_usd REAL,
|
|
98
|
+
output_usd REAL,
|
|
99
|
+
priced_at TEXT NOT NULL
|
|
100
|
+
);
|
|
101
|
+
CREATE TABLE IF NOT EXISTS budget (
|
|
102
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
103
|
+
scope_kind TEXT NOT NULL CHECK (scope_kind IN ('installation', 'project', 'person', 'teammate')),
|
|
104
|
+
scope_key TEXT NOT NULL,
|
|
105
|
+
limit_microusd INTEGER NOT NULL CHECK (limit_microusd > 0),
|
|
106
|
+
hard_stop INTEGER NOT NULL DEFAULT 1,
|
|
107
|
+
created_by TEXT NOT NULL,
|
|
108
|
+
created_at TEXT NOT NULL,
|
|
109
|
+
updated_by TEXT NOT NULL,
|
|
110
|
+
updated_at TEXT NOT NULL,
|
|
111
|
+
removed_by TEXT,
|
|
112
|
+
removed_at TEXT
|
|
113
|
+
);
|
|
114
|
+
CREATE UNIQUE INDEX IF NOT EXISTS budget_live ON budget (scope_kind, scope_key) WHERE removed_at IS NULL;
|
|
115
|
+
CREATE TABLE IF NOT EXISTS provider_limit (
|
|
116
|
+
provider TEXT NOT NULL,
|
|
117
|
+
window TEXT NOT NULL,
|
|
118
|
+
used_percent REAL NOT NULL,
|
|
119
|
+
window_minutes INTEGER,
|
|
120
|
+
resets_at TEXT,
|
|
121
|
+
reached INTEGER NOT NULL DEFAULT 0,
|
|
122
|
+
plan TEXT,
|
|
123
|
+
observed_at TEXT NOT NULL,
|
|
124
|
+
PRIMARY KEY (provider, window)
|
|
125
|
+
);
|
|
126
|
+
-- Spend that isn't a run, a teammate turn or a chat: a flow's Claude draft.
|
|
127
|
+
CREATE TABLE IF NOT EXISTS side_spend (
|
|
128
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
129
|
+
kind TEXT NOT NULL CHECK (kind IN ('draft')),
|
|
130
|
+
repo TEXT,
|
|
131
|
+
provider TEXT NOT NULL,
|
|
132
|
+
model TEXT,
|
|
133
|
+
cost_usd REAL,
|
|
134
|
+
billing TEXT NOT NULL CHECK (billing IN ('subscription', 'api-key')),
|
|
135
|
+
at TEXT NOT NULL
|
|
136
|
+
);
|
|
137
|
+
CREATE INDEX IF NOT EXISTS side_spend_at ON side_spend (at);
|
|
138
|
+
CREATE TABLE IF NOT EXISTS provider_account (
|
|
139
|
+
provider TEXT PRIMARY KEY,
|
|
140
|
+
billing TEXT NOT NULL CHECK (billing IN ('subscription', 'api-key')),
|
|
141
|
+
observed_at TEXT NOT NULL
|
|
142
|
+
);
|
|
143
|
+
CREATE INDEX IF NOT EXISTS chat_turn_created ON chat_turn (created_at);
|
|
144
|
+
CREATE INDEX IF NOT EXISTS mate_turn_created ON mate_turn (created_at);
|
|
145
|
+
CREATE INDEX IF NOT EXISTS teammate_turn_at ON teammate_turn (at);
|
|
146
|
+
`;
|
|
147
|
+
/** The calendar month (UTC) `at` falls in: [start, end) as ISO strings, and its name (YYYY-MM). */
|
|
148
|
+
export function monthOf(at) {
|
|
149
|
+
const from = new Date(Date.UTC(at.getUTCFullYear(), at.getUTCMonth(), 1));
|
|
150
|
+
const to = new Date(Date.UTC(at.getUTCFullYear(), at.getUTCMonth() + 1, 1));
|
|
151
|
+
return { from: from.toISOString(), to: to.toISOString(), name: from.toISOString().slice(0, 7) };
|
|
152
|
+
}
|
|
153
|
+
/** A month by its name (YYYY-MM), or null. */
|
|
154
|
+
export function monthNamed(name) {
|
|
155
|
+
if (name === null || !/^\d{4}-(0[1-9]|1[0-2])$/.test(name))
|
|
156
|
+
return null;
|
|
157
|
+
return monthOf(new Date(`${name}-01T00:00:00.000Z`));
|
|
158
|
+
}
|
|
159
|
+
/** The teammate behind a filing: tasks a teammate files are filed as its name ("Maya (AI)", from its soul file), in
|
|
160
|
+
* its project. A later teammate with the same name and project wins, as it would read on the card. */
|
|
161
|
+
/** A teammate's name as its soul file gives it ("Maya"), or null. */
|
|
162
|
+
const soulName = (soul) => {
|
|
163
|
+
const front = /^---\s*\n([\s\S]*?)\n---/.exec(String(soul ?? ""));
|
|
164
|
+
return front === null ? null : /^name:\s*(.+?)\s*$/m.exec(front[1])?.[1] ?? null;
|
|
165
|
+
};
|
|
166
|
+
/** Every teammate's name by id (its handle when its soul file names none). */
|
|
167
|
+
export function teammateNames(db) {
|
|
168
|
+
return new Map(db.prepare("SELECT id, handle, soul FROM teammate").all().map(row => [Number(row["id"]), soulName(row["soul"]) ?? String(row["handle"])]));
|
|
169
|
+
}
|
|
170
|
+
export function teammateFilers(db) {
|
|
171
|
+
const byName = new Map();
|
|
172
|
+
for (const row of db.prepare("SELECT id, repo, handle, soul FROM teammate ORDER BY id").all()) {
|
|
173
|
+
const name = soulName(row["soul"]);
|
|
174
|
+
for (const one of [String(row["handle"]), ...(name === null ? [] : [name])])
|
|
175
|
+
byName.set(`${String(row["repo"])}\n${one} (AI)`, Number(row["id"]));
|
|
176
|
+
}
|
|
177
|
+
return (repo, filedBy, kind) => kind !== "teammate" || repo === null || filedBy === null ? null : byName.get(`${repo}\n${filedBy}`) ?? null;
|
|
178
|
+
}
|
|
179
|
+
/** Who a task's work counts toward: its own filer, or, for a revision filed by automation (an automatic repair), the
|
|
180
|
+
* filer of the task it revises, up its ancestry (bounded). */
|
|
181
|
+
export function filersOf(db) {
|
|
182
|
+
const memo = new Map();
|
|
183
|
+
const read = db.prepare("SELECT id, backend, repo, filed_by, filed_by_kind, revision_of FROM task_ref WHERE id = ?");
|
|
184
|
+
const source = db.prepare("SELECT id, backend, repo, filed_by, filed_by_kind, revision_of FROM task_ref WHERE backend = ? AND external_id = ? ORDER BY id DESC LIMIT 1");
|
|
185
|
+
return taskRef => {
|
|
186
|
+
const known = memo.get(taskRef);
|
|
187
|
+
if (known !== undefined)
|
|
188
|
+
return known;
|
|
189
|
+
let row = read.get(taskRef);
|
|
190
|
+
const repo = row?.["repo"] == null ? null : String(row["repo"]);
|
|
191
|
+
const seen = new Set();
|
|
192
|
+
while (row !== undefined && row["filed_by"] == null && row["revision_of"] != null && !seen.has(Number(row["id"])) && seen.size < 64) {
|
|
193
|
+
seen.add(Number(row["id"]));
|
|
194
|
+
row = source.get(row["backend"], row["revision_of"]);
|
|
195
|
+
}
|
|
196
|
+
const found = { repo, filedBy: row?.["filed_by"] == null ? null : String(row["filed_by"]), kind: row?.["filed_by_kind"] == null ? null : String(row["filed_by_kind"]) };
|
|
197
|
+
memo.set(taskRef, found);
|
|
198
|
+
return found;
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
/** Every piece of spend in [from, to): runs (by when they started), teammate turns and chat turns. `modeOf` says how
|
|
202
|
+
* a provider bills when a record doesn't (older rows): its current login or key. */
|
|
203
|
+
export function spendItems(db, from, to, modeOf = provider => billingOf(provider, db)) {
|
|
204
|
+
const teammateOf = teammateFilers(db);
|
|
205
|
+
const filerOf = filersOf(db);
|
|
206
|
+
const text = (value) => value == null ? null : String(value);
|
|
207
|
+
const count = (value) => value == null ? null : Number(value);
|
|
208
|
+
const runs = db.prepare(`SELECT run.id, run.task_ref, run.provider, run.model, run.cost_usd, run.tokens_in, run.tokens_out, run.started_at, run.auth_mode,
|
|
209
|
+
r.external_id AS task, r.repo, r.filed_by, r.filed_by_kind, r.revision_of, s.microusd, s.source, s.billing
|
|
210
|
+
FROM run JOIN task_ref r ON r.id = run.task_ref LEFT JOIN run_spend s ON s.run = run.id
|
|
211
|
+
WHERE run.started_at >= ? AND run.started_at < ? ORDER BY run.id`).all(from, to);
|
|
212
|
+
const items = runs.map(row => {
|
|
213
|
+
const provider = String(row["provider"]);
|
|
214
|
+
const billing = row["billing"] === "subscription" || row["billing"] === "api-key" ? row["billing"]
|
|
215
|
+
: row["auth_mode"] === "subscription" || row["auth_mode"] === "api-key" ? row["auth_mode"] : modeOf(provider);
|
|
216
|
+
const settled = row["source"] != null;
|
|
217
|
+
const priced = settled ? null : priceWork(db, { provider, model: text(row["model"]), costUsd: count(row["cost_usd"]), tokensIn: count(row["tokens_in"]), tokensOut: count(row["tokens_out"]), billing });
|
|
218
|
+
// Most tasks name their filer; only an automation-filed revision walks to its source's.
|
|
219
|
+
const filer = row["filed_by"] == null && row["revision_of"] != null ? filerOf(Number(row["task_ref"]))
|
|
220
|
+
: { repo: text(row["repo"]), filedBy: text(row["filed_by"]), kind: text(row["filed_by_kind"]) };
|
|
221
|
+
const kind = filer.kind;
|
|
222
|
+
return {
|
|
223
|
+
at: String(row["started_at"]), kind: "run", microusd: settled ? count(row["microusd"]) : priced.microusd,
|
|
224
|
+
source: settled ? String(row["source"]) : priced.source,
|
|
225
|
+
project: text(row["repo"]),
|
|
226
|
+
person: kind === "person" || kind === "coordinator" ? filer.filedBy : null,
|
|
227
|
+
teammate: teammateOf(filer.repo, filer.filedBy, kind), provider, model: text(row["model"]),
|
|
228
|
+
tokensIn: count(row["tokens_in"]), tokensOut: count(row["tokens_out"]),
|
|
229
|
+
taskId: String(row["task"]), runId: Number(row["id"]), authMode: billing,
|
|
230
|
+
};
|
|
231
|
+
});
|
|
232
|
+
// A teammate's own turns get no key from Toolroll (teammates.ts claudeTurnRunner): they bill as this computer's
|
|
233
|
+
// Claude sign-in was seen billing when the turn ran (a plan, or a Console login, key helper or gateway).
|
|
234
|
+
for (const row of db.prepare(`SELECT t.at, t.teammate, t.model, t.cost_usd, t.tokens_in, t.tokens_out, t.billing, m.repo FROM teammate_turn t JOIN teammate m ON m.id = t.teammate
|
|
235
|
+
WHERE t.at >= ? AND t.at < ? ORDER BY t.id`).all(from, to)) {
|
|
236
|
+
const billing = row["billing"] === "api-key" ? "api-key" : "subscription";
|
|
237
|
+
const priced = priceWork(db, { provider: "claude", model: text(row["model"]), costUsd: count(row["cost_usd"]), tokensIn: count(row["tokens_in"]), tokensOut: count(row["tokens_out"]), billing });
|
|
238
|
+
items.push({ at: String(row["at"]), kind: "teammate", microusd: priced.microusd, source: priced.source, project: String(row["repo"]), person: null,
|
|
239
|
+
teammate: Number(row["teammate"]), provider: "claude", model: text(row["model"]),
|
|
240
|
+
tokensIn: count(row["tokens_in"]), tokensOut: count(row["tokens_out"]), taskId: null, runId: null, authMode: billing });
|
|
241
|
+
}
|
|
242
|
+
// A chat turn that mirrors a project chat's turn is that turn: counted once, as the project chat's. A task's chat
|
|
243
|
+
// counts toward the task's project.
|
|
244
|
+
for (const row of db.prepare(`SELECT created_at AS at, approver, provider, model, settled_microusd, tokens_in, tokens_out, NULL AS project
|
|
245
|
+
FROM chat_turn WHERE mate_turn IS NULL AND created_at >= ? AND created_at < ?
|
|
246
|
+
UNION ALL SELECT t.created_at, t.approver, COALESCE((SELECT c.provider FROM chat_turn c WHERE c.mate_turn = t.id ORDER BY c.id LIMIT 1), 'lead'),
|
|
247
|
+
(SELECT c.model FROM chat_turn c WHERE c.mate_turn = t.id ORDER BY c.id LIMIT 1), t.settled_microusd, t.tokens_in, t.tokens_out,
|
|
248
|
+
CASE th.scope_kind WHEN 'project' THEN th.scope_key WHEN 'task' THEN (SELECT r.repo FROM task_ref r WHERE r.external_id = th.scope_key ORDER BY r.id DESC LIMIT 1) END
|
|
249
|
+
FROM mate_turn t LEFT JOIN mate_thread th ON th.id = t.thread WHERE t.created_at >= ? AND t.created_at < ?`).all(from, to, from, to)) {
|
|
250
|
+
const provider = String(row["provider"]);
|
|
251
|
+
const subscription = provider.endsWith("-subscription") && modeOf(provider) === "subscription";
|
|
252
|
+
const settled = count(row["settled_microusd"]);
|
|
253
|
+
items.push({ at: String(row["at"]), kind: "chat", microusd: subscription ? 0 : settled, source: subscription ? "subscription" : settled === null ? "unpriced" : "reported",
|
|
254
|
+
project: text(row["project"]), person: text(row["approver"]), teammate: null, provider, model: text(row["model"]),
|
|
255
|
+
tokensIn: count(row["tokens_in"]), tokensOut: count(row["tokens_out"]), taskId: null, runId: null, authMode: subscription ? "subscription" : "api-key" });
|
|
256
|
+
}
|
|
257
|
+
// A flow's Claude drafts: like a teammate's turns, billed as the sign-in was.
|
|
258
|
+
for (const row of db.prepare("SELECT at, kind, repo, provider, model, cost_usd, billing FROM side_spend WHERE at >= ? AND at < ? ORDER BY id").all(from, to)) {
|
|
259
|
+
const billing = row["billing"] === "api-key" ? "api-key" : "subscription";
|
|
260
|
+
const priced = priceWork(db, { provider: String(row["provider"]), model: text(row["model"]), costUsd: count(row["cost_usd"]), tokensIn: null, tokensOut: null, billing });
|
|
261
|
+
items.push({ at: String(row["at"]), kind: "draft", microusd: priced.microusd, source: priced.source, project: text(row["repo"]), person: null, teammate: null,
|
|
262
|
+
provider: String(row["provider"]), model: text(row["model"]), tokensIn: null, tokensOut: null, taskId: null, runId: null, authMode: billing });
|
|
263
|
+
}
|
|
264
|
+
// A flow's Sort zone asks Jev on OpenRouter (the key's credit): what it cost is kept with its answer.
|
|
265
|
+
for (const row of db.prepare(`SELECT fs.started_at AS at, json_extract(fs.decision_json, '$.cost') AS cost, json_extract(fs.decision_json, '$.model') AS model, f.repo
|
|
266
|
+
FROM flow_step_run fs JOIN flow_card c ON c.id = fs.card JOIN flow f ON f.id = c.flow
|
|
267
|
+
WHERE fs.kind = 'sort' AND fs.decision_json IS NOT NULL AND fs.started_at >= ? AND fs.started_at < ? ORDER BY fs.started_at`).all(from, to)) {
|
|
268
|
+
const cost = typeof row["cost"] === "number" && Number.isFinite(row["cost"]) && row["cost"] >= 0 ? row["cost"] : null;
|
|
269
|
+
items.push({ at: String(row["at"]), kind: "sort", microusd: cost === null ? null : Math.round(cost * 1_000_000), source: cost === null ? "unpriced" : "reported",
|
|
270
|
+
project: text(row["repo"]), person: null, teammate: null, provider: "openrouter", model: text(row["model"]), tokensIn: null, tokensOut: null, taskId: null, runId: null, authMode: "api-key" });
|
|
271
|
+
}
|
|
272
|
+
return items;
|
|
273
|
+
}
|
|
274
|
+
/** Whether a piece of spend counts toward a budget. */
|
|
275
|
+
export function countsToward(item, budget) {
|
|
276
|
+
if (budget.scope === "installation")
|
|
277
|
+
return true;
|
|
278
|
+
if (budget.scope === "project")
|
|
279
|
+
return item.project === budget.key;
|
|
280
|
+
if (budget.scope === "person")
|
|
281
|
+
return item.person === budget.key;
|
|
282
|
+
return item.teammate !== null && String(item.teammate) === budget.key;
|
|
283
|
+
}
|
|
284
|
+
const PROVIDER_WORDS = { claude: "Claude", codex: "Codex", gemini: "Gemini", openrouter: "OpenRouter" };
|
|
285
|
+
/** Why work waits on a budget, in words. */
|
|
286
|
+
export function budgetHoldWords(hold, month, teammateName) {
|
|
287
|
+
if (hold.over === null)
|
|
288
|
+
return "";
|
|
289
|
+
const label = budgetLabel(hold.over, teammateName);
|
|
290
|
+
return hold.why === "unpriced"
|
|
291
|
+
? `${label} budget can't price ${PROVIDER_WORDS[hold.unpricedProvider ?? ""] ?? hold.unpricedProvider} work on a key yet. Open Settings → Models to load prices`
|
|
292
|
+
: `${label} budget is used up for ${month}`;
|
|
293
|
+
}
|
|
294
|
+
/** Each budget's month so far: what counted toward it, and how many pieces of work had no price. */
|
|
295
|
+
export function budgetStates(budgets, items) {
|
|
296
|
+
return budgets.map(budget => {
|
|
297
|
+
const mine = items.filter(item => countsToward(item, budget));
|
|
298
|
+
const spent = mine.reduce((sum, item) => sum + (item.microusd ?? 0), 0);
|
|
299
|
+
return { ...budget, spentMicrousd: spent, unpriced: mine.filter(item => item.microusd === null && item.source === "unpriced" && (item.tokensIn !== null || item.kind !== "run")).length,
|
|
300
|
+
percent: Math.floor((spent / budget.limitMicrousd) * 100) };
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
/** A budget in words: "The shop project's", "alex's", "Maya's (teammate)", "The whole installation's". */
|
|
304
|
+
export function budgetLabel(budget, teammateName) {
|
|
305
|
+
if (budget.scope === "installation")
|
|
306
|
+
return "The whole installation's";
|
|
307
|
+
if (budget.scope === "project")
|
|
308
|
+
return `The ${budget.key.split("/").filter(Boolean).pop() ?? budget.key} project's`;
|
|
309
|
+
if (budget.scope === "person")
|
|
310
|
+
return `${budget.key}'s`;
|
|
311
|
+
return `${teammateName ?? `Teammate ${budget.key}`}'s`;
|
|
312
|
+
}
|
|
313
|
+
export const usd = (microusd) => microusd === null ? "unpriced" : `$${(microusd / 1_000_000).toFixed(microusd !== 0 && Math.abs(microusd) < 10_000_000 ? 2 : 0)}`;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
/** Keep native SQLite out of import-only UI/module graphs. Load the real
|
|
3
|
+
* driver only when a database operation needs it, as the main store does. */
|
|
4
|
+
export function sqliteRuntime() {
|
|
5
|
+
return createRequire(import.meta.url)('node:sqlite');
|
|
6
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { OidcSettings } from "./oidc.js";
|
|
2
|
+
export declare const SSO_CALLBACK = "/login/sso/callback";
|
|
3
|
+
/** A name for the button from the issuer, when none is given. */
|
|
4
|
+
export declare function providerLabel(issuer: string): string;
|
|
5
|
+
export declare function readSsoSettings(dir: string | null | undefined): OidcSettings | null;
|
|
6
|
+
/**
|
|
7
|
+
* Save what the form sent. The client secret is kept when the field is left
|
|
8
|
+
* blank; rules come as rows (group, role, projects), blank groups dropped.
|
|
9
|
+
* `projects` for a row is "all" or the project paths chosen.
|
|
10
|
+
*/
|
|
11
|
+
export declare function saveSsoSettings(dir: string, form: {
|
|
12
|
+
issuer: string;
|
|
13
|
+
clientId: string;
|
|
14
|
+
clientSecret: string;
|
|
15
|
+
label: string;
|
|
16
|
+
scopes: string;
|
|
17
|
+
groupsClaim: string;
|
|
18
|
+
passwords: string;
|
|
19
|
+
rules: {
|
|
20
|
+
group: string;
|
|
21
|
+
role: string;
|
|
22
|
+
projects: string[];
|
|
23
|
+
}[];
|
|
24
|
+
}, previous: OidcSettings | null): {
|
|
25
|
+
ok: true;
|
|
26
|
+
settings: OidcSettings;
|
|
27
|
+
} | {
|
|
28
|
+
ok: false;
|
|
29
|
+
said: string;
|
|
30
|
+
};
|
|
31
|
+
export declare function removeSsoSettings(dir: string): void;
|
|
32
|
+
/** What changed, for the ledger: never the secret itself, only that it changed. */
|
|
33
|
+
export declare function ssoChangeWords(before: OidcSettings | null, after: OidcSettings | null): string;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settings → Sign-in (v100): the identity provider this installation trusts,
|
|
3
|
+
* kept in `sign-in.json` beside the database (0600) with its client secret,
|
|
4
|
+
* never in the database. Group rules are ordered: a person's first matching
|
|
5
|
+
* group sets their role and projects.
|
|
6
|
+
*/
|
|
7
|
+
import { chmodSync, existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
const FILE = "sign-in.json";
|
|
10
|
+
export const SSO_CALLBACK = "/login/sso/callback";
|
|
11
|
+
const isAddress = (value) => {
|
|
12
|
+
try {
|
|
13
|
+
const url = new URL(value);
|
|
14
|
+
return url.protocol === "https:" || (url.protocol === "http:" && ["127.0.0.1", "localhost", "[::1]"].includes(url.hostname));
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
/** A name for the button from the issuer, when none is given. */
|
|
21
|
+
export function providerLabel(issuer) {
|
|
22
|
+
const host = (() => { try {
|
|
23
|
+
return new URL(issuer).hostname;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return "";
|
|
27
|
+
} })();
|
|
28
|
+
return /okta\.com$|oktapreview\.com$/.test(host) ? "Okta" : /microsoftonline\.com$|microsoft\.com$/.test(host) ? "Microsoft"
|
|
29
|
+
: /accounts\.google\.com$/.test(host) ? "Google" : /auth0\.com$/.test(host) ? "Auth0" : /jumpcloud\.com$/.test(host) ? "JumpCloud" : "your identity provider";
|
|
30
|
+
}
|
|
31
|
+
export function readSsoSettings(dir) {
|
|
32
|
+
if (!dir)
|
|
33
|
+
return null;
|
|
34
|
+
const file = join(dir, FILE);
|
|
35
|
+
if (!existsSync(file))
|
|
36
|
+
return null;
|
|
37
|
+
try {
|
|
38
|
+
const value = JSON.parse(readFileSync(file, "utf8"));
|
|
39
|
+
if (typeof value.issuer !== "string" || typeof value.clientId !== "string" || !Array.isArray(value.rules))
|
|
40
|
+
return null;
|
|
41
|
+
return {
|
|
42
|
+
issuer: value.issuer, clientId: value.clientId, clientSecret: typeof value.clientSecret === "string" && value.clientSecret !== "" ? value.clientSecret : null,
|
|
43
|
+
label: typeof value.label === "string" && value.label !== "" ? value.label : providerLabel(value.issuer),
|
|
44
|
+
scopes: typeof value.scopes === "string" && value.scopes !== "" ? value.scopes : "openid email profile",
|
|
45
|
+
groupsClaim: typeof value.groupsClaim === "string" && value.groupsClaim !== "" ? value.groupsClaim : "groups",
|
|
46
|
+
rules: value.rules.filter((rule) => rule !== null && typeof rule === "object" && typeof rule.group === "string" && (rule.role === "operator" || rule.role === "viewer")
|
|
47
|
+
&& (rule.projects === "all" || (Array.isArray(rule.projects) && rule.projects.every(one => typeof one === "string")))),
|
|
48
|
+
passwords: value.passwords === "operators" ? "operators" : "everyone",
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Save what the form sent. The client secret is kept when the field is left
|
|
57
|
+
* blank; rules come as rows (group, role, projects), blank groups dropped.
|
|
58
|
+
* `projects` for a row is "all" or the project paths chosen.
|
|
59
|
+
*/
|
|
60
|
+
export function saveSsoSettings(dir, form, previous) {
|
|
61
|
+
const issuer = form.issuer.trim().replace(/\/+$/, "");
|
|
62
|
+
if (!isAddress(issuer))
|
|
63
|
+
return { ok: false, said: "The issuer is the provider's https address, like https://acme.okta.com/oauth2/default." };
|
|
64
|
+
const clientId = form.clientId.trim();
|
|
65
|
+
if (clientId === "" || clientId.length > 200)
|
|
66
|
+
return { ok: false, said: "Enter the client ID the provider gave this app." };
|
|
67
|
+
const secret = form.clientSecret.trim() === "" ? previous?.clientSecret ?? null : form.clientSecret.trim();
|
|
68
|
+
const rules = [];
|
|
69
|
+
for (const row of form.rules) {
|
|
70
|
+
const group = row.group.trim();
|
|
71
|
+
if (group === "")
|
|
72
|
+
continue;
|
|
73
|
+
if (group.length > 200)
|
|
74
|
+
return { ok: false, said: "A group name is 200 characters at most." };
|
|
75
|
+
if (row.role !== "operator" && row.role !== "viewer")
|
|
76
|
+
return { ok: false, said: "Choose Operator or Viewer for each group." };
|
|
77
|
+
const projects = row.projects.includes("all") || row.projects.length === 0 && row.role === "operator" ? "all" : row.projects;
|
|
78
|
+
rules.push({ group, role: row.role, projects });
|
|
79
|
+
}
|
|
80
|
+
if (rules.length === 0)
|
|
81
|
+
return { ok: false, said: "Add at least one group, so someone can sign in." };
|
|
82
|
+
const scopes = form.scopes.trim() || "openid email profile";
|
|
83
|
+
if (!scopes.split(/\s+/).includes("openid"))
|
|
84
|
+
return { ok: false, said: "The scopes need openid." };
|
|
85
|
+
const settings = {
|
|
86
|
+
issuer, clientId, clientSecret: secret, label: form.label.trim().slice(0, 40) || providerLabel(issuer), scopes,
|
|
87
|
+
groupsClaim: form.groupsClaim.trim() || "groups", rules, passwords: form.passwords === "operators" ? "operators" : "everyone",
|
|
88
|
+
};
|
|
89
|
+
const file = join(dir, FILE);
|
|
90
|
+
writeFileSync(file, `${JSON.stringify(settings, null, 2)}\n`, { mode: 0o600 });
|
|
91
|
+
chmodSync(file, 0o600);
|
|
92
|
+
return { ok: true, settings };
|
|
93
|
+
}
|
|
94
|
+
export function removeSsoSettings(dir) {
|
|
95
|
+
rmSync(join(dir, FILE), { force: true });
|
|
96
|
+
}
|
|
97
|
+
/** What changed, for the ledger: never the secret itself, only that it changed. */
|
|
98
|
+
export function ssoChangeWords(before, after) {
|
|
99
|
+
const rules = (settings) => settings.rules.map(rule => `${rule.group}: ${rule.role}${rule.projects === "all" ? ", all projects" : rule.projects.length === 0 ? ", no projects" : `, ${rule.projects.length} project${rule.projects.length === 1 ? "" : "s"}`}`).join("; ");
|
|
100
|
+
const words = (settings) => settings === null ? "off" : `${settings.label} (${settings.issuer}) · ${rules(settings)} · passwords: ${settings.passwords}`;
|
|
101
|
+
const secretChanged = before !== null && after !== null && before.clientSecret !== after.clientSecret ? " · client secret replaced" : "";
|
|
102
|
+
return `${words(before)} → ${words(after)}${secretChanged}`;
|
|
103
|
+
}
|
package/dist/sso-ui.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settings → Sign-in (v100): the identity provider, the groups that may sign
|
|
3
|
+
* in and what each may do, and who may still use a password. An instance
|
|
4
|
+
* operator's page; changes take their password.
|
|
5
|
+
*/
|
|
6
|
+
import type { OidcSettings } from "./oidc.js";
|
|
7
|
+
export declare const SSO_CSS: string;
|
|
8
|
+
export type SsoSettingsView = {
|
|
9
|
+
settings: OidcSettings | null;
|
|
10
|
+
/** The address to register with the provider as this app's sign-in redirect. */
|
|
11
|
+
redirect: string | null;
|
|
12
|
+
projects: {
|
|
13
|
+
path: string;
|
|
14
|
+
name: string;
|
|
15
|
+
}[];
|
|
16
|
+
/** Whether the person looking is linked to the provider already. */
|
|
17
|
+
linked: boolean;
|
|
18
|
+
};
|
|
19
|
+
export declare function ssoSettingsHtml(view: SsoSettingsView, csrf: string, notice: {
|
|
20
|
+
said?: string | null;
|
|
21
|
+
problem?: string | null;
|
|
22
|
+
}): string;
|
package/dist/sso-ui.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
const e = (value) => String(value ?? "").replace(/[&<>"']/g, c => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
2
|
+
export const SSO_CSS = `.sign-in{max-width:780px;min-width:0}.sign-in form{display:grid;gap:12px;margin:0}.sign-in label{display:grid;gap:6px}.sign-in input,.sign-in select{box-sizing:border-box;width:100%;max-width:100%;min-width:0}` +
|
|
3
|
+
`.sign-in .card{padding:16px 18px;margin:12px 0}.sign-in .card h2{font-size:1.05rem;margin:0 0 6px}.sign-in .redirect code{user-select:all;overflow-wrap:anywhere}.sign-in button{justify-self:start;min-height:44px}` +
|
|
4
|
+
`.sign-in .rules{display:grid;gap:8px}.sign-in .rule{display:grid;grid-template-columns:minmax(0,2fr) minmax(0,1fr) minmax(0,2fr);gap:8px;align-items:start}.sign-in .rule select[multiple]{min-height:5.5rem}` +
|
|
5
|
+
`.sign-in .row{display:flex;gap:12px;flex-wrap:wrap;align-items:center}.sign-in fieldset{border:0;padding:0;margin:0;display:grid;gap:6px}.sign-in fieldset label{display:flex;gap:8px;align-items:center}.sign-in fieldset input{width:auto}` +
|
|
6
|
+
`@media(max-width:600px){.sign-in .rule{grid-template-columns:1fr}.sign-in input,.sign-in select{font-size:16px}}`;
|
|
7
|
+
const password = '<label>Your Toolroll password<input type="password" name="password" autocomplete="current-password" required></label>';
|
|
8
|
+
function ruleRow(index, rule, projects) {
|
|
9
|
+
const chosen = rule === null ? [] : rule.projects === "all" ? ["all"] : rule.projects;
|
|
10
|
+
return `<div class="rule"><label>Group<input type="text" name="group-${index}" value="${e(rule?.group ?? "")}" placeholder="${index === 0 ? "for example: engineering" : ""}" autocomplete="off"></label>` +
|
|
11
|
+
`<label>Role<select name="role-${index}"><option value="operator"${rule?.role === "operator" ? " selected" : ""}>Operator</option><option value="viewer"${rule?.role === "viewer" ? " selected" : ""}>Viewer</option></select></label>` +
|
|
12
|
+
`<label>Projects<select name="projects-${index}" multiple><option value="all"${chosen.includes("all") ? " selected" : ""}>All projects</option>${projects.map(one => `<option value="${e(one.path)}"${chosen.includes(one.path) ? " selected" : ""}>${e(one.name)}</option>`).join("")}</select></label></div>`;
|
|
13
|
+
}
|
|
14
|
+
export function ssoSettingsHtml(view, csrf, notice) {
|
|
15
|
+
const s = view.settings;
|
|
16
|
+
const hidden = (fields) => Object.entries(fields).map(([k, v]) => `<input type="hidden" name="${e(k)}" value="${e(v)}">`).join("");
|
|
17
|
+
const rows = [...(s?.rules ?? []), null, null].slice(0, Math.max(3, (s?.rules.length ?? 0) + 1)).map((rule, index) => ruleRow(index, rule, view.projects)).join("");
|
|
18
|
+
const note = notice.problem ? `<p class="problem" role="alert">${e(notice.problem)}</p>` : notice.said ? `<p role="status">${e(notice.said)}</p>` : "";
|
|
19
|
+
const redirect = view.redirect === null
|
|
20
|
+
? `<p class="meta">Open this page from the address people use (your https address) to see the redirect address to register.</p>`
|
|
21
|
+
: `<p class="redirect">Register this redirect address with the provider: <code>${e(view.redirect)}</code></p>`;
|
|
22
|
+
const form = `<form method="post" action="/settings/sign-in">${hidden({ csrf, action: "save" })}` +
|
|
23
|
+
`<label>Provider address (issuer)<input type="url" name="issuer" value="${e(s?.issuer ?? "")}" placeholder="https://acme.okta.com/oauth2/default" required inputmode="url" autocomplete="off"></label>` +
|
|
24
|
+
`<label>Client ID<input type="text" name="client-id" value="${e(s?.clientId ?? "")}" required autocomplete="off"></label>` +
|
|
25
|
+
`<label>Client secret<input type="password" name="client-secret" autocomplete="off" placeholder="${s?.clientSecret ? "Saved. Leave blank to keep it" : "Leave blank for a public client"}"></label>` +
|
|
26
|
+
`<details><summary>More</summary><label>Button says “Sign in with …”<input type="text" name="label" value="${e(s?.label ?? "")}" placeholder="Okta"></label>` +
|
|
27
|
+
`<label>Scopes<input type="text" name="scopes" value="${e(s?.scopes ?? "openid email profile")}"></label><label>Groups claim<input type="text" name="groups-claim" value="${e(s?.groupsClaim ?? "groups")}"></label></details>` +
|
|
28
|
+
`<h2>Who may sign in</h2><p class="meta">A person's first matching group sets their role and projects, again at every sign-in. No match, no entry. Use * for everyone.</p><div class="rules">${rows}</div>` +
|
|
29
|
+
`<fieldset><legend>Passwords</legend><label><input type="radio" name="passwords" value="everyone"${s?.passwords !== "operators" ? " checked" : ""}> Everyone may still use a password</label>` +
|
|
30
|
+
`<label><input type="radio" name="passwords" value="operators"${s?.passwords === "operators" ? " checked" : ""}> Only instance operators (a way in if the provider is down)</label></fieldset>` +
|
|
31
|
+
`${password}<button>${s === null ? "Turn on sign-in with the provider" : "Save"}</button></form>`;
|
|
32
|
+
const test = s === null ? "" : `<form method="post" action="/settings/sign-in" class="row">${hidden({ csrf, action: "test" })}<button class="secondary">Check the provider</button></form>`;
|
|
33
|
+
const link = s === null ? "" : view.linked
|
|
34
|
+
? `<p class="meta">You sign in with ${e(s.label)}.</p>`
|
|
35
|
+
: `<section class="card"><h2>Your account</h2><p>Sign in with ${e(s.label)} as this account from now on.</p><p><a class="button-link" href="/login/sso?link=1">Link to ${e(s.label)}</a></p></section>`;
|
|
36
|
+
const off = s === null ? "" : `<details class="card"><summary>Turn off</summary><form method="post" action="/settings/sign-in">${hidden({ csrf, action: "remove" })}<p>People who signed in with ${e(s.label)} can't sign in until it's on again, unless they have a password.</p>${password}<button class="danger">Turn off sign-in with ${e(s.label)}</button></form></details>`;
|
|
37
|
+
return `<section class="sign-in">${note}<p>People sign in with your organisation's identity provider (Okta, Microsoft Entra, Google, anything that speaks OpenID Connect), and its groups decide what they may do.</p>` +
|
|
38
|
+
`${redirect}<section class="card">${form}</section>${test}${link}${off}</section>`;
|
|
39
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Store } from "./store.js";
|
|
2
|
+
export type StorageLine = {
|
|
3
|
+
what: string;
|
|
4
|
+
bytes: number;
|
|
5
|
+
count?: number;
|
|
6
|
+
note?: string;
|
|
7
|
+
};
|
|
8
|
+
/** Bytes a tree holds on disk (allocated blocks; links not followed). */
|
|
9
|
+
export declare function treeBytes(path: string): number;
|
|
10
|
+
export declare function storageReport(store: Store, databaseFile: string, now: Date, keepMs: number): {
|
|
11
|
+
folder: string;
|
|
12
|
+
total: number;
|
|
13
|
+
lines: StorageLine[];
|
|
14
|
+
};
|
|
15
|
+
export declare function bytesWords(bytes: number): string;
|
package/dist/storage.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where Toolroll's disk goes (`toolroll storage`): the folder
|
|
3
|
+
* beside the database, by kind, and what storage retention takes care of.
|
|
4
|
+
* Build checkouts and staged releases are the big ones; the worker removes a
|
|
5
|
+
* finished task's clean checkout two days after it was let go, and a deploy
|
|
6
|
+
* keeps its last few releases.
|
|
7
|
+
*/
|
|
8
|
+
import { lstatSync, readdirSync } from "node:fs";
|
|
9
|
+
import { basename, dirname, join } from "node:path";
|
|
10
|
+
/** Bytes a tree holds on disk (allocated blocks; links not followed). */
|
|
11
|
+
export function treeBytes(path) {
|
|
12
|
+
let total = 0;
|
|
13
|
+
const pending = [path];
|
|
14
|
+
while (pending.length > 0) {
|
|
15
|
+
const at = pending.pop();
|
|
16
|
+
let stat;
|
|
17
|
+
try {
|
|
18
|
+
stat = lstatSync(at);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
total += typeof stat.blocks === "number" ? stat.blocks * 512 : stat.size;
|
|
24
|
+
if (!stat.isDirectory())
|
|
25
|
+
continue;
|
|
26
|
+
let names = [];
|
|
27
|
+
try {
|
|
28
|
+
names = readdirSync(at);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
names = [];
|
|
32
|
+
}
|
|
33
|
+
for (const name of names)
|
|
34
|
+
pending.push(join(at, name));
|
|
35
|
+
}
|
|
36
|
+
return total;
|
|
37
|
+
}
|
|
38
|
+
export function storageReport(store, databaseFile, now, keepMs) {
|
|
39
|
+
const folder = dirname(databaseFile);
|
|
40
|
+
const base = basename(databaseFile);
|
|
41
|
+
let names = [];
|
|
42
|
+
try {
|
|
43
|
+
names = readdirSync(folder);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
names = [];
|
|
47
|
+
}
|
|
48
|
+
const sizes = new Map(names.map(name => [name, treeBytes(join(folder, name))]));
|
|
49
|
+
const take = (match) => {
|
|
50
|
+
let bytes = 0;
|
|
51
|
+
for (const [name, size] of sizes)
|
|
52
|
+
if (match(name)) {
|
|
53
|
+
bytes += size;
|
|
54
|
+
sizes.delete(name);
|
|
55
|
+
}
|
|
56
|
+
return bytes;
|
|
57
|
+
};
|
|
58
|
+
const checkouts = store.listWorktrees();
|
|
59
|
+
const due = checkouts.filter(row => row.releasedAt !== null && row.runner === null && now.getTime() - Date.parse(row.releasedAt) >= keepMs).length;
|
|
60
|
+
let staged = 0;
|
|
61
|
+
try {
|
|
62
|
+
staged = readdirSync(join(folder, "staged-upgrades")).length;
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
staged = 0;
|
|
66
|
+
}
|
|
67
|
+
const lines = [
|
|
68
|
+
{ what: "Database", bytes: take(name => name.startsWith(base)) },
|
|
69
|
+
{ what: "Build checkouts", bytes: take(name => name === "worktrees"), count: checkouts.length,
|
|
70
|
+
note: due === 0 ? "none due for removal" : `${due} let go over ${Math.round(keepMs / 86_400_000)} days ago: removed on the next pass if their task is finished and they're clean` },
|
|
71
|
+
{ what: "Staged releases", bytes: take(name => name === "staged-upgrades"), count: staged, note: "a deploy keeps the release it installed, the one before and the newest few" },
|
|
72
|
+
{ what: "Evidence", bytes: take(name => name === "evidence"), note: "the audit record of every run; kept" },
|
|
73
|
+
{ what: "Backups", bytes: take(name => name === "backups") },
|
|
74
|
+
];
|
|
75
|
+
lines.push({ what: "Everything else", bytes: [...sizes.values()].reduce((sum, one) => sum + one, 0) });
|
|
76
|
+
return { folder, total: lines.reduce((sum, one) => sum + one.bytes, 0), lines };
|
|
77
|
+
}
|
|
78
|
+
export function bytesWords(bytes) {
|
|
79
|
+
if (bytes >= 1024 ** 3)
|
|
80
|
+
return `${(bytes / 1024 ** 3).toFixed(1)} GB`;
|
|
81
|
+
if (bytes >= 1024 ** 2)
|
|
82
|
+
return `${Math.round(bytes / 1024 ** 2)} MB`;
|
|
83
|
+
return `${Math.round(bytes / 1024)} KB`;
|
|
84
|
+
}
|