@oh-my-pi/pi-coding-agent 15.10.10 → 15.10.11

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.
Files changed (345) hide show
  1. package/CHANGELOG.md +95 -4
  2. package/dist/cli.js +23087 -0
  3. package/dist/tokenizers.linux-x64-gnu-xcjh3jwk.node +0 -0
  4. package/dist/types/async/job-manager.d.ts +18 -0
  5. package/dist/types/cli/args.d.ts +1 -1
  6. package/dist/types/cli/dry-balance-cli.d.ts +1 -1
  7. package/dist/types/cli/gallery-cli.d.ts +1 -1
  8. package/dist/types/cli/gallery-fixtures/types.d.ts +1 -1
  9. package/dist/types/cli/usage-cli.d.ts +72 -0
  10. package/dist/types/commands/launch.d.ts +1 -1
  11. package/dist/types/commands/read.d.ts +1 -1
  12. package/dist/types/commands/usage.d.ts +25 -0
  13. package/dist/types/config/append-only-context-mode.d.ts +2 -1
  14. package/dist/types/config/model-discovery.d.ts +55 -0
  15. package/dist/types/config/model-registry.d.ts +7 -219
  16. package/dist/types/config/model-resolver.d.ts +16 -10
  17. package/dist/types/config/model-roles.d.ts +28 -0
  18. package/dist/types/config/models-config-schema.d.ts +523 -42
  19. package/dist/types/config/models-config.d.ts +385 -0
  20. package/dist/types/config/settings-schema.d.ts +12 -7
  21. package/dist/types/config/settings.d.ts +1 -1
  22. package/dist/types/debug/log-viewer.d.ts +1 -1
  23. package/dist/types/debug/raw-sse.d.ts +1 -1
  24. package/dist/types/eval/backend.d.ts +0 -2
  25. package/dist/types/eval/idle-timeout.d.ts +0 -4
  26. package/dist/types/eval/js/shared/rewrite-imports.d.ts +6 -6
  27. package/dist/types/export/html/template.generated.d.ts +1 -1
  28. package/dist/types/extensibility/extensions/types.d.ts +3 -3
  29. package/dist/types/hindsight/mental-models.d.ts +17 -8
  30. package/dist/types/internal-urls/artifact-protocol.d.ts +2 -2
  31. package/dist/types/internal-urls/types.d.ts +1 -1
  32. package/dist/types/lsp/edits.d.ts +9 -0
  33. package/dist/types/lsp/index.d.ts +2 -2
  34. package/dist/types/lsp/types.d.ts +2 -0
  35. package/dist/types/lsp/utils.d.ts +3 -0
  36. package/dist/types/mcp/json-rpc.d.ts +5 -0
  37. package/dist/types/mnemopi/state.d.ts +11 -1
  38. package/dist/types/modes/components/agent-dashboard.d.ts +1 -1
  39. package/dist/types/modes/components/assistant-message.d.ts +3 -1
  40. package/dist/types/modes/components/bash-execution.d.ts +1 -1
  41. package/dist/types/modes/components/copy-selector.d.ts +1 -1
  42. package/dist/types/modes/components/dynamic-border.d.ts +1 -1
  43. package/dist/types/modes/components/extensions/extension-dashboard.d.ts +1 -1
  44. package/dist/types/modes/components/extensions/extension-list.d.ts +1 -1
  45. package/dist/types/modes/components/extensions/inspector-panel.d.ts +1 -1
  46. package/dist/types/modes/components/footer.d.ts +1 -1
  47. package/dist/types/modes/components/hook-editor.d.ts +5 -0
  48. package/dist/types/modes/components/hook-input.d.ts +4 -0
  49. package/dist/types/modes/components/hook-selector.d.ts +1 -1
  50. package/dist/types/modes/components/model-selector.d.ts +1 -1
  51. package/dist/types/modes/components/plan-review-overlay.d.ts +1 -1
  52. package/dist/types/modes/components/session-observer-overlay.d.ts +1 -1
  53. package/dist/types/modes/components/session-selector.d.ts +1 -1
  54. package/dist/types/modes/components/status-line/component.d.ts +1 -1
  55. package/dist/types/modes/components/tiny-title-download-progress.d.ts +1 -1
  56. package/dist/types/modes/components/transcript-container.d.ts +25 -6
  57. package/dist/types/modes/components/tree-selector.d.ts +1 -1
  58. package/dist/types/modes/components/user-message-selector.d.ts +1 -1
  59. package/dist/types/modes/components/user-message.d.ts +2 -1
  60. package/dist/types/modes/components/visual-truncate.d.ts +1 -1
  61. package/dist/types/modes/components/welcome.d.ts +19 -3
  62. package/dist/types/modes/controllers/mcp-command-controller.d.ts +1 -1
  63. package/dist/types/modes/controllers/streaming-reveal.d.ts +1 -1
  64. package/dist/types/modes/interactive-mode.d.ts +1 -1
  65. package/dist/types/modes/setup-wizard/scenes/sign-in.d.ts +1 -1
  66. package/dist/types/modes/setup-wizard/scenes/types.d.ts +1 -1
  67. package/dist/types/modes/setup-wizard/scenes/web-search.d.ts +1 -1
  68. package/dist/types/modes/setup-wizard/wizard-overlay.d.ts +1 -1
  69. package/dist/types/modes/types.d.ts +2 -1
  70. package/dist/types/session/agent-session.d.ts +1 -1
  71. package/dist/types/session/auth-broker-config.d.ts +4 -0
  72. package/dist/types/session/session-manager.d.ts +1 -1
  73. package/dist/types/slash-commands/helpers/stats-dashboard.d.ts +13 -0
  74. package/dist/types/ssh/connection-manager.d.ts +8 -0
  75. package/dist/types/task/parallel.d.ts +2 -2
  76. package/dist/types/task/worktree.d.ts +2 -0
  77. package/dist/types/tools/ask.d.ts +4 -0
  78. package/dist/types/tools/conflict-detect.d.ts +16 -0
  79. package/dist/types/tools/github-cache.d.ts +7 -0
  80. package/dist/types/tools/sqlite-reader.d.ts +3 -0
  81. package/dist/types/tui/output-block.d.ts +3 -3
  82. package/dist/types/utils/changelog.d.ts +8 -0
  83. package/dist/types/web/scrapers/readthedocs.d.ts +3 -0
  84. package/dist/types/web/scrapers/types.d.ts +12 -0
  85. package/dist/types/web/search/providers/codex.d.ts +1 -1
  86. package/dist/types/web/search/providers/gemini.d.ts +1 -1
  87. package/examples/extensions/tools.ts +5 -4
  88. package/package.json +14 -11
  89. package/scripts/build-binary.ts +18 -23
  90. package/scripts/bundle-dist.ts +81 -0
  91. package/scripts/{dev-launch → omp} +1 -1
  92. package/scripts/{dev-launch-preload.ts → omp.ts} +1 -1
  93. package/src/async/job-manager.ts +57 -3
  94. package/src/autoresearch/dashboard.ts +1 -1
  95. package/src/autoresearch/prompt-setup.md +6 -6
  96. package/src/autoresearch/prompt.md +6 -6
  97. package/src/capability/fs.ts +10 -0
  98. package/src/cli/args.ts +1 -1
  99. package/src/cli/auth-gateway-cli.ts +1 -3
  100. package/src/cli/dry-balance-cli.ts +1 -1
  101. package/src/cli/gallery-cli.ts +1 -1
  102. package/src/cli/gallery-fixtures/fs.ts +1 -1
  103. package/src/cli/gallery-fixtures/types.ts +5 -1
  104. package/src/cli/list-models.ts +2 -1
  105. package/src/cli/usage-cli.ts +603 -0
  106. package/src/cli-commands.ts +1 -0
  107. package/src/cli.ts +69 -5
  108. package/src/commands/complete.ts +1 -1
  109. package/src/commands/launch.ts +1 -1
  110. package/src/commands/read.ts +6 -3
  111. package/src/commands/usage.ts +35 -0
  112. package/src/commit/agentic/agent.ts +1 -1
  113. package/src/commit/model-selection.ts +1 -1
  114. package/src/config/append-only-context-mode.ts +6 -12
  115. package/src/config/model-discovery.ts +554 -0
  116. package/src/config/model-registry.ts +231 -1019
  117. package/src/config/model-resolver.ts +113 -156
  118. package/src/config/model-roles.ts +74 -0
  119. package/src/config/models-config-schema.ts +57 -8
  120. package/src/config/models-config.ts +129 -0
  121. package/src/config/settings-schema.ts +18 -4
  122. package/src/config/settings.ts +37 -1
  123. package/src/dap/client.ts +124 -37
  124. package/src/dap/session.ts +259 -158
  125. package/src/debug/log-viewer.ts +1 -1
  126. package/src/debug/raw-sse.ts +1 -1
  127. package/src/edit/diff.ts +47 -3
  128. package/src/edit/hashline/block-resolver.ts +20 -1
  129. package/src/edit/hashline/diff.ts +36 -1
  130. package/src/edit/hashline/execute.ts +8 -2
  131. package/src/edit/index.ts +16 -1
  132. package/src/edit/modes/patch.ts +52 -0
  133. package/src/edit/modes/replace.ts +56 -22
  134. package/src/edit/notebook.ts +22 -2
  135. package/src/edit/renderer.ts +36 -10
  136. package/src/eval/__tests__/completion-bridge.test.ts +1 -1
  137. package/src/eval/backend.ts +0 -2
  138. package/src/eval/completion-bridge.ts +2 -1
  139. package/src/eval/idle-timeout.ts +2 -9
  140. package/src/eval/js/context-manager.ts +6 -8
  141. package/src/eval/js/executor.ts +6 -2
  142. package/src/eval/js/index.ts +0 -2
  143. package/src/eval/js/shared/helpers.ts +5 -6
  144. package/src/eval/js/shared/local-module-loader.ts +1 -1
  145. package/src/eval/js/shared/prelude.txt +62 -1
  146. package/src/eval/js/shared/rewrite-imports.ts +40 -22
  147. package/src/eval/js/shared/runtime.ts +1 -1
  148. package/src/eval/py/index.ts +0 -2
  149. package/src/eval/py/kernel.ts +19 -0
  150. package/src/eval/py/runner.py +107 -3
  151. package/src/exec/bash-executor.ts +3 -1
  152. package/src/export/html/template.generated.ts +1 -1
  153. package/src/export/html/template.js +3 -1
  154. package/src/extensibility/extensions/types.ts +3 -2
  155. package/src/extensibility/plugins/legacy-pi-compat.ts +20 -3
  156. package/src/hindsight/mental-models.ts +59 -12
  157. package/src/hindsight/state.ts +6 -1
  158. package/src/internal-urls/artifact-protocol.ts +11 -2
  159. package/src/internal-urls/docs-index.generated.ts +8 -8
  160. package/src/internal-urls/issue-pr-protocol.ts +12 -5
  161. package/src/internal-urls/router.ts +1 -1
  162. package/src/internal-urls/types.ts +1 -1
  163. package/src/lib/xai-http.ts +1 -1
  164. package/src/lsp/client.ts +118 -38
  165. package/src/lsp/clients/biome-client.ts +101 -39
  166. package/src/lsp/edits.ts +143 -95
  167. package/src/lsp/index.ts +31 -22
  168. package/src/lsp/render.ts +1 -1
  169. package/src/lsp/types.ts +2 -0
  170. package/src/lsp/utils.ts +28 -10
  171. package/src/main.ts +165 -17
  172. package/src/mcp/json-rpc.ts +35 -5
  173. package/src/mcp/transports/stdio.ts +7 -1
  174. package/src/memories/index.ts +2 -1
  175. package/src/mnemopi/backend.ts +25 -3
  176. package/src/mnemopi/state.ts +38 -2
  177. package/src/modes/components/agent-dashboard.ts +10 -7
  178. package/src/modes/components/assistant-message.ts +19 -13
  179. package/src/modes/components/bash-execution.ts +1 -1
  180. package/src/modes/components/copy-selector.ts +1 -1
  181. package/src/modes/components/diff.ts +13 -2
  182. package/src/modes/components/dynamic-border.ts +12 -3
  183. package/src/modes/components/extensions/extension-dashboard.ts +8 -5
  184. package/src/modes/components/extensions/extension-list.ts +1 -1
  185. package/src/modes/components/extensions/inspector-panel.ts +1 -1
  186. package/src/modes/components/footer.ts +1 -1
  187. package/src/modes/components/history-search.ts +1 -1
  188. package/src/modes/components/hook-editor.ts +8 -0
  189. package/src/modes/components/hook-input.ts +8 -0
  190. package/src/modes/components/hook-selector.ts +2 -2
  191. package/src/modes/components/model-selector.ts +4 -2
  192. package/src/modes/components/plan-review-overlay.ts +1 -1
  193. package/src/modes/components/session-observer-overlay.ts +2 -2
  194. package/src/modes/components/session-selector.ts +1 -1
  195. package/src/modes/components/settings-selector.ts +5 -1
  196. package/src/modes/components/status-line/component.ts +1 -1
  197. package/src/modes/components/tiny-title-download-progress.ts +1 -1
  198. package/src/modes/components/transcript-container.ts +258 -53
  199. package/src/modes/components/tree-selector.ts +3 -3
  200. package/src/modes/components/user-message-selector.ts +1 -1
  201. package/src/modes/components/user-message.ts +17 -5
  202. package/src/modes/components/visual-truncate.ts +1 -1
  203. package/src/modes/components/welcome.ts +108 -26
  204. package/src/modes/controllers/command-controller.ts +10 -3
  205. package/src/modes/controllers/event-controller.ts +73 -4
  206. package/src/modes/controllers/input-controller.ts +1 -1
  207. package/src/modes/controllers/mcp-command-controller.ts +1 -1
  208. package/src/modes/controllers/selector-controller.ts +1 -1
  209. package/src/modes/controllers/streaming-reveal.ts +85 -18
  210. package/src/modes/interactive-mode.ts +3 -9
  211. package/src/modes/setup-wizard/scenes/glyph.ts +1 -1
  212. package/src/modes/setup-wizard/scenes/providers.ts +1 -1
  213. package/src/modes/setup-wizard/scenes/sign-in.ts +1 -1
  214. package/src/modes/setup-wizard/scenes/theme.ts +1 -1
  215. package/src/modes/setup-wizard/scenes/types.ts +1 -1
  216. package/src/modes/setup-wizard/scenes/web-search.ts +1 -1
  217. package/src/modes/setup-wizard/wizard-overlay.ts +1 -1
  218. package/src/modes/types.ts +2 -1
  219. package/src/prompts/agents/explore.md +2 -2
  220. package/src/prompts/agents/librarian.md +1 -2
  221. package/src/prompts/agents/oracle.md +1 -1
  222. package/src/prompts/agents/plan.md +5 -5
  223. package/src/prompts/agents/task.md +5 -5
  224. package/src/prompts/ci-green-request.md +5 -7
  225. package/src/prompts/goals/goal-budget-limit.md +2 -2
  226. package/src/prompts/goals/goal-continuation.md +4 -4
  227. package/src/prompts/goals/goal-mode-active.md +1 -1
  228. package/src/prompts/memories/read-path.md +1 -1
  229. package/src/prompts/memories/stage_one_system.md +2 -2
  230. package/src/prompts/review-custom-request.md +1 -1
  231. package/src/prompts/system/agent-creation-architect.md +2 -2
  232. package/src/prompts/system/auto-continue.md +1 -1
  233. package/src/prompts/system/background-tan-dispatch.md +1 -1
  234. package/src/prompts/system/btw-user.md +2 -2
  235. package/src/prompts/system/commit-message-system.md +13 -1
  236. package/src/prompts/system/custom-system-prompt.md +1 -1
  237. package/src/prompts/system/eager-todo.md +2 -2
  238. package/src/prompts/system/irc-incoming.md +1 -1
  239. package/src/prompts/system/manual-continue.md +1 -1
  240. package/src/prompts/system/omfg-user.md +3 -4
  241. package/src/prompts/system/orchestrate-notice.md +9 -9
  242. package/src/prompts/system/plan-mode-active.md +4 -4
  243. package/src/prompts/system/plan-mode-subagent.md +4 -5
  244. package/src/prompts/system/plan-mode-tool-decision-reminder.md +1 -1
  245. package/src/prompts/system/project-prompt.md +2 -2
  246. package/src/prompts/system/subagent-system-prompt.md +4 -4
  247. package/src/prompts/system/system-prompt.md +13 -24
  248. package/src/prompts/system/title-system.md +2 -2
  249. package/src/prompts/system/ttsr-tool-reminder.md +1 -1
  250. package/src/prompts/system/workflow-notice.md +1 -1
  251. package/src/prompts/tools/ast-edit.md +1 -1
  252. package/src/prompts/tools/ast-grep.md +2 -2
  253. package/src/prompts/tools/bash.md +5 -7
  254. package/src/prompts/tools/browser.md +7 -7
  255. package/src/prompts/tools/debug.md +1 -1
  256. package/src/prompts/tools/eval.md +3 -3
  257. package/src/prompts/tools/find.md +0 -1
  258. package/src/prompts/tools/github.md +8 -7
  259. package/src/prompts/tools/goal.md +1 -1
  260. package/src/prompts/tools/image-gen.md +1 -1
  261. package/src/prompts/tools/inspect-image-system.md +1 -1
  262. package/src/prompts/tools/irc.md +15 -15
  263. package/src/prompts/tools/lsp.md +2 -2
  264. package/src/prompts/tools/patch.md +2 -2
  265. package/src/prompts/tools/read.md +3 -4
  266. package/src/prompts/tools/recall.md +1 -1
  267. package/src/prompts/tools/reflect.md +1 -1
  268. package/src/prompts/tools/render-mermaid.md +2 -2
  269. package/src/prompts/tools/replace.md +4 -10
  270. package/src/prompts/tools/rewind.md +2 -2
  271. package/src/prompts/tools/search-tool-bm25.md +1 -9
  272. package/src/prompts/tools/search.md +0 -1
  273. package/src/prompts/tools/ssh.md +0 -4
  274. package/src/prompts/tools/task.md +2 -3
  275. package/src/prompts/tools/todo.md +1 -1
  276. package/src/sdk.ts +23 -10
  277. package/src/session/agent-session.ts +44 -10
  278. package/src/session/auth-broker-config.ts +30 -1
  279. package/src/session/session-manager.ts +2 -2
  280. package/src/session/streaming-output.ts +23 -2
  281. package/src/slash-commands/builtin-registry.ts +20 -0
  282. package/src/slash-commands/helpers/stats-dashboard.ts +85 -0
  283. package/src/ssh/connection-manager.ts +27 -0
  284. package/src/task/commands.ts +2 -1
  285. package/src/task/executor.ts +61 -53
  286. package/src/task/index.ts +137 -60
  287. package/src/task/parallel.ts +3 -3
  288. package/src/task/render.ts +2 -2
  289. package/src/task/worktree.ts +64 -56
  290. package/src/thinking.ts +2 -1
  291. package/src/tiny/title-client.ts +26 -11
  292. package/src/tools/archive-reader.ts +30 -2
  293. package/src/tools/ask.ts +104 -21
  294. package/src/tools/ast-edit.ts +25 -5
  295. package/src/tools/auto-generated-guard.ts +20 -3
  296. package/src/tools/bash-interactive.ts +27 -7
  297. package/src/tools/bash.ts +54 -13
  298. package/src/tools/browser/launch.ts +11 -2
  299. package/src/tools/browser/readable.ts +19 -2
  300. package/src/tools/browser/registry.ts +4 -1
  301. package/src/tools/browser/render.ts +2 -2
  302. package/src/tools/browser/tab-supervisor.ts +55 -16
  303. package/src/tools/conflict-detect.ts +50 -4
  304. package/src/tools/debug.ts +1 -1
  305. package/src/tools/eval-render.ts +5 -5
  306. package/src/tools/eval.ts +0 -2
  307. package/src/tools/fetch.ts +33 -10
  308. package/src/tools/gh-cache-invalidation.ts +63 -8
  309. package/src/tools/gh-renderer.ts +1 -1
  310. package/src/tools/gh.ts +172 -29
  311. package/src/tools/github-cache.ts +70 -6
  312. package/src/tools/image-gen.ts +3 -9
  313. package/src/tools/irc.ts +5 -1
  314. package/src/tools/job.ts +1 -1
  315. package/src/tools/read.ts +202 -61
  316. package/src/tools/render-utils.ts +3 -3
  317. package/src/tools/resolve.ts +1 -1
  318. package/src/tools/search.ts +92 -29
  319. package/src/tools/sqlite-reader.ts +17 -5
  320. package/src/tools/ssh.ts +8 -8
  321. package/src/tools/todo.ts +38 -8
  322. package/src/tools/write.ts +118 -18
  323. package/src/tui/output-block.ts +4 -4
  324. package/src/utils/changelog.ts +27 -1
  325. package/src/utils/file-mentions.ts +2 -1
  326. package/src/web/scrapers/arxiv.ts +1 -1
  327. package/src/web/scrapers/go-pkg.ts +1 -1
  328. package/src/web/scrapers/iacr.ts +1 -1
  329. package/src/web/scrapers/readthedocs.ts +1 -1
  330. package/src/web/scrapers/twitter.ts +2 -1
  331. package/src/web/scrapers/types.ts +87 -8
  332. package/src/web/scrapers/wikipedia.ts +1 -1
  333. package/src/web/scrapers/youtube.ts +6 -1
  334. package/src/web/search/index.ts +1 -1
  335. package/src/web/search/providers/codex.ts +2 -1
  336. package/src/web/search/providers/gemini.ts +2 -3
  337. package/src/web/search/render.ts +8 -6
  338. package/dist/types/config/model-equivalence.d.ts +0 -24
  339. package/dist/types/config/model-id-affixes.d.ts +0 -12
  340. package/dist/types/config/model-provider-priority.d.ts +0 -1
  341. package/dist/types/exec/idle-timeout-watchdog.d.ts +0 -18
  342. package/src/config/model-equivalence.ts +0 -875
  343. package/src/config/model-id-affixes.ts +0 -81
  344. package/src/config/model-provider-priority.ts +0 -56
  345. 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
+ }
@@ -32,6 +32,7 @@ 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"] },