@oh-my-pi/pi-coding-agent 15.10.10 → 15.10.12
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/CHANGELOG.md +142 -7
- package/dist/cli.js +23108 -0
- package/dist/tokenizers.linux-x64-gnu-xcjh3jwk.node +0 -0
- package/dist/types/async/job-manager.d.ts +18 -0
- package/dist/types/cli/args.d.ts +2 -1
- package/dist/types/cli/dry-balance-cli.d.ts +1 -1
- package/dist/types/cli/gallery-cli.d.ts +1 -1
- package/dist/types/cli/gallery-fixtures/types.d.ts +1 -1
- package/dist/types/cli/usage-cli.d.ts +72 -0
- package/dist/types/cli-commands.d.ts +12 -0
- package/dist/types/commands/launch.d.ts +5 -1
- package/dist/types/commands/read.d.ts +1 -1
- package/dist/types/commands/usage.d.ts +25 -0
- package/dist/types/config/api-key-resolver.d.ts +3 -0
- package/dist/types/config/append-only-context-mode.d.ts +2 -1
- package/dist/types/config/model-discovery.d.ts +55 -0
- package/dist/types/config/model-registry.d.ts +8 -219
- package/dist/types/config/model-resolver.d.ts +34 -10
- package/dist/types/config/model-roles.d.ts +28 -0
- package/dist/types/config/models-config-schema.d.ts +523 -42
- package/dist/types/config/models-config.d.ts +385 -0
- package/dist/types/config/settings-schema.d.ts +41 -8
- package/dist/types/config/settings.d.ts +8 -1
- package/dist/types/debug/log-viewer.d.ts +1 -1
- package/dist/types/debug/raw-sse.d.ts +1 -1
- package/dist/types/edit/hashline/noop-loop-guard.d.ts +72 -0
- package/dist/types/eval/backend.d.ts +0 -2
- package/dist/types/eval/idle-timeout.d.ts +0 -4
- package/dist/types/eval/js/shared/rewrite-imports.d.ts +6 -6
- package/dist/types/eval/py/executor.d.ts +5 -0
- package/dist/types/eval/py/kernel.d.ts +6 -1
- package/dist/types/eval/py/runtime.d.ts +9 -0
- package/dist/types/exec/bash-executor.d.ts +2 -0
- package/dist/types/export/html/template.generated.d.ts +1 -1
- package/dist/types/extensibility/extensions/runner.d.ts +3 -2
- package/dist/types/extensibility/extensions/types.d.ts +6 -3
- package/dist/types/hindsight/mental-models.d.ts +17 -8
- package/dist/types/internal-urls/artifact-protocol.d.ts +2 -2
- package/dist/types/internal-urls/types.d.ts +1 -1
- package/dist/types/lsp/edits.d.ts +9 -0
- package/dist/types/lsp/index.d.ts +2 -2
- package/dist/types/lsp/types.d.ts +2 -0
- package/dist/types/lsp/utils.d.ts +3 -0
- package/dist/types/mcp/json-rpc.d.ts +5 -0
- package/dist/types/memory-backend/index.d.ts +1 -0
- package/dist/types/memory-backend/runtime.d.ts +4 -0
- package/dist/types/memory-backend/types.d.ts +66 -1
- package/dist/types/mnemopi/state.d.ts +11 -1
- package/dist/types/modes/components/agent-dashboard.d.ts +1 -1
- package/dist/types/modes/components/assistant-message.d.ts +3 -1
- package/dist/types/modes/components/bash-execution.d.ts +1 -1
- package/dist/types/modes/components/copy-selector.d.ts +1 -1
- package/dist/types/modes/components/dynamic-border.d.ts +1 -1
- package/dist/types/modes/components/extensions/extension-dashboard.d.ts +1 -1
- package/dist/types/modes/components/extensions/extension-list.d.ts +1 -1
- package/dist/types/modes/components/extensions/inspector-panel.d.ts +1 -1
- package/dist/types/modes/components/footer.d.ts +1 -1
- package/dist/types/modes/components/hook-editor.d.ts +5 -0
- package/dist/types/modes/components/hook-input.d.ts +4 -0
- package/dist/types/modes/components/hook-selector.d.ts +1 -1
- package/dist/types/modes/components/model-selector.d.ts +1 -1
- package/dist/types/modes/components/plan-review-overlay.d.ts +1 -1
- package/dist/types/modes/components/session-observer-overlay.d.ts +1 -1
- package/dist/types/modes/components/session-selector.d.ts +1 -1
- package/dist/types/modes/components/status-line/component.d.ts +1 -1
- package/dist/types/modes/components/tiny-title-download-progress.d.ts +1 -1
- package/dist/types/modes/components/transcript-container.d.ts +25 -6
- package/dist/types/modes/components/tree-selector.d.ts +1 -1
- package/dist/types/modes/components/user-message-selector.d.ts +1 -1
- package/dist/types/modes/components/user-message.d.ts +2 -1
- package/dist/types/modes/components/visual-truncate.d.ts +1 -1
- package/dist/types/modes/components/welcome.d.ts +19 -3
- package/dist/types/modes/controllers/mcp-command-controller.d.ts +1 -1
- package/dist/types/modes/controllers/streaming-reveal.d.ts +1 -1
- package/dist/types/modes/index.d.ts +3 -3
- package/dist/types/modes/interactive-mode.d.ts +8 -3
- package/dist/types/modes/oauth-manual-input.d.ts +7 -0
- package/dist/types/modes/rpc/rpc-client.d.ts +39 -2
- package/dist/types/modes/rpc/rpc-mode.d.ts +31 -2
- package/dist/types/modes/rpc/rpc-subagents.d.ts +24 -0
- package/dist/types/modes/rpc/rpc-types.d.ts +75 -1
- package/dist/types/modes/setup-wizard/index.d.ts +5 -1
- package/dist/types/modes/setup-wizard/lazy.d.ts +2 -0
- package/dist/types/modes/setup-wizard/scenes/sign-in.d.ts +1 -1
- package/dist/types/modes/setup-wizard/scenes/types.d.ts +1 -1
- package/dist/types/modes/setup-wizard/scenes/web-search.d.ts +1 -1
- package/dist/types/modes/setup-wizard/wizard-overlay.d.ts +1 -1
- package/dist/types/modes/types.d.ts +4 -1
- package/dist/types/secrets/index.d.ts +1 -1
- package/dist/types/secrets/obfuscator.d.ts +8 -2
- package/dist/types/session/agent-session.d.ts +15 -3
- package/dist/types/session/auth-broker-config.d.ts +4 -0
- package/dist/types/session/session-manager.d.ts +1 -1
- package/dist/types/session/streaming-output.d.ts +23 -0
- package/dist/types/slash-commands/acp-builtins.d.ts +16 -0
- package/dist/types/slash-commands/builtin-registry.d.ts +1 -0
- package/dist/types/slash-commands/helpers/stats-dashboard.d.ts +13 -0
- package/dist/types/slash-commands/types.d.ts +1 -1
- package/dist/types/ssh/connection-manager.d.ts +8 -0
- package/dist/types/system-prompt.d.ts +2 -0
- package/dist/types/task/executor.d.ts +1 -0
- package/dist/types/task/index.d.ts +2 -2
- package/dist/types/task/parallel.d.ts +2 -2
- package/dist/types/task/types.d.ts +8 -0
- package/dist/types/task/worktree.d.ts +2 -0
- package/dist/types/thinking.d.ts +4 -0
- package/dist/types/tiny/title-client.d.ts +11 -0
- package/dist/types/tiny/title-protocol.d.ts +1 -0
- package/dist/types/tools/ask.d.ts +4 -0
- package/dist/types/tools/conflict-detect.d.ts +16 -0
- package/dist/types/tools/github-cache.d.ts +7 -0
- package/dist/types/tools/index.d.ts +6 -0
- package/dist/types/tools/sqlite-reader.d.ts +3 -0
- package/dist/types/tui/output-block.d.ts +3 -3
- package/dist/types/utils/changelog.d.ts +8 -0
- package/dist/types/utils/git.d.ts +15 -2
- package/dist/types/utils/title-generator.d.ts +3 -2
- package/dist/types/web/scrapers/readthedocs.d.ts +3 -0
- package/dist/types/web/scrapers/types.d.ts +12 -0
- package/dist/types/web/search/providers/codex.d.ts +1 -1
- package/dist/types/web/search/providers/gemini.d.ts +1 -1
- package/examples/extensions/tools.ts +5 -4
- package/package.json +14 -11
- package/scripts/build-binary.ts +18 -23
- package/scripts/bundle-dist.ts +81 -0
- package/scripts/{dev-launch → omp} +1 -1
- package/scripts/{dev-launch-preload.ts → omp.ts} +1 -1
- package/src/async/job-manager.ts +57 -3
- package/src/auto-thinking/classifier.ts +1 -0
- package/src/autoresearch/dashboard.ts +1 -1
- package/src/autoresearch/prompt-setup.md +6 -6
- package/src/autoresearch/prompt.md +6 -6
- package/src/capability/fs.ts +10 -0
- package/src/cli/args.ts +4 -1
- package/src/cli/auth-gateway-cli.ts +1 -3
- package/src/cli/dry-balance-cli.ts +1 -1
- package/src/cli/gallery-cli.ts +1 -1
- package/src/cli/gallery-fixtures/fs.ts +1 -1
- package/src/cli/gallery-fixtures/types.ts +5 -1
- package/src/cli/list-models.ts +2 -1
- package/src/cli/usage-cli.ts +603 -0
- package/src/cli-commands.ts +30 -0
- package/src/cli.ts +76 -13
- package/src/commands/complete.ts +1 -1
- package/src/commands/launch.ts +5 -1
- package/src/commands/read.ts +6 -3
- package/src/commands/usage.ts +35 -0
- package/src/commit/agentic/agent.ts +1 -1
- package/src/commit/model-selection.ts +4 -3
- package/src/config/api-key-resolver.ts +8 -6
- package/src/config/append-only-context-mode.ts +6 -12
- package/src/config/model-discovery.ts +554 -0
- package/src/config/model-registry.ts +320 -1041
- package/src/config/model-resolver.ts +173 -156
- package/src/config/model-roles.ts +74 -0
- package/src/config/models-config-schema.ts +57 -8
- package/src/config/models-config.ts +129 -0
- package/src/config/settings-schema.ts +61 -19
- package/src/config/settings.ts +98 -4
- package/src/dap/client.ts +124 -37
- package/src/dap/session.ts +259 -158
- package/src/debug/log-viewer.ts +1 -1
- package/src/debug/raw-sse.ts +1 -1
- package/src/edit/diff.ts +47 -3
- package/src/edit/hashline/block-resolver.ts +20 -1
- package/src/edit/hashline/diff.ts +36 -1
- package/src/edit/hashline/execute.ts +47 -4
- package/src/edit/hashline/noop-loop-guard.ts +99 -0
- package/src/edit/index.ts +16 -1
- package/src/edit/modes/patch.ts +52 -0
- package/src/edit/modes/replace.ts +56 -22
- package/src/edit/notebook.ts +22 -2
- package/src/edit/renderer.ts +36 -10
- package/src/eval/__tests__/completion-bridge.test.ts +1 -1
- package/src/eval/backend.ts +0 -2
- package/src/eval/completion-bridge.ts +3 -1
- package/src/eval/idle-timeout.ts +2 -9
- package/src/eval/js/context-manager.ts +6 -8
- package/src/eval/js/executor.ts +6 -2
- package/src/eval/js/index.ts +0 -2
- package/src/eval/js/shared/helpers.ts +5 -6
- package/src/eval/js/shared/local-module-loader.ts +1 -1
- package/src/eval/js/shared/prelude.txt +62 -1
- package/src/eval/js/shared/rewrite-imports.ts +40 -22
- package/src/eval/js/shared/runtime.ts +1 -1
- package/src/eval/py/executor.ts +29 -7
- package/src/eval/py/index.ts +6 -3
- package/src/eval/py/kernel.ts +43 -4
- package/src/eval/py/runner.py +107 -3
- package/src/eval/py/runtime.ts +37 -0
- package/src/exec/bash-executor.ts +85 -4
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +3 -1
- package/src/extensibility/extensions/get-commands-handler.ts +2 -1
- package/src/extensibility/extensions/runner.ts +6 -1
- package/src/extensibility/extensions/types.ts +6 -2
- package/src/extensibility/plugins/legacy-pi-compat.ts +20 -3
- package/src/hindsight/bank.ts +17 -2
- package/src/hindsight/mental-models.ts +59 -12
- package/src/hindsight/state.ts +6 -1
- package/src/internal-urls/artifact-protocol.ts +11 -2
- package/src/internal-urls/docs-index.generated.ts +11 -11
- package/src/internal-urls/issue-pr-protocol.ts +12 -5
- package/src/internal-urls/router.ts +1 -1
- package/src/internal-urls/types.ts +1 -1
- package/src/lib/xai-http.ts +1 -1
- package/src/lsp/client.ts +118 -38
- package/src/lsp/clients/biome-client.ts +101 -39
- package/src/lsp/edits.ts +143 -95
- package/src/lsp/index.ts +31 -22
- package/src/lsp/render.ts +1 -1
- package/src/lsp/types.ts +2 -0
- package/src/lsp/utils.ts +28 -10
- package/src/main.ts +183 -23
- package/src/mcp/json-rpc.ts +35 -5
- package/src/mcp/transports/stdio.ts +7 -1
- package/src/memories/index.ts +4 -1
- package/src/memory-backend/index.ts +1 -0
- package/src/memory-backend/local-backend.ts +9 -0
- package/src/memory-backend/off-backend.ts +9 -0
- package/src/memory-backend/runtime.ts +66 -0
- package/src/memory-backend/types.ts +81 -1
- package/src/mnemopi/backend.ts +176 -7
- package/src/mnemopi/state.ts +38 -2
- package/src/modes/acp/acp-agent.ts +119 -11
- package/src/modes/components/agent-dashboard.ts +10 -7
- package/src/modes/components/assistant-message.ts +32 -28
- package/src/modes/components/bash-execution.ts +1 -1
- package/src/modes/components/copy-selector.ts +1 -1
- package/src/modes/components/diff.ts +13 -2
- package/src/modes/components/dynamic-border.ts +12 -3
- package/src/modes/components/extensions/extension-dashboard.ts +8 -5
- package/src/modes/components/extensions/extension-list.ts +1 -1
- package/src/modes/components/extensions/inspector-panel.ts +1 -1
- package/src/modes/components/footer.ts +4 -2
- package/src/modes/components/history-search.ts +1 -1
- package/src/modes/components/hook-editor.ts +8 -0
- package/src/modes/components/hook-input.ts +8 -0
- package/src/modes/components/hook-selector.ts +2 -2
- package/src/modes/components/model-selector.ts +4 -2
- package/src/modes/components/plan-review-overlay.ts +1 -1
- package/src/modes/components/session-observer-overlay.ts +2 -2
- package/src/modes/components/session-selector.ts +1 -1
- package/src/modes/components/settings-selector.ts +5 -1
- package/src/modes/components/status-line/component.ts +119 -35
- package/src/modes/components/tiny-title-download-progress.ts +1 -1
- package/src/modes/components/transcript-container.ts +258 -53
- package/src/modes/components/tree-selector.ts +3 -3
- package/src/modes/components/user-message-selector.ts +1 -1
- package/src/modes/components/user-message.ts +17 -5
- package/src/modes/components/visual-truncate.ts +1 -1
- package/src/modes/components/welcome.ts +108 -26
- package/src/modes/controllers/command-controller.ts +11 -4
- package/src/modes/controllers/event-controller.ts +73 -4
- package/src/modes/controllers/input-controller.ts +2 -1
- package/src/modes/controllers/mcp-command-controller.ts +39 -4
- package/src/modes/controllers/selector-controller.ts +1 -1
- package/src/modes/controllers/streaming-reveal.ts +85 -18
- package/src/modes/index.ts +3 -21
- package/src/modes/interactive-mode.ts +42 -18
- package/src/modes/oauth-manual-input.ts +30 -3
- package/src/modes/rpc/rpc-client.ts +154 -3
- package/src/modes/rpc/rpc-mode.ts +97 -12
- package/src/modes/rpc/rpc-subagents.ts +265 -0
- package/src/modes/rpc/rpc-types.ts +81 -1
- package/src/modes/setup-wizard/index.ts +12 -2
- package/src/modes/setup-wizard/lazy.ts +16 -0
- package/src/modes/setup-wizard/scenes/glyph.ts +1 -1
- package/src/modes/setup-wizard/scenes/providers.ts +1 -1
- package/src/modes/setup-wizard/scenes/sign-in.ts +1 -1
- package/src/modes/setup-wizard/scenes/theme.ts +1 -1
- package/src/modes/setup-wizard/scenes/types.ts +1 -1
- package/src/modes/setup-wizard/scenes/web-search.ts +1 -1
- package/src/modes/setup-wizard/wizard-overlay.ts +1 -1
- package/src/modes/types.ts +4 -1
- package/src/prompts/agents/explore.md +2 -2
- package/src/prompts/agents/librarian.md +1 -2
- package/src/prompts/agents/oracle.md +1 -1
- package/src/prompts/agents/plan.md +5 -5
- package/src/prompts/agents/task.md +5 -5
- package/src/prompts/ci-green-request.md +5 -7
- package/src/prompts/goals/goal-budget-limit.md +2 -2
- package/src/prompts/goals/goal-continuation.md +4 -4
- package/src/prompts/goals/goal-mode-active.md +1 -1
- package/src/prompts/memories/read-path.md +1 -1
- package/src/prompts/memories/stage_one_system.md +2 -2
- package/src/prompts/review-custom-request.md +1 -1
- package/src/prompts/system/agent-creation-architect.md +2 -2
- package/src/prompts/system/auto-continue.md +1 -1
- package/src/prompts/system/background-tan-dispatch.md +1 -1
- package/src/prompts/system/btw-user.md +2 -2
- package/src/prompts/system/commit-message-system.md +13 -1
- package/src/prompts/system/custom-system-prompt.md +1 -1
- package/src/prompts/system/eager-todo.md +2 -2
- package/src/prompts/system/irc-incoming.md +1 -1
- package/src/prompts/system/manual-continue.md +1 -1
- package/src/prompts/system/omfg-user.md +3 -4
- package/src/prompts/system/orchestrate-notice.md +9 -9
- package/src/prompts/system/plan-mode-active.md +4 -4
- package/src/prompts/system/plan-mode-subagent.md +4 -5
- package/src/prompts/system/plan-mode-tool-decision-reminder.md +1 -1
- package/src/prompts/system/project-prompt.md +2 -2
- package/src/prompts/system/subagent-system-prompt.md +4 -4
- package/src/prompts/system/system-prompt.md +13 -24
- package/src/prompts/system/title-system.md +2 -2
- package/src/prompts/system/ttsr-tool-reminder.md +1 -1
- package/src/prompts/system/workflow-notice.md +1 -1
- package/src/prompts/tools/ast-edit.md +1 -1
- package/src/prompts/tools/ast-grep.md +2 -2
- package/src/prompts/tools/bash.md +5 -7
- package/src/prompts/tools/browser.md +7 -7
- package/src/prompts/tools/debug.md +1 -1
- package/src/prompts/tools/eval.md +3 -3
- package/src/prompts/tools/find.md +0 -1
- package/src/prompts/tools/github.md +8 -7
- package/src/prompts/tools/goal.md +1 -1
- package/src/prompts/tools/image-gen.md +1 -1
- package/src/prompts/tools/inspect-image-system.md +1 -1
- package/src/prompts/tools/irc.md +15 -15
- package/src/prompts/tools/lsp.md +2 -2
- package/src/prompts/tools/patch.md +2 -2
- package/src/prompts/tools/read.md +3 -4
- package/src/prompts/tools/recall.md +1 -1
- package/src/prompts/tools/reflect.md +1 -1
- package/src/prompts/tools/render-mermaid.md +2 -2
- package/src/prompts/tools/replace.md +4 -10
- package/src/prompts/tools/rewind.md +2 -2
- package/src/prompts/tools/search-tool-bm25.md +1 -9
- package/src/prompts/tools/search.md +0 -1
- package/src/prompts/tools/ssh.md +0 -4
- package/src/prompts/tools/task.md +2 -3
- package/src/prompts/tools/todo.md +1 -1
- package/src/sdk.ts +31 -11
- package/src/secrets/index.ts +8 -1
- package/src/secrets/obfuscator.ts +39 -18
- package/src/session/agent-session.ts +223 -64
- package/src/session/auth-broker-config.ts +30 -1
- package/src/session/session-manager.ts +2 -2
- package/src/session/streaming-output.ts +188 -11
- package/src/slash-commands/acp-builtins.ts +24 -0
- package/src/slash-commands/builtin-registry.ts +40 -0
- package/src/slash-commands/helpers/stats-dashboard.ts +85 -0
- package/src/slash-commands/types.ts +1 -1
- package/src/ssh/connection-manager.ts +27 -0
- package/src/system-prompt.ts +14 -0
- package/src/task/commands.ts +2 -1
- package/src/task/executor.ts +74 -65
- package/src/task/index.ts +146 -68
- package/src/task/parallel.ts +3 -3
- package/src/task/render.ts +20 -5
- package/src/task/types.ts +9 -0
- package/src/task/worktree.ts +64 -56
- package/src/thinking.ts +9 -1
- package/src/tiny/title-client.ts +60 -16
- package/src/tiny/title-protocol.ts +1 -1
- package/src/tiny/worker.ts +6 -4
- package/src/tools/archive-reader.ts +30 -2
- package/src/tools/ask.ts +104 -21
- package/src/tools/ast-edit.ts +25 -5
- package/src/tools/auto-generated-guard.ts +20 -3
- package/src/tools/bash-interactive.ts +27 -7
- package/src/tools/bash.ts +100 -18
- package/src/tools/browser/launch.ts +11 -2
- package/src/tools/browser/readable.ts +19 -2
- package/src/tools/browser/registry.ts +4 -1
- package/src/tools/browser/render.ts +2 -2
- package/src/tools/browser/tab-supervisor.ts +55 -16
- package/src/tools/conflict-detect.ts +50 -4
- package/src/tools/debug.ts +1 -1
- package/src/tools/eval-render.ts +5 -5
- package/src/tools/eval.ts +0 -2
- package/src/tools/fetch.ts +33 -10
- package/src/tools/gh-cache-invalidation.ts +63 -8
- package/src/tools/gh-renderer.ts +1 -1
- package/src/tools/gh.ts +172 -29
- package/src/tools/github-cache.ts +70 -6
- package/src/tools/image-gen.ts +14 -13
- package/src/tools/index.ts +13 -1
- package/src/tools/inspect-image.ts +1 -0
- package/src/tools/irc.ts +5 -1
- package/src/tools/job.ts +1 -1
- package/src/tools/read.ts +202 -61
- package/src/tools/render-utils.ts +3 -3
- package/src/tools/resolve.ts +1 -1
- package/src/tools/search.ts +92 -29
- package/src/tools/sqlite-reader.ts +17 -5
- package/src/tools/ssh.ts +8 -8
- package/src/tools/todo.ts +38 -8
- package/src/tools/write.ts +118 -18
- package/src/tui/output-block.ts +4 -4
- package/src/utils/changelog.ts +27 -1
- package/src/utils/commit-message-generator.ts +1 -0
- package/src/utils/file-mentions.ts +2 -1
- package/src/utils/git.ts +267 -13
- package/src/utils/title-generator.ts +24 -5
- package/src/web/scrapers/arxiv.ts +1 -1
- package/src/web/scrapers/go-pkg.ts +1 -1
- package/src/web/scrapers/iacr.ts +1 -1
- package/src/web/scrapers/readthedocs.ts +1 -1
- package/src/web/scrapers/twitter.ts +2 -1
- package/src/web/scrapers/types.ts +87 -8
- package/src/web/scrapers/wikipedia.ts +1 -1
- package/src/web/scrapers/youtube.ts +6 -1
- package/src/web/search/index.ts +1 -1
- package/src/web/search/providers/codex.ts +2 -1
- package/src/web/search/providers/gemini.ts +2 -3
- package/src/web/search/render.ts +8 -6
- package/dist/types/config/model-equivalence.d.ts +0 -24
- package/dist/types/config/model-id-affixes.d.ts +0 -12
- package/dist/types/config/model-provider-priority.d.ts +0 -1
- package/dist/types/exec/idle-timeout-watchdog.d.ts +0 -18
- package/src/config/model-equivalence.ts +0 -875
- package/src/config/model-id-affixes.ts +0 -81
- package/src/config/model-provider-priority.ts +0 -56
- package/src/exec/idle-timeout-watchdog.ts +0 -126
|
@@ -0,0 +1,603 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Usage CLI command handler.
|
|
3
|
+
*
|
|
4
|
+
* Handles `omp usage` — fetches provider usage reports for every
|
|
5
|
+
* authenticated account and prints a detailed per-account breakdown
|
|
6
|
+
* (limits, windows, reset times, plan metadata). Accounts whose
|
|
7
|
+
* credentials produced no usage report are listed too, so the output
|
|
8
|
+
* always covers the full credential pool.
|
|
9
|
+
*/
|
|
10
|
+
import type { AuthStorage, UsageLimit, UsageReport, UsageUnit } from "@oh-my-pi/pi-ai";
|
|
11
|
+
import { formatDuration, formatNumber } from "@oh-my-pi/pi-utils";
|
|
12
|
+
import chalk from "chalk";
|
|
13
|
+
import { ModelRegistry } from "../config/model-registry";
|
|
14
|
+
import { discoverAuthStorage } from "../sdk";
|
|
15
|
+
|
|
16
|
+
const BAR_WIDTH = 28;
|
|
17
|
+
|
|
18
|
+
export interface UsageCommandArgs {
|
|
19
|
+
json?: boolean;
|
|
20
|
+
provider?: string;
|
|
21
|
+
redact?: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Identity slice of a stored credential, for "every account" coverage. */
|
|
25
|
+
export interface UsageAccountIdentity {
|
|
26
|
+
provider: string;
|
|
27
|
+
type: "api_key" | "oauth";
|
|
28
|
+
email?: string;
|
|
29
|
+
accountId?: string;
|
|
30
|
+
projectId?: string;
|
|
31
|
+
enterpriseUrl?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Minimal-reveal masks for identity strings (`--redact`).
|
|
36
|
+
*
|
|
37
|
+
* Every mask shows a two-character anchor. When two identities share the
|
|
38
|
+
* anchor, the mask additionally reveals the shortest "middle-out"
|
|
39
|
+
* differentiator — the shortest substring (closest to the string's middle on
|
|
40
|
+
* ties) that no colliding identity contains — as `an*`, `ca*9*`, `ca*nb*`.
|
|
41
|
+
* Prefix growth is deliberately avoided: it leaks the start of the local
|
|
42
|
+
* part (`can.boluk@*`) when a couple of mid-string characters suffice.
|
|
43
|
+
* Duplicate strings (same account on two providers) share a mask.
|
|
44
|
+
*/
|
|
45
|
+
export function buildRedactionMap(values: Iterable<string>): Map<string, string> {
|
|
46
|
+
const unique = [...new Set(values)];
|
|
47
|
+
const map = new Map<string, string>();
|
|
48
|
+
const byAnchor = new Map<string, string[]>();
|
|
49
|
+
for (const value of unique) {
|
|
50
|
+
const anchor = value.slice(0, 2);
|
|
51
|
+
const list = byAnchor.get(anchor) ?? [];
|
|
52
|
+
list.push(value);
|
|
53
|
+
byAnchor.set(anchor, list);
|
|
54
|
+
}
|
|
55
|
+
for (const value of unique) {
|
|
56
|
+
const anchor = value.slice(0, 2);
|
|
57
|
+
const peers = (byAnchor.get(anchor) ?? []).filter(other => other !== value);
|
|
58
|
+
if (peers.length === 0) {
|
|
59
|
+
map.set(value, `${anchor}*`);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
const infix = findDistinguishingInfix(value, peers);
|
|
63
|
+
map.set(value, infix === undefined ? `${anchor}*` : `${anchor}*${infix}*`);
|
|
64
|
+
}
|
|
65
|
+
// Residual collisions (a value whose every substring also occurs in a
|
|
66
|
+
// peer gets the bare anchor mask) fall back to prefix extension.
|
|
67
|
+
const byMask = new Map<string, string[]>();
|
|
68
|
+
for (const value of unique) {
|
|
69
|
+
const mask = map.get(value)!;
|
|
70
|
+
const list = byMask.get(mask) ?? [];
|
|
71
|
+
list.push(value);
|
|
72
|
+
byMask.set(mask, list);
|
|
73
|
+
}
|
|
74
|
+
for (const collided of byMask.values()) {
|
|
75
|
+
if (collided.length < 2) continue;
|
|
76
|
+
for (const value of collided) {
|
|
77
|
+
let length = Math.min(2, value.length);
|
|
78
|
+
while (
|
|
79
|
+
length < value.length &&
|
|
80
|
+
collided.some(other => other !== value && other.startsWith(value.slice(0, length)))
|
|
81
|
+
) {
|
|
82
|
+
length++;
|
|
83
|
+
}
|
|
84
|
+
map.set(value, `${value.slice(0, length)}*`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return map;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Shortest substring of `value` (past the revealed two-char anchor) that no
|
|
92
|
+
* peer contains. Among equal-length candidates, picks the one centered
|
|
93
|
+
* closest to the middle of the string. Returns undefined when every
|
|
94
|
+
* substring also occurs in a peer (e.g. `value` is contained in a peer —
|
|
95
|
+
* that peer's own differentiator keeps the masks distinct).
|
|
96
|
+
*/
|
|
97
|
+
function findDistinguishingInfix(value: string, peers: string[]): string | undefined {
|
|
98
|
+
const start = Math.min(2, value.length);
|
|
99
|
+
const center = value.length / 2;
|
|
100
|
+
for (let length = 1; length <= value.length - start; length++) {
|
|
101
|
+
let best: { infix: string; distance: number } | undefined;
|
|
102
|
+
for (let pos = start; pos + length <= value.length; pos++) {
|
|
103
|
+
const candidate = value.slice(pos, pos + length);
|
|
104
|
+
if (peers.some(peer => peer.includes(candidate))) continue;
|
|
105
|
+
const distance = Math.abs(pos + length / 2 - center);
|
|
106
|
+
if (!best || distance < best.distance) best = { infix: candidate, distance };
|
|
107
|
+
}
|
|
108
|
+
if (best) return best.infix;
|
|
109
|
+
}
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Every identity string the output could surface — input for {@link buildRedactionMap}. */
|
|
114
|
+
function collectIdentityStrings(reports: UsageReport[], accounts: UsageAccountIdentity[]): string[] {
|
|
115
|
+
const values: string[] = [];
|
|
116
|
+
const add = (value: unknown): void => {
|
|
117
|
+
if (typeof value === "string" && value) values.push(value);
|
|
118
|
+
};
|
|
119
|
+
for (const report of reports) {
|
|
120
|
+
const meta = report.metadata ?? {};
|
|
121
|
+
add(meta.email);
|
|
122
|
+
add(meta.accountId);
|
|
123
|
+
add(meta.projectId);
|
|
124
|
+
add(meta.orgId);
|
|
125
|
+
for (const limit of report.limits) {
|
|
126
|
+
add(limit.scope.accountId);
|
|
127
|
+
add(limit.scope.projectId);
|
|
128
|
+
add(limit.scope.orgId);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
for (const account of accounts) {
|
|
132
|
+
add(account.email);
|
|
133
|
+
add(account.accountId);
|
|
134
|
+
add(account.projectId);
|
|
135
|
+
add(account.enterpriseUrl);
|
|
136
|
+
}
|
|
137
|
+
return values;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
type LimitStatus = NonNullable<UsageLimit["status"]>;
|
|
141
|
+
|
|
142
|
+
function resolveFraction(limit: UsageLimit): number | undefined {
|
|
143
|
+
const amount = limit.amount;
|
|
144
|
+
if (amount.usedFraction !== undefined) return amount.usedFraction;
|
|
145
|
+
if (amount.used !== undefined && amount.limit !== undefined && amount.limit > 0) {
|
|
146
|
+
return amount.used / amount.limit;
|
|
147
|
+
}
|
|
148
|
+
if (amount.unit === "percent" && amount.used !== undefined) return amount.used / 100;
|
|
149
|
+
if (amount.remainingFraction !== undefined) return Math.max(0, 1 - amount.remainingFraction);
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function resolveStatus(limit: UsageLimit): LimitStatus {
|
|
154
|
+
if (limit.status && limit.status !== "unknown") return limit.status;
|
|
155
|
+
const fraction = resolveFraction(limit);
|
|
156
|
+
if (fraction === undefined) return "unknown";
|
|
157
|
+
if (fraction >= 1) return "exhausted";
|
|
158
|
+
if (fraction >= 0.8) return "warning";
|
|
159
|
+
return "ok";
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const STATUS_COLOR: Record<LimitStatus, (text: string) => string> = {
|
|
163
|
+
exhausted: chalk.red,
|
|
164
|
+
warning: chalk.yellow,
|
|
165
|
+
ok: chalk.green,
|
|
166
|
+
unknown: chalk.dim,
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
/** Worst-of aggregation: exhausted > warning > ok > unknown. */
|
|
170
|
+
function aggregateStatus(limits: UsageLimit[]): LimitStatus {
|
|
171
|
+
const statuses = limits.map(resolveStatus);
|
|
172
|
+
if (statuses.includes("exhausted")) return "exhausted";
|
|
173
|
+
if (statuses.includes("warning")) return "warning";
|
|
174
|
+
if (statuses.includes("ok")) return "ok";
|
|
175
|
+
return "unknown";
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function formatProviderName(provider: string): string {
|
|
179
|
+
return provider
|
|
180
|
+
.split(/[-_]/g)
|
|
181
|
+
.map(part => (part ? part[0].toUpperCase() + part.slice(1) : ""))
|
|
182
|
+
.join(" ");
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function formatUnitValue(value: number, unit: UsageUnit): string {
|
|
186
|
+
if (unit === "usd") return `$${value.toFixed(2)}`;
|
|
187
|
+
return formatNumber(value);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const UNIT_SUFFIX: Record<UsageUnit, string> = {
|
|
191
|
+
tokens: " tokens",
|
|
192
|
+
requests: " requests",
|
|
193
|
+
minutes: " min",
|
|
194
|
+
bytes: " bytes",
|
|
195
|
+
percent: "",
|
|
196
|
+
usd: "",
|
|
197
|
+
unknown: "",
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
function describeAmount(limit: UsageLimit): string {
|
|
201
|
+
const amount = limit.amount;
|
|
202
|
+
const parts: string[] = [];
|
|
203
|
+
const absoluteUnit = amount.unit !== "percent" && amount.unit !== "unknown";
|
|
204
|
+
if (absoluteUnit && amount.used !== undefined && amount.limit !== undefined) {
|
|
205
|
+
parts.push(
|
|
206
|
+
`${formatUnitValue(amount.used, amount.unit)} / ${formatUnitValue(amount.limit, amount.unit)}${UNIT_SUFFIX[amount.unit]}`,
|
|
207
|
+
);
|
|
208
|
+
} else if (absoluteUnit && amount.remaining !== undefined) {
|
|
209
|
+
parts.push(`${formatUnitValue(amount.remaining, amount.unit)}${UNIT_SUFFIX[amount.unit]} left`);
|
|
210
|
+
}
|
|
211
|
+
const fraction = resolveFraction(limit);
|
|
212
|
+
if (fraction !== undefined) {
|
|
213
|
+
parts.push(`${(fraction * 100).toFixed(1)}% used`);
|
|
214
|
+
} else if (amount.remainingFraction !== undefined) {
|
|
215
|
+
parts.push(`${(amount.remainingFraction * 100).toFixed(1)}% left`);
|
|
216
|
+
}
|
|
217
|
+
if (parts.length === 0) parts.push("no data");
|
|
218
|
+
return parts.join(" · ");
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function renderBar(limit: UsageLimit): string {
|
|
222
|
+
const fraction = resolveFraction(limit);
|
|
223
|
+
if (fraction === undefined) return chalk.dim("·".repeat(BAR_WIDTH));
|
|
224
|
+
const clamped = Math.min(Math.max(fraction, 0), 1);
|
|
225
|
+
const filled = Math.round(clamped * BAR_WIDTH);
|
|
226
|
+
const color = STATUS_COLOR[resolveStatus(limit)];
|
|
227
|
+
return color("█".repeat(filled)) + chalk.dim("░".repeat(BAR_WIDTH - filled));
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** Append the window label when the limit label doesn't already carry it. */
|
|
231
|
+
function limitTitle(limit: UsageLimit): string {
|
|
232
|
+
let label = limit.label;
|
|
233
|
+
const tier = limit.scope.tier;
|
|
234
|
+
if (tier && !label.toLowerCase().includes(tier.toLowerCase())) label = `${label} (${tier})`;
|
|
235
|
+
const windowLabel = limit.window?.label ?? limit.scope.windowId;
|
|
236
|
+
if (!windowLabel) return label;
|
|
237
|
+
if (windowLabel.toLowerCase() === "quota window") return label;
|
|
238
|
+
if (label.toLowerCase().includes(windowLabel.toLowerCase())) return label;
|
|
239
|
+
return `${label} (${windowLabel})`;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function reportAccountLabel(report: UsageReport, index: number): string {
|
|
243
|
+
const meta = report.metadata ?? {};
|
|
244
|
+
for (const key of ["email", "accountId", "projectId"] as const) {
|
|
245
|
+
const value = meta[key];
|
|
246
|
+
if (typeof value === "string" && value) return value;
|
|
247
|
+
}
|
|
248
|
+
for (const limit of report.limits) {
|
|
249
|
+
const scoped = limit.scope.accountId ?? limit.scope.projectId;
|
|
250
|
+
if (scoped) return scoped;
|
|
251
|
+
}
|
|
252
|
+
return `account ${index + 1}`;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** Lowercased identity strings a report can be attributed to. */
|
|
256
|
+
function reportIdentifiers(report: UsageReport): Set<string> {
|
|
257
|
+
const ids = new Set<string>();
|
|
258
|
+
const add = (value: unknown): void => {
|
|
259
|
+
if (typeof value === "string" && value) ids.add(value.toLowerCase());
|
|
260
|
+
};
|
|
261
|
+
const meta = report.metadata ?? {};
|
|
262
|
+
add(meta.email);
|
|
263
|
+
add(meta.accountId);
|
|
264
|
+
add(meta.projectId);
|
|
265
|
+
add(meta.orgId);
|
|
266
|
+
for (const limit of report.limits) {
|
|
267
|
+
add(limit.scope.accountId);
|
|
268
|
+
add(limit.scope.projectId);
|
|
269
|
+
add(limit.scope.orgId);
|
|
270
|
+
}
|
|
271
|
+
return ids;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Stored credentials that no usage report could be attributed to.
|
|
276
|
+
*
|
|
277
|
+
* Conservative on purpose: when a provider's reports carry no identity at
|
|
278
|
+
* all (or the credential is an API key alongside existing reports), we
|
|
279
|
+
* can't attribute, so we don't claim the account is missing.
|
|
280
|
+
*/
|
|
281
|
+
export function collectUnreportedAccounts(
|
|
282
|
+
reports: UsageReport[],
|
|
283
|
+
accounts: UsageAccountIdentity[],
|
|
284
|
+
): UsageAccountIdentity[] {
|
|
285
|
+
const byProvider = new Map<string, UsageReport[]>();
|
|
286
|
+
for (const report of reports) {
|
|
287
|
+
const list = byProvider.get(report.provider) ?? [];
|
|
288
|
+
list.push(report);
|
|
289
|
+
byProvider.set(report.provider, list);
|
|
290
|
+
}
|
|
291
|
+
return accounts.filter(account => {
|
|
292
|
+
const providerReports = byProvider.get(account.provider) ?? [];
|
|
293
|
+
if (providerReports.length === 0) return true;
|
|
294
|
+
if (account.type === "api_key") return false;
|
|
295
|
+
const ids = [account.email, account.accountId, account.projectId]
|
|
296
|
+
.filter((value): value is string => typeof value === "string" && value.length > 0)
|
|
297
|
+
.map(value => value.toLowerCase());
|
|
298
|
+
if (ids.length === 0) return false;
|
|
299
|
+
const reported = new Set<string>();
|
|
300
|
+
let anyIdentified = false;
|
|
301
|
+
for (const report of providerReports) {
|
|
302
|
+
const identifiers = reportIdentifiers(report);
|
|
303
|
+
if (identifiers.size > 0) anyIdentified = true;
|
|
304
|
+
for (const id of identifiers) reported.add(id);
|
|
305
|
+
}
|
|
306
|
+
if (!anyIdentified) return false;
|
|
307
|
+
return !ids.some(id => reported.has(id));
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function accountIdentityLabel(account: UsageAccountIdentity): string {
|
|
312
|
+
if (account.type === "api_key") return "API key";
|
|
313
|
+
return account.email ?? account.accountId ?? account.projectId ?? account.enterpriseUrl ?? "OAuth account";
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function formatAccountHeader(
|
|
317
|
+
report: UsageReport,
|
|
318
|
+
index: number,
|
|
319
|
+
nowMs: number,
|
|
320
|
+
redaction?: Map<string, string>,
|
|
321
|
+
): string {
|
|
322
|
+
const status = aggregateStatus(report.limits);
|
|
323
|
+
const icon = STATUS_COLOR[status]("●");
|
|
324
|
+
const label = reportAccountLabel(report, index);
|
|
325
|
+
let header = `${icon} ${chalk.bold(redaction?.get(label) ?? label)}`;
|
|
326
|
+
const planType = report.metadata?.planType;
|
|
327
|
+
if (typeof planType === "string" && planType) header += chalk.dim(` · plan: ${planType}`);
|
|
328
|
+
if (report.fetchedAt && nowMs - report.fetchedAt > 90_000) {
|
|
329
|
+
header += chalk.dim(` · fetched ${formatDuration(nowMs - report.fetchedAt)} ago`);
|
|
330
|
+
}
|
|
331
|
+
return header;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function formatLimitLine(limit: UsageLimit, labelWidth: number, nowMs: number): string[] {
|
|
335
|
+
const status = resolveStatus(limit);
|
|
336
|
+
const title = limitTitle(limit);
|
|
337
|
+
const padded = title.padEnd(labelWidth);
|
|
338
|
+
const details: string[] = [describeAmount(limit)];
|
|
339
|
+
const resetsAt = limit.window?.resetsAt;
|
|
340
|
+
if (resetsAt !== undefined && resetsAt > nowMs) {
|
|
341
|
+
details.push(`resets in ${formatDuration(resetsAt - nowMs)}`);
|
|
342
|
+
}
|
|
343
|
+
const lines = [
|
|
344
|
+
` ${STATUS_COLOR[status]("●")} ${padded} ${renderBar(limit)} ${chalk.dim(details.join(" · "))}`,
|
|
345
|
+
];
|
|
346
|
+
if (limit.notes && limit.notes.length > 0) {
|
|
347
|
+
lines.push(` ${chalk.dim(limit.notes.join(" · "))}`);
|
|
348
|
+
}
|
|
349
|
+
return lines;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/** Per-window capacity stat: how much account quota is burned and left. */
|
|
353
|
+
export interface ProviderWindowStat {
|
|
354
|
+
/** Compact window label, e.g. "5h", "7d". */
|
|
355
|
+
window: string;
|
|
356
|
+
durationMs?: number;
|
|
357
|
+
/** Accounts reporting a limit in this window. */
|
|
358
|
+
accounts: number;
|
|
359
|
+
/** Sum of each account's binding used fraction — accounts' worth of quota burned. */
|
|
360
|
+
usedAccounts: number;
|
|
361
|
+
/** Accounts' worth of quota still available across reporting accounts. */
|
|
362
|
+
remainingAccounts: number;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Aggregate one provider's reports into per-window quota capacity stats.
|
|
367
|
+
*
|
|
368
|
+
* Limits are bucketed by window duration (5h, 7d, ...). Within a bucket each
|
|
369
|
+
* account contributes its single highest used fraction — when an account has
|
|
370
|
+
* several meters on the same window (tiered/metered limits), the most-burned
|
|
371
|
+
* one is what binds.
|
|
372
|
+
*/
|
|
373
|
+
export function computeProviderWindowStats(reports: UsageReport[]): ProviderWindowStat[] {
|
|
374
|
+
const buckets = new Map<string, { window: string; durationMs?: number; fractions: number[] }>();
|
|
375
|
+
for (const report of reports) {
|
|
376
|
+
const accountMax = new Map<string, number>();
|
|
377
|
+
for (const limit of report.limits) {
|
|
378
|
+
const fraction = resolveFraction(limit);
|
|
379
|
+
if (fraction === undefined) continue;
|
|
380
|
+
const durationMs = limit.window?.durationMs;
|
|
381
|
+
const key =
|
|
382
|
+
durationMs !== undefined ? `d:${durationMs}` : (limit.scope.windowId ?? limit.window?.label ?? limit.label);
|
|
383
|
+
const previous = accountMax.get(key);
|
|
384
|
+
if (previous === undefined || fraction > previous) accountMax.set(key, fraction);
|
|
385
|
+
if (!buckets.has(key)) {
|
|
386
|
+
const window =
|
|
387
|
+
durationMs !== undefined
|
|
388
|
+
? formatDuration(durationMs)
|
|
389
|
+
: (limit.window?.label ?? limit.scope.windowId ?? limit.label);
|
|
390
|
+
buckets.set(key, { window, durationMs, fractions: [] });
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
for (const [key, fraction] of accountMax) buckets.get(key)!.fractions.push(fraction);
|
|
394
|
+
}
|
|
395
|
+
return [...buckets.values()]
|
|
396
|
+
.sort((a, b) => (a.durationMs ?? Number.POSITIVE_INFINITY) - (b.durationMs ?? Number.POSITIVE_INFINITY))
|
|
397
|
+
.map(bucket => {
|
|
398
|
+
const usedAccounts = bucket.fractions.reduce((sum, fraction) => sum + fraction, 0);
|
|
399
|
+
return {
|
|
400
|
+
window: bucket.window,
|
|
401
|
+
durationMs: bucket.durationMs,
|
|
402
|
+
accounts: bucket.fractions.length,
|
|
403
|
+
usedAccounts,
|
|
404
|
+
remainingAccounts: Math.max(0, bucket.fractions.length - usedAccounts),
|
|
405
|
+
};
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Render the full text breakdown: per provider, per account, every limit
|
|
411
|
+
* with a bar, amounts, and reset times; unattributed credentials trail
|
|
412
|
+
* each provider section as "no usage data" rows.
|
|
413
|
+
*/
|
|
414
|
+
export function formatUsageBreakdown(
|
|
415
|
+
reports: UsageReport[],
|
|
416
|
+
accounts: UsageAccountIdentity[],
|
|
417
|
+
nowMs: number,
|
|
418
|
+
redaction?: Map<string, string>,
|
|
419
|
+
): string {
|
|
420
|
+
const reportsByProvider = new Map<string, UsageReport[]>();
|
|
421
|
+
for (const report of reports) {
|
|
422
|
+
const list = reportsByProvider.get(report.provider) ?? [];
|
|
423
|
+
list.push(report);
|
|
424
|
+
reportsByProvider.set(report.provider, list);
|
|
425
|
+
}
|
|
426
|
+
const unreported = collectUnreportedAccounts(reports, accounts);
|
|
427
|
+
const unreportedByProvider = new Map<string, UsageAccountIdentity[]>();
|
|
428
|
+
for (const account of unreported) {
|
|
429
|
+
const list = unreportedByProvider.get(account.provider) ?? [];
|
|
430
|
+
list.push(account);
|
|
431
|
+
unreportedByProvider.set(account.provider, list);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const providers = [...new Set([...reportsByProvider.keys(), ...unreportedByProvider.keys()])].sort((a, b) =>
|
|
435
|
+
a.localeCompare(b),
|
|
436
|
+
);
|
|
437
|
+
|
|
438
|
+
const lines: string[] = [];
|
|
439
|
+
const latestFetchedAt = Math.max(0, ...reports.map(report => report.fetchedAt ?? 0));
|
|
440
|
+
const headerSuffix = latestFetchedAt ? chalk.dim(` · fetched ${formatDuration(nowMs - latestFetchedAt)} ago`) : "";
|
|
441
|
+
lines.push(`${chalk.bold("Usage")}${headerSuffix}`);
|
|
442
|
+
|
|
443
|
+
for (const provider of providers) {
|
|
444
|
+
const providerReports = reportsByProvider.get(provider) ?? [];
|
|
445
|
+
const providerUnreported = unreportedByProvider.get(provider) ?? [];
|
|
446
|
+
const accountCount = providerReports.length + providerUnreported.length;
|
|
447
|
+
lines.push("");
|
|
448
|
+
lines.push(
|
|
449
|
+
`${chalk.bold.cyan(formatProviderName(provider))} ${chalk.dim(`— ${accountCount} ${accountCount === 1 ? "account" : "accounts"}`)}`,
|
|
450
|
+
);
|
|
451
|
+
|
|
452
|
+
const labelWidth = providerReports
|
|
453
|
+
.flatMap(report => report.limits)
|
|
454
|
+
.reduce((max, limit) => Math.max(max, limitTitle(limit).length), 0);
|
|
455
|
+
|
|
456
|
+
providerReports.forEach((report, index) => {
|
|
457
|
+
lines.push(` ${formatAccountHeader(report, index, nowMs, redaction)}`);
|
|
458
|
+
if (report.limits.length === 0) {
|
|
459
|
+
lines.push(` ${chalk.dim("no limits reported")}`);
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
for (const limit of report.limits) {
|
|
463
|
+
lines.push(...formatLimitLine(limit, labelWidth, nowMs));
|
|
464
|
+
}
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
for (const account of providerUnreported) {
|
|
468
|
+
const label = accountIdentityLabel(account);
|
|
469
|
+
lines.push(` ${chalk.dim("○")} ${chalk.dim(`${redaction?.get(label) ?? label} — no usage data`)}`);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const stats = computeProviderWindowStats(providerReports);
|
|
473
|
+
if (stats.length > 0) {
|
|
474
|
+
const parts = stats.map(
|
|
475
|
+
stat =>
|
|
476
|
+
`${stat.window} → ${stat.usedAccounts.toFixed(2)}/${stat.accounts} ${stat.accounts === 1 ? "account" : "accounts"} used (${stat.remainingAccounts.toFixed(2)}× quota left)`,
|
|
477
|
+
);
|
|
478
|
+
lines.push(` ${chalk.dim(`capacity: ${parts.join(" · ")}`)}`);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
return lines.join("\n");
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function collectStoredAccounts(authStorage: AuthStorage): UsageAccountIdentity[] {
|
|
486
|
+
const accounts: UsageAccountIdentity[] = [];
|
|
487
|
+
const all = authStorage.getAll();
|
|
488
|
+
for (const provider in all) {
|
|
489
|
+
const entry = all[provider];
|
|
490
|
+
const credentials = Array.isArray(entry) ? entry : [entry];
|
|
491
|
+
for (const credential of credentials) {
|
|
492
|
+
if (credential.type === "oauth") {
|
|
493
|
+
accounts.push({
|
|
494
|
+
provider,
|
|
495
|
+
type: "oauth",
|
|
496
|
+
email: credential.email,
|
|
497
|
+
accountId: credential.accountId,
|
|
498
|
+
projectId: credential.projectId,
|
|
499
|
+
enterpriseUrl: credential.enterpriseUrl,
|
|
500
|
+
});
|
|
501
|
+
} else {
|
|
502
|
+
accounts.push({ provider, type: "api_key" });
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
return accounts;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/** Apply a redaction mask to an optional identity field. */
|
|
510
|
+
function maskIdentity(redaction: Map<string, string>, value: string | undefined): string | undefined {
|
|
511
|
+
return value === undefined ? undefined : (redaction.get(value) ?? value);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const IDENTITY_METADATA_KEYS = ["email", "accountId", "projectId", "orgId"] as const;
|
|
515
|
+
|
|
516
|
+
/** Mask identity fields in a raw-stripped report for `--redact --json`. */
|
|
517
|
+
function redactReportForJson(
|
|
518
|
+
report: Omit<UsageReport, "raw">,
|
|
519
|
+
redaction: Map<string, string>,
|
|
520
|
+
): Omit<UsageReport, "raw"> {
|
|
521
|
+
let metadata = report.metadata;
|
|
522
|
+
if (metadata) {
|
|
523
|
+
metadata = { ...metadata };
|
|
524
|
+
for (const key of IDENTITY_METADATA_KEYS) {
|
|
525
|
+
const value = metadata[key];
|
|
526
|
+
if (typeof value === "string") metadata[key] = redaction.get(value) ?? value;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
const limits = report.limits.map(limit => ({
|
|
530
|
+
...limit,
|
|
531
|
+
scope: {
|
|
532
|
+
...limit.scope,
|
|
533
|
+
accountId: maskIdentity(redaction, limit.scope.accountId),
|
|
534
|
+
projectId: maskIdentity(redaction, limit.scope.projectId),
|
|
535
|
+
orgId: maskIdentity(redaction, limit.scope.orgId),
|
|
536
|
+
},
|
|
537
|
+
}));
|
|
538
|
+
return { ...report, metadata, limits };
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
export async function runUsageCommand(cmd: UsageCommandArgs): Promise<void> {
|
|
542
|
+
const authStorage = await discoverAuthStorage();
|
|
543
|
+
try {
|
|
544
|
+
const modelRegistry = new ModelRegistry(authStorage);
|
|
545
|
+
const reports =
|
|
546
|
+
(await authStorage.fetchUsageReports({
|
|
547
|
+
baseUrlResolver: provider => modelRegistry.getProviderBaseUrl(provider),
|
|
548
|
+
})) ?? [];
|
|
549
|
+
let accounts = collectStoredAccounts(authStorage);
|
|
550
|
+
let filteredReports = reports;
|
|
551
|
+
if (cmd.provider) {
|
|
552
|
+
const wanted = cmd.provider.toLowerCase();
|
|
553
|
+
filteredReports = reports.filter(report => report.provider.toLowerCase() === wanted);
|
|
554
|
+
accounts = accounts.filter(account => account.provider.toLowerCase() === wanted);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
const redaction = cmd.redact ? buildRedactionMap(collectIdentityStrings(filteredReports, accounts)) : undefined;
|
|
558
|
+
|
|
559
|
+
if (cmd.json) {
|
|
560
|
+
// Drop the heavy provider-specific `raw` payload — same shape as the
|
|
561
|
+
// broker/gateway `/v1/usage` endpoints.
|
|
562
|
+
let trimmed = filteredReports.map(({ raw: _raw, ...rest }) => rest);
|
|
563
|
+
let unreportedAccounts = collectUnreportedAccounts(filteredReports, accounts);
|
|
564
|
+
if (redaction) {
|
|
565
|
+
trimmed = trimmed.map(report => redactReportForJson(report, redaction));
|
|
566
|
+
unreportedAccounts = unreportedAccounts.map(account => ({
|
|
567
|
+
...account,
|
|
568
|
+
email: maskIdentity(redaction, account.email),
|
|
569
|
+
accountId: maskIdentity(redaction, account.accountId),
|
|
570
|
+
projectId: maskIdentity(redaction, account.projectId),
|
|
571
|
+
enterpriseUrl: maskIdentity(redaction, account.enterpriseUrl),
|
|
572
|
+
}));
|
|
573
|
+
}
|
|
574
|
+
const capacity: Record<string, ProviderWindowStat[]> = {};
|
|
575
|
+
for (const report of filteredReports) {
|
|
576
|
+
if (capacity[report.provider]) continue;
|
|
577
|
+
const stats = computeProviderWindowStats(filteredReports.filter(peer => peer.provider === report.provider));
|
|
578
|
+
if (stats.length > 0) capacity[report.provider] = stats;
|
|
579
|
+
}
|
|
580
|
+
const payload = {
|
|
581
|
+
generatedAt: Date.now(),
|
|
582
|
+
reports: trimmed,
|
|
583
|
+
accountsWithoutUsage: unreportedAccounts,
|
|
584
|
+
capacity,
|
|
585
|
+
};
|
|
586
|
+
process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
if (filteredReports.length === 0 && accounts.length === 0) {
|
|
591
|
+
const scope = cmd.provider ? ` for provider "${cmd.provider}"` : "";
|
|
592
|
+
process.stderr.write(
|
|
593
|
+
chalk.yellow(`No credentials found${scope}. Run \`omp\` and use /login to add accounts.\n`),
|
|
594
|
+
);
|
|
595
|
+
process.exitCode = 1;
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
process.stdout.write(`${formatUsageBreakdown(filteredReports, accounts, Date.now(), redaction)}\n`);
|
|
600
|
+
} finally {
|
|
601
|
+
authStorage.close();
|
|
602
|
+
}
|
|
603
|
+
}
|
package/src/cli-commands.ts
CHANGED
|
@@ -32,11 +32,24 @@ export const commands: CommandEntry[] = [
|
|
|
32
32
|
{ name: "ssh", load: () => import("./commands/ssh").then(m => m.default) },
|
|
33
33
|
{ name: "stats", load: () => import("./commands/stats").then(m => m.default) },
|
|
34
34
|
{ name: "update", load: () => import("./commands/update").then(m => m.default) },
|
|
35
|
+
{ name: "usage", load: () => import("./commands/usage").then(m => m.default) },
|
|
35
36
|
{ name: "tiny-models", load: () => import("./commands/tiny-models").then(m => m.default) },
|
|
36
37
|
{ name: "worktree", load: () => import("./commands/worktree").then(m => m.default), aliases: ["wt"] },
|
|
37
38
|
{ name: "search", load: () => import("./commands/web-search").then(m => m.default), aliases: ["q"] },
|
|
38
39
|
];
|
|
39
40
|
|
|
41
|
+
const RESERVED_TOP_LEVEL_WORDS = new Map<string, string>([
|
|
42
|
+
[
|
|
43
|
+
"extensions",
|
|
44
|
+
'`omp extensions` is not a management command. Use `omp plugin list` / `omp plugin install`, or run `omp launch extensions` if you meant to send "extensions" as a prompt.',
|
|
45
|
+
],
|
|
46
|
+
]);
|
|
47
|
+
|
|
48
|
+
export function reservedTopLevelWordMessage(first: string | undefined, argc = 1): string | undefined {
|
|
49
|
+
if (argc !== 1 || !first || first.startsWith("-") || first.startsWith("@")) return undefined;
|
|
50
|
+
return RESERVED_TOP_LEVEL_WORDS.get(first);
|
|
51
|
+
}
|
|
52
|
+
|
|
40
53
|
/**
|
|
41
54
|
* Return true when `first` matches a registered subcommand name or alias.
|
|
42
55
|
*
|
|
@@ -47,3 +60,20 @@ export function isSubcommand(first: string | undefined): boolean {
|
|
|
47
60
|
if (!first || first.startsWith("-") || first.startsWith("@")) return false;
|
|
48
61
|
return commands.some(entry => entry.name === first || entry.aliases?.includes(first));
|
|
49
62
|
}
|
|
63
|
+
|
|
64
|
+
export type ResolvedCliArgv = { argv: string[] } | { error: string };
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Decide what the CLI runner should do with raw argv: reject bare reserved
|
|
68
|
+
* management words, pass help/version through untouched, and route everything
|
|
69
|
+
* that is not a known subcommand to `launch`.
|
|
70
|
+
*/
|
|
71
|
+
export function resolveCliArgv(argv: string[]): ResolvedCliArgv {
|
|
72
|
+
const first = argv[0];
|
|
73
|
+
const reservedMessage = reservedTopLevelWordMessage(first, argv.length);
|
|
74
|
+
if (reservedMessage) return { error: reservedMessage };
|
|
75
|
+
if (first === "--help" || first === "-h" || first === "--version" || first === "-v" || first === "help") {
|
|
76
|
+
return { argv };
|
|
77
|
+
}
|
|
78
|
+
return { argv: isSubcommand(first) ? argv : ["launch", ...argv] };
|
|
79
|
+
}
|