@duckmind/dm-windows-x64 0.60.4 → 0.60.8

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 (197) hide show
  1. package/dm.exe +0 -0
  2. package/extensions/.dm-extensions.json +70 -123
  3. package/extensions/dm-9router-ext/src/index.js +410 -5
  4. package/extensions/dm-caveman/extensions/caveman.js +283 -12
  5. package/extensions/dm-cliproxy/index.js +182 -2
  6. package/extensions/dm-cliproxy/scripts/check-config-migration.js +66 -8
  7. package/extensions/dm-cliproxy/src/apply.js +228 -1
  8. package/extensions/dm-cliproxy/src/cache.js +42 -1
  9. package/extensions/dm-cliproxy/src/commands.js +50 -2
  10. package/extensions/dm-cliproxy/src/compat.js +81 -1
  11. package/extensions/dm-cliproxy/src/config.js +192 -2
  12. package/extensions/dm-cliproxy/src/conflicts.js +46 -1
  13. package/extensions/dm-cliproxy/src/fetch-models.js +190 -1
  14. package/extensions/dm-cliproxy/src/fetch-usage.js +41 -1
  15. package/extensions/dm-cliproxy/src/log.js +23 -1
  16. package/extensions/dm-cliproxy/src/status-quota.js +77 -1
  17. package/extensions/dm-cliproxy/src/ui-frame.js +50 -1
  18. package/extensions/dm-cliproxy/src/ui-hub/hub.js +199 -2
  19. package/extensions/dm-cliproxy/src/ui-hub/index.js +26 -2
  20. package/extensions/dm-cliproxy/src/ui-hub/shell.js +46 -1
  21. package/extensions/dm-cliproxy/src/ui-hub/view-diagnostics.js +69 -1
  22. package/extensions/dm-cliproxy/src/ui-hub/view-models.js +379 -1
  23. package/extensions/dm-cliproxy/src/ui-hub/view-usage.js +106 -1
  24. package/extensions/dm-cliproxy/src/ui-picker/catalog.js +41 -1
  25. package/extensions/dm-cliproxy/src/ui-picker/mutate.js +115 -1
  26. package/extensions/dm-cliproxy/src/ui-picker/prompt-confirm.js +35 -1
  27. package/extensions/dm-cliproxy/src/ui-picker/prompt-name.js +62 -1
  28. package/extensions/dm-cliproxy/src/ui-picker/providers.js +45 -1
  29. package/extensions/dm-cliproxy/src/ui-picker/render-text.js +34 -1
  30. package/extensions/dm-cliproxy/src/ui-picker/rows.js +57 -1
  31. package/extensions/dm-cliproxy/src/ui-setup.js +260 -2
  32. package/extensions/dm-cliproxy/src/ui-usage.js +151 -1
  33. package/extensions/dm-cliproxy/src/usage-shared-cache.js +97 -1
  34. package/extensions/dm-context/src/context.js +144 -1
  35. package/extensions/dm-context/src/index.js +339 -7
  36. package/extensions/dm-context/src/utils.js +9 -1
  37. package/extensions/dm-cua/bin/browser-cua.mjs +73 -8
  38. package/extensions/dm-cua/index.js +75 -6
  39. package/extensions/dm-cua/src/browser-cua-lib.mjs +490 -6
  40. package/extensions/dm-cua/src/browser-install.mjs +331 -2
  41. package/extensions/dm-fff/src/index.js +688 -12
  42. package/extensions/dm-fff/src/query.js +60 -1
  43. package/extensions/dm-goal/src/goal.js +894 -23
  44. package/extensions/dm-image2/index.js +103 -9
  45. package/extensions/dm-image2/src/image-lib.mjs +1275 -8
  46. package/extensions/dm-subagents/install.mjs +62 -8
  47. package/extensions/dm-subagents/src/agents/agent-management.js +1190 -36
  48. package/extensions/dm-subagents/src/agents/agent-memory.js +216 -6
  49. package/extensions/dm-subagents/src/agents/agent-scope.js +5 -1
  50. package/extensions/dm-subagents/src/agents/agent-selection.js +20 -1
  51. package/extensions/dm-subagents/src/agents/agent-serializer.js +120 -5
  52. package/extensions/dm-subagents/src/agents/agents.js +1139 -11
  53. package/extensions/dm-subagents/src/agents/chain-serializer.js +299 -11
  54. package/extensions/dm-subagents/src/agents/frontmatter.js +65 -6
  55. package/extensions/dm-subagents/src/agents/identity.js +29 -1
  56. package/extensions/dm-subagents/src/agents/proactive-skills.js +141 -1
  57. package/extensions/dm-subagents/src/agents/skills.js +614 -6
  58. package/extensions/dm-subagents/src/extension/config.js +35 -2
  59. package/extensions/dm-subagents/src/extension/control-notices.js +69 -4
  60. package/extensions/dm-subagents/src/extension/doctor.js +172 -15
  61. package/extensions/dm-subagents/src/extension/fanout-child.js +158 -231
  62. package/extensions/dm-subagents/src/extension/index.js +521 -359
  63. package/extensions/dm-subagents/src/extension/rpc.js +266 -7
  64. package/extensions/dm-subagents/src/extension/schemas.js +275 -1
  65. package/extensions/dm-subagents/src/extension/tool-description.js +111 -6
  66. package/extensions/dm-subagents/src/intercom/intercom-bridge.js +126 -4
  67. package/extensions/dm-subagents/src/intercom/native-supervisor-channel.js +452 -5
  68. package/extensions/dm-subagents/src/intercom/result-intercom.js +319 -3
  69. package/extensions/dm-subagents/src/profiles/profiles.js +458 -3
  70. package/extensions/dm-subagents/src/runs/background/async-execution.js +834 -40
  71. package/extensions/dm-subagents/src/runs/background/async-job-tracker.js +435 -14
  72. package/extensions/dm-subagents/src/runs/background/async-resume.js +334 -8
  73. package/extensions/dm-subagents/src/runs/background/async-status.js +313 -12
  74. package/extensions/dm-subagents/src/runs/background/chain-append.js +245 -2
  75. package/extensions/dm-subagents/src/runs/background/chain-root-attachment.js +136 -1
  76. package/extensions/dm-subagents/src/runs/background/completion-batcher.js +94 -1
  77. package/extensions/dm-subagents/src/runs/background/completion-dedupe.js +54 -1
  78. package/extensions/dm-subagents/src/runs/background/control-channel.js +190 -1
  79. package/extensions/dm-subagents/src/runs/background/fleet-view.js +483 -17
  80. package/extensions/dm-subagents/src/runs/background/notify.js +129 -3
  81. package/extensions/dm-subagents/src/runs/background/parallel-groups.js +34 -1
  82. package/extensions/dm-subagents/src/runs/background/result-watcher.js +236 -6
  83. package/extensions/dm-subagents/src/runs/background/run-id-resolver.js +76 -4
  84. package/extensions/dm-subagents/src/runs/background/run-status.js +427 -23
  85. package/extensions/dm-subagents/src/runs/background/scheduled-runs.js +487 -4
  86. package/extensions/dm-subagents/src/runs/background/stale-run-reconciler.js +306 -9
  87. package/extensions/dm-subagents/src/runs/background/subagent-runner.js +2849 -73
  88. package/extensions/dm-subagents/src/runs/background/top-level-async.js +5 -1
  89. package/extensions/dm-subagents/src/runs/background/wait.js +206 -11
  90. package/extensions/dm-subagents/src/runs/foreground/chain-clarify.js +1013 -12
  91. package/extensions/dm-subagents/src/runs/foreground/chain-execution.js +980 -101
  92. package/extensions/dm-subagents/src/runs/foreground/execution.js +1165 -45
  93. package/extensions/dm-subagents/src/runs/foreground/subagent-executor.js +3157 -222
  94. package/extensions/dm-subagents/src/runs/shared/acceptance.js +835 -3
  95. package/extensions/dm-subagents/src/runs/shared/chain-outputs.js +104 -1
  96. package/extensions/dm-subagents/src/runs/shared/completion-guard.js +116 -3
  97. package/extensions/dm-subagents/src/runs/shared/dm-args.js +208 -1
  98. package/extensions/dm-subagents/src/runs/shared/dm-spawn.js +90 -1
  99. package/extensions/dm-subagents/src/runs/shared/dynamic-fanout.js +282 -1
  100. package/extensions/dm-subagents/src/runs/shared/long-running-guard.js +148 -1
  101. package/extensions/dm-subagents/src/runs/shared/mcp-direct-tool-allowlist.js +305 -1
  102. package/extensions/dm-subagents/src/runs/shared/model-fallback.js +194 -1
  103. package/extensions/dm-subagents/src/runs/shared/model-scope.js +65 -1
  104. package/extensions/dm-subagents/src/runs/shared/nested-events.js +851 -8
  105. package/extensions/dm-subagents/src/runs/shared/nested-path.js +41 -1
  106. package/extensions/dm-subagents/src/runs/shared/nested-render.js +105 -1
  107. package/extensions/dm-subagents/src/runs/shared/parallel-utils.js +81 -4
  108. package/extensions/dm-subagents/src/runs/shared/run-history.js +51 -4
  109. package/extensions/dm-subagents/src/runs/shared/single-output.js +149 -8
  110. package/extensions/dm-subagents/src/runs/shared/structured-output.js +58 -1
  111. package/extensions/dm-subagents/src/runs/shared/subagent-control.js +166 -5
  112. package/extensions/dm-subagents/src/runs/shared/subagent-prompt-runtime.js +329 -13
  113. package/extensions/dm-subagents/src/runs/shared/tool-budget.js +73 -1
  114. package/extensions/dm-subagents/src/runs/shared/turn-budget.js +47 -4
  115. package/extensions/dm-subagents/src/runs/shared/workflow-graph.js +196 -1
  116. package/extensions/dm-subagents/src/runs/shared/worktree.js +435 -3
  117. package/extensions/dm-subagents/src/shared/artifacts.js +92 -2
  118. package/extensions/dm-subagents/src/shared/atomic-json.js +55 -1
  119. package/extensions/dm-subagents/src/shared/child-transcript.js +167 -5
  120. package/extensions/dm-subagents/src/shared/file-coalescer.js +25 -1
  121. package/extensions/dm-subagents/src/shared/fork-context.js +147 -4
  122. package/extensions/dm-subagents/src/shared/formatters.js +98 -7
  123. package/extensions/dm-subagents/src/shared/jsonl-writer.js +56 -2
  124. package/extensions/dm-subagents/src/shared/model-info.js +62 -1
  125. package/extensions/dm-subagents/src/shared/post-exit-stdio-guard.js +68 -1
  126. package/extensions/dm-subagents/src/shared/session-identity.js +6 -1
  127. package/extensions/dm-subagents/src/shared/session-tokens.js +39 -2
  128. package/extensions/dm-subagents/src/shared/settings.js +198 -11
  129. package/extensions/dm-subagents/src/shared/status-format.js +53 -1
  130. package/extensions/dm-subagents/src/shared/types.js +184 -6
  131. package/extensions/dm-subagents/src/shared/utils.js +462 -2
  132. package/extensions/dm-subagents/src/slash/prompt-template-bridge.js +288 -1
  133. package/extensions/dm-subagents/src/slash/prompt-workflows.js +297 -7
  134. package/extensions/dm-subagents/src/slash/slash-bridge.js +118 -1
  135. package/extensions/dm-subagents/src/slash/slash-commands.js +1287 -31
  136. package/extensions/dm-subagents/src/slash/slash-live-state.js +240 -4
  137. package/extensions/dm-subagents/src/tui/render-helpers.js +64 -1
  138. package/extensions/dm-subagents/src/tui/render.js +1542 -4
  139. package/extensions/dm-usage/index.js +1294 -9
  140. package/extensions/greedysearch-dm/bin/cdp-greedy.mjs +40 -9
  141. package/extensions/greedysearch-dm/bin/cdp-headless.mjs +5 -2
  142. package/extensions/greedysearch-dm/bin/cdp-visible.mjs +5 -2
  143. package/extensions/greedysearch-dm/bin/cdp.mjs +896 -30
  144. package/extensions/greedysearch-dm/bin/gschrome.mjs +30 -2
  145. package/extensions/greedysearch-dm/bin/kill-visible.mjs +7 -2
  146. package/extensions/greedysearch-dm/bin/launch-visible.mjs +13 -2
  147. package/extensions/greedysearch-dm/bin/launch.mjs +282 -10
  148. package/extensions/greedysearch-dm/bin/mcp.mjs +386 -361
  149. package/extensions/greedysearch-dm/bin/search.mjs +620 -540
  150. package/extensions/greedysearch-dm/bin/visible.mjs +22 -2
  151. package/extensions/greedysearch-dm/extractors/bing-copilot.mjs +329 -579
  152. package/extensions/greedysearch-dm/extractors/chatgpt.mjs +301 -583
  153. package/extensions/greedysearch-dm/extractors/common.mjs +408 -32
  154. package/extensions/greedysearch-dm/extractors/consensus.mjs +376 -365
  155. package/extensions/greedysearch-dm/extractors/consent.mjs +303 -14
  156. package/extensions/greedysearch-dm/extractors/gemini.mjs +228 -592
  157. package/extensions/greedysearch-dm/extractors/google-ai.mjs +78 -499
  158. package/extensions/greedysearch-dm/extractors/logically.mjs +270 -347
  159. package/extensions/greedysearch-dm/extractors/perplexity.mjs +243 -581
  160. package/extensions/greedysearch-dm/extractors/selectors.mjs +32 -1
  161. package/extensions/greedysearch-dm/extractors/semantic-scholar.mjs +130 -317
  162. package/extensions/greedysearch-dm/index.js +123 -23
  163. package/extensions/greedysearch-dm/src/fetcher.mjs +576 -2
  164. package/extensions/greedysearch-dm/src/formatters/results.js +95 -10
  165. package/extensions/greedysearch-dm/src/formatters/sources.js +57 -1
  166. package/extensions/greedysearch-dm/src/formatters/synthesis.js +49 -1
  167. package/extensions/greedysearch-dm/src/github.mjs +222 -7
  168. package/extensions/greedysearch-dm/src/reddit.mjs +145 -14
  169. package/extensions/greedysearch-dm/src/search/browser-lifecycle.mjs +340 -9
  170. package/extensions/greedysearch-dm/src/search/challenge-detect.mjs +112 -4
  171. package/extensions/greedysearch-dm/src/search/chrome.mjs +486 -285
  172. package/extensions/greedysearch-dm/src/search/constants.mjs +109 -8
  173. package/extensions/greedysearch-dm/src/search/defaults.mjs +10 -1
  174. package/extensions/greedysearch-dm/src/search/engines.mjs +79 -9
  175. package/extensions/greedysearch-dm/src/search/fetch-source.mjs +441 -349
  176. package/extensions/greedysearch-dm/src/search/file-sources.mjs +29 -7
  177. package/extensions/greedysearch-dm/src/search/minimize.mjs +86 -1
  178. package/extensions/greedysearch-dm/src/search/output.mjs +51 -5
  179. package/extensions/greedysearch-dm/src/search/paths.mjs +48 -1
  180. package/extensions/greedysearch-dm/src/search/pdf.mjs +63 -2
  181. package/extensions/greedysearch-dm/src/search/port-pid.mjs +69 -1
  182. package/extensions/greedysearch-dm/src/search/progress.mjs +109 -2
  183. package/extensions/greedysearch-dm/src/search/query.mjs +21 -1
  184. package/extensions/greedysearch-dm/src/search/recovery.mjs +49 -1
  185. package/extensions/greedysearch-dm/src/search/research.mjs +2227 -458
  186. package/extensions/greedysearch-dm/src/search/scale-aware.mjs +61 -11
  187. package/extensions/greedysearch-dm/src/search/simple-research.mjs +396 -805
  188. package/extensions/greedysearch-dm/src/search/sources.mjs +412 -1
  189. package/extensions/greedysearch-dm/src/search/synthesis-runner.mjs +127 -12
  190. package/extensions/greedysearch-dm/src/search/synthesis.mjs +202 -12
  191. package/extensions/greedysearch-dm/src/tools/greedy-search-handler.js +209 -23
  192. package/extensions/greedysearch-dm/src/tools/shared.js +226 -10
  193. package/extensions/greedysearch-dm/src/utils/content.mjs +35 -4
  194. package/extensions/greedysearch-dm/src/utils/helpers.js +22 -1
  195. package/extensions/greedysearch-dm/src/utils/node-runtime.mjs +10 -1
  196. package/extensions/greedysearch-dm/src/utils/system-cmds.mjs +61 -1
  197. package/package.json +1 -1
@@ -1,9 +1,1294 @@
1
- import{DynamicBorder as bJ}from"@duckmind/dm-coding-agent";import{CancellableLoader as vJ,Container as xJ,Spacer as QJ,matchesKey as R,visibleWidth as C,truncateToWidth as _,wrapTextWithAnsi as ZJ}from"@duckmind/dm-tui";import{constants as d}from"node:fs";import{access as RJ,mkdir as IJ,readdir as pJ,readFile as kJ,writeFile as yJ}from"node:fs/promises";import{dirname as EJ,join as M}from"node:path";import{homedir as SJ}from"node:os";var m=26,gJ={"openai-codex":"duckmind-ultra",openrouter:"duckmind-standard"};function S(J){return gJ[J]??J}var c={label:"Sessions",width:9,getValue:(J)=>E(typeof J.sessions==="number"?J.sessions:J.sessions.size)},LJ={label:"Msgs",width:9,getValue:(J)=>E(J.messages)},W={label:"Cost",width:9,getValue:(J)=>N(J.cost)},p={label:"Tokens",width:9,getValue:(J)=>O(J.tokens.total)},hJ={label:"↑In",width:8,dimmed:!0,getValue:(J)=>O(J.tokens.input+J.tokens.cacheWrite)},lJ={label:"↓Out",width:8,dimmed:!0,getValue:(J)=>O(J.tokens.output)},uJ={label:"Cache",width:8,dimmed:!0,getValue:(J)=>O(J.tokens.cacheRead+J.tokens.cacheWrite)},dJ=[c,LJ,W,p,hJ,lJ,uJ],h=[{columns:dJ,minNameWidth:m},{columns:[c,LJ,W,p],minNameWidth:14,compact:!0},{columns:[c,W,p],minNameWidth:12,compact:!0},{columns:[W,p],minNameWidth:10,compact:!0},{columns:[W],minNameWidth:8,compact:!0}];function s(){return process.env.DM_CODING_AGENT_DIR||process.env.PI_CODING_AGENT_DIR||M(SJ(),".dm","agent")}function e(){return M(s(),"sessions")}function mJ(){return M(s(),"usage")}function A(){return M(mJ(),"state.json")}function UJ(){return M(s(),"settings.json")}async function _J(J,Q,Z){try{let $=await pJ(J,{withFileTypes:!0});for(let Y of $){if(Z?.aborted)return;let X=M(J,Y.name);if(Y.isDirectory())await _J(X,Q,Z);else if(Y.isFile()&&Y.name.endsWith(".jsonl"))Q.push(X)}}catch{}}async function cJ(J){let Q=[];return await _J(e(),Q,J),Q.sort(),Q}async function rJ(J,Q,Z){try{let $=await kJ(J,"utf8");if(Z?.aborted)return null;let Y=$.trim().split(`
2
- `),X=[],z="";for(let j=0;j<Y.length;j++){if(Z?.aborted)return null;if(j%500===0)await new Promise((V)=>setImmediate(V));let F=Y[j];if(!F.trim())continue;try{let V=JSON.parse(F);if(V.type==="session")z=V.id;else if(V.type==="message"&&V.message?.role==="assistant"){let q=V.message;if(q.usage&&q.provider&&q.model){let K=q.usage.input||0,G=q.usage.output||0,H=q.usage.cacheRead||0,B=q.usage.cacheWrite||0,P=V.timestamp?new Date(V.timestamp).getTime():0,D=q.timestamp||(Number.isNaN(P)?0:P),I=K+G+H+B,w=`${D}:${I}`;if(Q.has(w))continue;Q.add(w),X.push({provider:q.provider,model:q.model,cost:q.usage.cost?.total||0,input:K,output:G,cacheRead:H,cacheWrite:B,timestamp:D})}}}catch{}}return z?{sessionId:z,messages:X}:null}catch{return null}}function l(J,Q,Z){J.messages++,J.cost+=Q,J.tokens.total+=Z.total,J.tokens.input+=Z.input,J.tokens.output+=Z.output,J.tokens.cacheRead+=Z.cacheRead,J.tokens.cacheWrite+=Z.cacheWrite}function a(){return{total:0,input:0,output:0,cacheRead:0,cacheWrite:0}}function iJ(){return{sessions:new Set,messages:0,cost:0,tokens:a()}}function oJ(){return{sessions:new Set,messages:0,cost:0,tokens:a(),models:new Map}}function b(){return{providers:new Map,totals:{sessions:0,messages:0,cost:0,tokens:a()},dailySpend:new Map,insights:{insights:[]}}}function v(){return{messages:[],sessionCosts:new Map}}function nJ(){return{today:b(),thisWeek:b(),lastWeek:b(),allTime:b()}}function sJ(J,Q,Z,$){let Y=["allTime"];if(J>=Q)Y.push("today");if(J>=Z)Y.push("thisWeek");else if(J>=$)Y.push("lastWeek");return Y}function eJ(J,Q,Z,$,Y,X,z,j){let F={today:!1,thisWeek:!1,lastWeek:!1,allTime:!1};for(let V of Z){if(V.timestamp>0){let G=j.get(Q);if(!G)j.set(Q,{startMs:V.timestamp,endMs:V.timestamp});else{if(V.timestamp<G.startMs)G.startMs=V.timestamp;if(V.timestamp>G.endMs)G.endMs=V.timestamp}}let q=sJ(V.timestamp,$,Y,X),K={total:V.input+V.output+V.cacheWrite,input:V.input,output:V.output,cacheRead:V.cacheRead,cacheWrite:V.cacheWrite};for(let G of q){let H=J[G],B=H.providers.get(V.provider);if(!B)B=oJ(),H.providers.set(V.provider,B);let P=B.models.get(V.model);if(!P)P=iJ(),B.models.set(V.model,P);if(P.sessions.add(Q),l(P,V.cost,K),B.sessions.add(Q),l(B,V.cost,K),l(H.totals,V.cost,K),V.cost>0&&V.timestamp>0){let I=new Date(V.timestamp).toISOString().slice(0,10);H.dailySpend.set(I,(H.dailySpend.get(I)??0)+V.cost)}F[G]=!0;let D=z[G];D.messages.push({sessionId:Q,timestamp:V.timestamp,cost:V.cost,input:V.input,cacheRead:V.cacheRead,cacheWrite:V.cacheWrite}),D.sessionCosts.set(Q,(D.sessionCosts.get(Q)??0)+V.cost)}}if(F.today)J.today.totals.sessions++;if(F.thisWeek)J.thisWeek.totals.sessions++;if(F.lastWeek)J.lastWeek.totals.sessions++;if(F.allTime)J.allTime.totals.sessions++}async function NJ(J){let Q=new Date;Q.setHours(0,0,0,0);let Z=Q.getTime(),$=new Date,Y=$.getDay(),X=Y===0?6:Y-1;$.setDate($.getDate()-X),$.setHours(0,0,0,0);let z=$.getTime(),j=new Date($);j.setDate(j.getDate()-7);let F=j.getTime(),V=nJ(),q={today:v(),thisWeek:v(),lastWeek:v(),allTime:v()},K=new Map,G=await cJ(J);if(J?.aborted)return null;let H=new Set;for(let P of G){if(J?.aborted)return null;let D=await rJ(P,H,J);if(J?.aborted)return null;if(!D)continue;eJ(V,D.sessionId,D.messages,Z,z,F,q,K),await new Promise((I)=>setImmediate(I))}let B=new Set;for(let[P,D]of K)if(D.endMs-D.startMs>=AJ)B.add(P);for(let P of k)V[P].insights=DQ(q[P],B);return V}var y=["use","show","rotation","verify","path","reset","help"],aJ=["manual","quota","all"],CJ=["Usage: /usage [use|show|rotation|verify|path|reset|help]","use: select, activate, or remove managed account","show: managed account and usage summary","rotation: current rotation behavior","verify: runtime health checks","path: storage and settings locations","reset: clear manual or quota state"].join(`
3
- `);function tJ(J){let Q=J.trim();if(!Q)return{rest:""};let Z=Q.indexOf(" ");if(Z<0)return{subcommand:Q.toLowerCase(),rest:""};return{subcommand:Q.slice(0,Z).toLowerCase(),rest:Q.slice(Z+1).trim()}}function JQ(J){return y.some((Q)=>Q===J)}function QQ(J){if(J==="manual"||J==="quota"||J==="all")return J;return}function $J(J){return J.map((Q)=>({value:Q,label:Q}))}function ZQ(J){let Q=J.trimStart();if(!Q)return $J(y);let Z=Q.indexOf(" ");if(Z<0){let X=y.filter((z)=>z.startsWith(Q.toLowerCase()));return X.length>0?$J(X):null}let $=Q.slice(0,Z).toLowerCase(),Y=Q.slice(Z+1).toLowerCase();if($==="reset"){let X=aJ.filter((z)=>z.startsWith(Y));return X.length>0?X.map((z)=>({value:`reset ${z}`,label:z})):null}if($==="use"){let X=["clear","remove"].filter((z)=>z.startsWith(Y));return X.length>0?X.map((z)=>({value:`use ${z}`,label:z})):null}return null}function r(){return{version:1,removedAccountIds:[],accounts:[],updatedAt:new Date(0).toISOString()}}function L(J){return typeof J==="string"&&J.trim()?J.trim():void 0}function $Q(J){if(!J||typeof J!=="object")return;let Q=J,Z=L(Q.id),$=L(Q.provider)??Z;if(!Z||!$)return;let Y=L(Q.label)??S($),X=Q.source==="manual"?"manual":"session",z=L(Q.updatedAt)??new Date(0).toISOString(),j=typeof Q.quotaExhaustedUntil==="number"&&Number.isFinite(Q.quotaExhaustedUntil)?Q.quotaExhaustedUntil:void 0;return{id:Z,label:Y,provider:$,source:X,updatedAt:z,quotaExhaustedUntil:j}}function VQ(J){let Q=r();if(!J||typeof J!=="object")return Q;let Z=J,$=Array.isArray(Z.accounts)?Z.accounts.map($Q).filter((X)=>Boolean(X)):[],Y=Array.isArray(Z.removedAccountIds)?Z.removedAccountIds.map(L).filter((X)=>Boolean(X)):[];return{version:1,manualAccountId:L(Z.manualAccountId),removedAccountIds:Y,accounts:$,updatedAt:L(Z.updatedAt)??Q.updatedAt}}function YQ(J){return J&&typeof J==="object"&&"code"in J?String(J.code):void 0}async function t(){try{let J=await kJ(A(),"utf8");return{state:VQ(JSON.parse(J))}}catch(J){if(YQ(J)==="ENOENT")return{state:r()};return{state:r(),error:J instanceof Error?J.message:String(J)}}}async function g(J){J.version=1,J.updatedAt=new Date().toISOString(),J.removedAccountIds=[...new Set(J.removedAccountIds)].sort(),J.accounts=J.accounts.filter((Q)=>!J.removedAccountIds.includes(Q.id)).sort((Q,Z)=>Q.label.localeCompare(Z.label)),await IJ(EJ(A()),{recursive:!0}),await yJ(A(),`${JSON.stringify(J,null,2)}
4
- `,"utf8")}function U(J){return typeof J.quotaExhaustedUntil==="number"&&J.quotaExhaustedUntil>Date.now()}function i(J,Q){return J.allTime.providers.get(Q.provider)}function zQ(J,Q){let Z=new Date().toISOString(),$=new Map;for(let Y of Q.accounts)if(!Q.removedAccountIds.includes(Y.id))$.set(Y.id,Y);for(let Y of J.allTime.providers.keys()){if(Q.removedAccountIds.includes(Y)||$.has(Y))continue;$.set(Y,{id:Y,label:S(Y),provider:Y,source:"session",updatedAt:Z})}return Array.from($.values()).sort((Y,X)=>{let z=Q.manualAccountId;if(Y.id===z&&X.id!==z)return-1;if(X.id===z&&Y.id!==z)return 1;if(U(Y)!==U(X))return U(Y)?1:-1;let j=(i(J,X)?.cost??0)-(i(J,Y)?.cost??0);if(j!==0)return j;return Y.label.localeCompare(X.label)})}function MJ(J,Q){return J.find(($)=>$.id===Q.manualAccountId&&!U($))??J.find(($)=>!U($))}function VJ(J,Q){let Z=Q.trim().toLowerCase();return J.find(($)=>$.id.toLowerCase()===Z||$.provider.toLowerCase()===Z||$.label.toLowerCase()===Z)}function o(J,Q,Z){let $=i(Q,J),Y=[Z.manualAccountId===J.id?"manual":void 0,U(J)?"quota":void 0,J.source==="manual"?"manual-entry":void 0].filter(Boolean).join(", "),X=Y?` (${Y})`:"",z=$?N($.cost):N(0),j=$?E($.messages):"0",F=$?E($.sessions.size):"0";return`${J.label}${X} · id=${J.id} · spend=${z} · messages=${j} · sessions=${F}`}async function JJ(J){let Q=await NJ(J.signal);if(!Q)return J.ui.notify("Usage data unavailable or cancelled.","warning"),null;let{state:Z,error:$}=await t();if($)J.ui.notify(`dm-usage state unreadable; using empty state: ${$}`,"warning");return{data:Q,state:Z,accounts:zQ(Q,Z)}}function OJ(){return"No managed accounts found. Run /usage after at least one provider response, or use /usage use <provider-id> to create a local manual entry."}async function YJ(J,Q,Z){Q.state.manualAccountId=Z.id,Q.state.removedAccountIds=Q.state.removedAccountIds.filter(($)=>$!==Z.id),Q.state.accounts=Q.accounts.map(($)=>$.id===Z.id?{...$,updatedAt:new Date().toISOString()}:$),await g(Q.state),J.ui.notify(`dm-usage: now using ${Z.label} (${Z.id})`,"info")}async function zJ(J,Q){let Z=Boolean(Q.manualAccountId);Q.manualAccountId=void 0,await g(Q),J.ui.notify(`dm-usage: manual account ${Z?"cleared":"was not set"}`,"info")}async function XJ(J,Q,Z){if(Q.state.removedAccountIds=[...Q.state.removedAccountIds,Z.id],Q.state.manualAccountId===Z.id)Q.state.manualAccountId=void 0;Q.state.accounts=Q.accounts.filter(($)=>$.id!==Z.id),await g(Q.state),J.ui.notify(`dm-usage: removed local managed-account state for ${Z.label}; session data was not changed.`,"info")}async function TJ(J,Q){let Z=await JJ(J);if(!Z)return;if(Q==="clear"){await zJ(J,Z.state);return}if(Q.startsWith("remove ")){let j=VJ(Z.accounts,Q.slice(7));if(!j){J.ui.notify(`Unknown managed account: ${Q.slice(7)}`,"warning");return}await XJ(J,Z,j);return}if(Q){let j=VJ(Z.accounts,Q)??{id:Q,label:S(Q),provider:Q,source:"manual",updatedAt:new Date().toISOString()};if(!Z.accounts.some((F)=>F.id===j.id))Z.accounts.push(j);await YJ(J,Z,j);return}if(!J.hasUI){J.ui.notify(Z.accounts.length?Z.accounts.map((j)=>o(j,Z.data,Z.state)).join(`
5
- `):OJ(),Z.accounts.length?"info":"warning");return}let $=[...Z.accounts.map((j)=>o(j,Z.data,Z.state)),"add manual account","clear manual account"],Y=await J.ui.select("DuckMind Usage Accounts",$);if(!Y)return;if(Y==="add manual account"){let j=(await J.ui.input("Managed account id (provider key, no secret)"))?.trim();if(!j)return;await TJ(J,j);return}if(Y==="clear manual account"){await zJ(J,Z.state);return}let X=Z.accounts[$.indexOf(Y)];if(!X)return;let z=await J.ui.select("Account Action",["activate","remove local state","cancel"]);if(z==="activate")await YJ(J,Z,X);if(z==="remove local state")await XJ(J,Z,X)}async function WJ(J){let Q=await JJ(J);if(!Q)return;let Z=MJ(Q.accounts,Q.state),$=Q.state.removedAccountIds.length,Y=["DuckMind Usage managed account summary",`active: ${Z?`${Z.label} (${Z.id})`:"none"}`,`manual: ${Q.state.manualAccountId??"none"}`,`accounts: ${Q.accounts.length}${$?` · hidden=${$}`:""}`,`all-time: spend=${N(Q.data.allTime.totals.cost)} · messages=${E(Q.data.allTime.totals.messages)} · sessions=${E(Q.data.allTime.totals.sessions)} · tokens=${O(Q.data.allTime.totals.tokens.total)}`,"",...Q.accounts.length?Q.accounts.map((X)=>o(X,Q.data,Q.state)):[OJ()]];if(J.hasUI)await J.ui.select("DuckMind Usage Summary",Y);else J.ui.notify(Y.join(`
6
- `),"info")}async function XQ(J){let Q=["Current policy: manual account first, then accounts without quota cooldown, then highest all-time local spend, then alphabetical fallback.","dm-usage is observational: this state changes /usage account summaries only and does not route provider requests or store API secrets.","Use /usage reset manual to clear manual selection, or /usage reset quota to clear quota cooldown markers."];if(J.hasUI)await J.ui.select("DuckMind Usage Rotation",Q);else J.ui.notify(Q.join(" "),"info")}async function jQ(J){try{return await RJ(J,d.R_OK),!0}catch{return!1}}async function jJ(J){try{let Q=EJ(J);return await IJ(Q,{recursive:!0}),await RJ(Q,d.R_OK|d.W_OK),!0}catch{return!1}}async function qQ(J){let Q=await jQ(e()),Z=await jJ(A()),$=await jJ(UJ()),Y=await t(),X=await JJ(J),z=X?.accounts.length??0,j=X?MJ(X.accounts,X.state)?.id??"none":"none",F=Q&&Z&&$&&!Y.error,V=[`verify: ${F?"PASS":"WARN"}`,`sessions readable: ${Q?"yes":"no"}`,`state directory writable: ${Z?"yes":"no"}`,`settings directory writable: ${$?"yes":"no"}`,`state json: ${Y.error?`invalid (${Y.error})`:"ok"}`,`managed accounts: ${z}`,`active account: ${j}`];if(J.hasUI)await J.ui.select(`DuckMind Usage Verify (${F?"PASS":"WARN"})`,V);else J.ui.notify(V.join(`
7
- `),F?"info":"warning")}async function FQ(J){let Q=[`Session storage: ${e()}`,`Managed account state: ${A()}`,`Agent settings: ${UJ()}`];if(J.hasUI)await J.ui.select("DuckMind Usage Paths",Q);else J.ui.notify(Q.join(`
8
- `),"info")}async function GQ(J,Q){let Z=QQ(Q.toLowerCase());if(Z)return Z;if(Q){J.ui.notify("Unknown reset target. Use: /usage reset [manual|quota|all]","warning");return}if(!J.hasUI)return"all";let $=await J.ui.select("Reset DuckMind Usage State",["manual - clear manual account override","quota - clear quota cooldown markers","all - clear manual override, quota cooldown markers, and hidden account state"]);if(!$)return;if($.startsWith("manual"))return"manual";if($.startsWith("quota"))return"quota";return"all"}async function KQ(J,Q){let Z=await GQ(J,Q);if(!Z)return;let{state:$}=await t(),Y=Boolean($.manualAccountId);if(Z==="manual"||Z==="all")$.manualAccountId=void 0;let X=0;if(Z==="quota"||Z==="all")$.accounts=$.accounts.map((j)=>{if(j.quotaExhaustedUntil===void 0)return j;X++;let{quotaExhaustedUntil:F,...V}=j;return V});let z=Z==="all"?$.removedAccountIds.length:0;if(Z==="all")$.removedAccountIds=[];await g($),J.ui.notify(`reset: target=${Z} manualCleared=${Y&&!$.manualAccountId?"yes":"no"} quotaCleared=${X} hiddenCleared=${z}`,"info")}async function HQ(J,Q,Z){if(J==="use")return TJ(Z,Q);if(J==="show")return WJ(Z);if(J==="rotation")return XQ(Z);if(J==="verify")return qQ(Z);if(J==="path")return FQ(Z);if(J==="reset")return KQ(Z,Q);Z.ui.notify(CJ,"info")}var qJ=120000,n=4,FJ=150000,GJ=1e5,AJ=28800000,KJ=5,BQ=10,PQ=1;function DQ(J,Q){if(J.messages.length===0)return{insights:[]};let Z=J.messages.reduce((V,q)=>V+q.cost,0);if(Z<=0)return{insights:[]};let $=[],Y=RQ(J.messages);if(Y!==null)$.push({percent:Y/Z*100,headline:`of your cost was while ${n}+ sessions ran in parallel`,advice:"All sessions share one rate limit. If you don't need them all at once, queueing uses capacity more evenly."});let X=J.messages.filter((V)=>V.input+V.cacheRead+V.cacheWrite>FJ).reduce((V,q)=>V+q.cost,0);$.push({percent:X/Z*100,headline:`of your cost was at >${HJ(FJ)} context`,advice:"Longer sessions are more expensive even when cached. /compact mid-task, /clear when switching to new tasks."});let z=J.messages.filter((V)=>V.input+V.cacheWrite>GJ).reduce((V,q)=>V+q.cost,0);$.push({percent:z/Z*100,headline:`of your cost came from >${HJ(GJ)}-token uncached prompts`,advice:"Uncached input is expensive, and often happens when sending a message to a session that has gone idle. /compact before stepping away keeps the cold-start small."});let j=J.messages.filter((V)=>Q.has(V.sessionId)).reduce((V,q)=>V+q.cost,0);if(j>0)$.push({percent:j/Z*100,headline:`of your cost came from sessions active for ${AJ/3600000}+ hours`,advice:"These are often background/loop sessions. Continuous usage can add up quickly so make sure it is intentional."});if(J.sessionCosts.size>KJ){let V=Array.from(J.sessionCosts.values()).sort((G,H)=>H-G),q=Math.min(KJ,V.length),K=V.slice(0,q).reduce((G,H)=>G+H,0);$.push({percent:K/Z*100,headline:`of your cost came from your top ${q} session${q===1?"":"s"}`,advice:"A small number of sessions drives most of your spend. The table view can help pinpoint which ones."})}return{insights:$.filter((V)=>V.percent>=PQ).sort((V,q)=>q.percent-V.percent)}}function RQ(J){let Q=J.filter((V)=>V.timestamp>0);if(Q.length<BQ)return null;if(new Set(Q.map((V)=>V.sessionId)).size<n)return null;let $=Q.slice().sort((V,q)=>V.timestamp-q.timestamp),Y=new Map,X=0,z=0,j=0,F=0;for(let V=0;V<$.length;V++){let q=$[V],K=q.timestamp+qJ,G=q.timestamp-qJ;while(j<$.length&&$[j].timestamp<=K){let H=$[j].sessionId,B=(Y.get(H)??0)+1;if(Y.set(H,B),B===1)X++;j++}while(z<j&&$[z].timestamp<G){let H=$[z].sessionId,B=(Y.get(H)??0)-1;if(B===0)Y.delete(H),X--;else Y.set(H,B);z++}if(X>=n)F+=q.cost}return F}function HJ(J){if(J>=1e6)return`${J/1e6}M`;if(J>=1000)return`${J/1000}k`;return String(J)}function IQ(J){if(J>=10)return`${Math.round(J)}%`;return`${Math.round(J*10)/10}%`}function N(J){if(J===0)return"-";if(J<0.01)return`$${J.toFixed(4)}`;if(J<1)return`$${J.toFixed(2)}`;if(J<10)return`$${J.toFixed(2)}`;if(J<100)return`$${J.toFixed(1)}`;return`$${Math.round(J)}`}function O(J){if(J===0)return"-";if(J<1000)return J.toString();if(J<1e4)return`${(J/1000).toFixed(1)}k`;if(J<1e6)return`${Math.round(J/1000)}k`;if(J<1e7)return`${(J/1e6).toFixed(1)}M`;return`${Math.round(J/1e6)}M`}function E(J){if(J===0)return"-";return J.toLocaleString()}function kQ(J,Q){let Z=C(J);if(Z>=Q)return J;return" ".repeat(Q-Z)+J}function fJ(J,Q){let Z=C(J);if(Z>=Q)return J;return J+" ".repeat(Q-Z)}function BJ(J){return J.reduce((Q,Z)=>Q+Z.width,0)}function T(J,Q,Z="left"){if(Q<=0)return"";let $=_(J,Q);return Z==="right"?kQ($,Q):fJ($,Q)}function f(J,Q){return J.map((Z)=>_(Z,Math.max(Q,0)))}function u(J,Q){for(let Z of Q)if(C(Z)<=J)return Z;return Q[Q.length-1]||""}function PJ(J){let Q=Math.max(J,0);for(let X of h){let z=BJ(X.columns),j=Math.min(m,Math.max(Q-z,0));if(j>=X.minNameWidth)return{columns:X.columns,nameWidth:j,tableWidth:j+z,compact:X.compact??!1}}let Z=h[h.length-1],$=BJ(Z.columns),Y=Math.min(m,Math.max(Q-$,0));return{columns:Z.columns,nameWidth:Y,tableWidth:Y+$,compact:Z.compact??!1}}function DJ(J,Q,Z,$){let Y=Math.max(Math.floor(Z),0);if(Y<12)return f([$.fg("accent",$.bold(J)),...Q,""],Y);let X=Math.max(Y-2,1),z=` ${J} `,j=_(z,Math.max(X-1,1),""),F=C(j),V=Math.max(X-F,0),q=`╭${j}${"─".repeat(V)}╮`,K=`╰${"─".repeat(X)}╯`,G=Q.length>0?Q:[$.fg("dim","No data")];return[$.fg("border",q),...G.map((H)=>EQ(H,X,$)),$.fg("border",K),""]}function EQ(J,Q,Z){let $=_(J,Q),Y=Math.max(Q-C($),0);return`${Z.fg("border","│")}${$}${" ".repeat(Y)}${Z.fg("border","│")}`}function LQ(J,Q){if(J.size===0)return"No spend data";let Y=Array.from(J.keys()).sort().slice(-Math.max(1,Math.min(30,Q))).map((j)=>J.get(j)??0),X=Math.max(...Y);if(X<=0)return"No spend";let z=["▁","▂","▃","▄","▅","▆","▇","█"];return Y.map((j)=>{let F=Math.max(0,Math.min(z.length-1,Math.ceil(j/X*z.length)-1));return z[F]}).join("")}function UQ(J){let Q=J.dailySpend.size;if(Q===0)return"avg/day -";return`avg/day ${N(J.totals.cost/Q)}`}var x={today:"Today",thisWeek:"This Week",lastWeek:"Last Week",allTime:"All Time"},k=["today","thisWeek","lastWeek","allTime"];class wJ{activeTab="allTime";viewMode="table";data;selectedIndex=0;expanded=new Set;providerOrder=[];theme;requestRender;done;constructor(J,Q,Z,$){this.theme=J,this.requestRender=Z,this.done=$,this.data=Q,this.updateProviderOrder()}updateProviderOrder(){let J=this.data[this.activeTab];this.providerOrder=Array.from(J.providers.entries()).sort((Q,Z)=>Z[1].cost-Q[1].cost).map(([Q])=>Q),this.selectedIndex=Math.min(this.selectedIndex,Math.max(0,this.providerOrder.length-1))}handleInput(J){if(R(J,"escape")||R(J,"q")){this.done();return}if(R(J,"v")){this.viewMode=this.viewMode==="table"?"insights":"table",this.requestRender();return}if(R(J,"tab")||R(J,"right")){let Q=k.indexOf(this.activeTab);this.activeTab=k[(Q+1)%k.length],this.updateProviderOrder(),this.requestRender()}else if(R(J,"shift+tab")||R(J,"left")){let Q=k.indexOf(this.activeTab);this.activeTab=k[(Q-1+k.length)%k.length],this.updateProviderOrder(),this.requestRender()}else if(this.viewMode==="table"&&R(J,"up")){if(this.selectedIndex>0)this.selectedIndex--,this.requestRender()}else if(this.viewMode==="table"&&R(J,"down")){if(this.selectedIndex<this.providerOrder.length-1)this.selectedIndex++,this.requestRender()}else if(this.viewMode==="table"&&(R(J,"enter")||R(J,"space"))){let Q=this.providerOrder[this.selectedIndex];if(Q){if(this.expanded.has(Q))this.expanded.delete(Q);else this.expanded.add(Q);this.requestRender()}}}render(J){if(this.viewMode==="insights")return f([...this.renderTitle(J),...this.renderTabs(J,PJ(J)),...this.renderSpendChart(J),...this.renderInsights(J),...this.renderHelp(J)],J);let Q=PJ(J);return f([...this.renderTitle(J),...this.renderTabs(J,Q),...this.renderHeader(Q),...this.renderRows(Q),...this.renderTotals(Q),...this.renderFormulaNote(J),...this.renderHelp(J)],J)}renderTitle(J){let Q=this.theme,Z=this.data[this.activeTab],$=this.viewMode==="insights"?"Usage Insights":"Usage Statistics";return DJ("DuckMind Usage",[`${Q.fg("accent",Q.bold($))} · ${x[this.activeTab]}`,`Spend ${N(Z.totals.cost)} · ${E(Z.totals.messages)} messages · ${O(Z.totals.tokens.total)} tokens`,`Providers ${E(Z.providers.size)} · Sessions ${E(Z.totals.sessions)} · ${UQ(Z)}`],J,Q)}renderSpendChart(J){let Q=this.theme,Z=this.data[this.activeTab];return DJ("Daily Spend",[Q.fg("accent",LQ(Z.dailySpend,Math.max(J-4,10))),Q.fg("dim","Local session spend, last 30 active UTC days")],J,Q)}renderInsights(J){let Q=this.theme,Z=this.data[this.activeTab],{insights:$}=Z.insights,Y=Z.totals.messages>0,X=Z.totals.cost>0,z=[];z.push("What's contributing to your cost?"),z.push(Q.fg("dim","Approximate, based on local sessions on this machine.")),z.push("");let j=`${x[this.activeTab]} · weighted by cost (USD) · these overlap and can sum to >100%`;if(z.push(Q.fg("dim",j)),z.push(""),!Y)return z.push(Q.fg("dim"," No usage recorded for this period.")),z.push(""),z;if(!X)return z.push(Q.fg("dim"," No cost data recorded for this period.")),z.push(""),z;if($.length===0)return z.push(Q.fg("dim"," No insights above 1% for this period.")),z.push(""),z;let F=" ",V=Math.max(J-F.length,30);for(let q of $){let K=Q.fg("accent",Q.bold(IQ(q.percent)));z.push(`${K} ${q.headline}`);for(let G of ZJ(q.advice,V))z.push(`${F}${Q.fg("dim",G)}`);z.push("")}return z}renderTabs(J,Q){let Z=this.theme,$=k.map((j)=>{let F=x[j];return j===this.activeTab?Z.fg("accent",`[${F}]`):Z.fg("dim",` ${F} `)}).join(" "),Y=Z.fg("accent",`[${x[this.activeTab]}]`),X=u(J,[$,`${Y} ${Z.fg("dim","[Tab/←→]")}`,Y]),z=this.viewMode==="table"&&Q.compact?ZJ(Z.fg("dim","Compact view. Widen the terminal for more columns."),Math.max(J,1)):[];return[X,...z,""]}renderHeader(J){let Q=this.theme,Z=T("Provider / Model",J.nameWidth);for(let $ of J.columns){let Y=T($.label,$.width,"right");Z+=$.dimmed?Q.fg("dim",Y):Y}return[Q.fg("muted",Z),Q.fg("border","─".repeat(J.tableWidth))]}renderDataRow(J,Q,Z,$={}){let Y=this.theme,{indent:X=0,selected:z=!1,dimAll:j=!1,prefix:F}=$,V=F??" ".repeat(X),q=Z.nameWidth>0?_(V,Z.nameWidth,""):"",K=C(q),G=Math.max(Z.nameWidth-K,0),H=G>0?_(J,G):"",B=z?Y.fg("accent",H):j?Y.fg("dim",H):H,P=q+(G>0?fJ(B,G):"");for(let D of Z.columns){let I=T(D.getValue(Q),D.width,"right"),w=D.dimmed||j;P+=w?Y.fg("dim",I):I}return P}renderRows(J){let Q=this.theme,Z=this.data[this.activeTab],$=[];if(this.providerOrder.length===0)return $.push(Q.fg("dim"," No usage data for this period")),$;for(let Y=0;Y<this.providerOrder.length;Y++){let X=this.providerOrder[Y],z=Z.providers.get(X),j=Y===this.selectedIndex,F=this.expanded.has(X),V=F?"▾":"▸",q=j?Q.fg("accent",`${V} `):Q.fg("dim",`${V} `);if($.push(this.renderDataRow(S(X),z,J,{selected:j,prefix:q})),F){let K=Array.from(z.models.entries()).sort((G,H)=>H[1].cost-G[1].cost);for(let[G,H]of K)$.push(this.renderDataRow(G,H,J,{indent:4,dimAll:!0}))}}return $}renderTotals(J){let Q=this.theme,Z=this.data[this.activeTab],$=T(Q.bold("Total"),J.nameWidth);for(let Y of J.columns){let X=T(Y.getValue(Z.totals),Y.width,"right");$+=Y.dimmed?Q.fg("dim",X):X}return[Q.fg("border","─".repeat(J.tableWidth)),$,""]}renderFormulaNote(J){let Q=u(J,["Tokens = Input + Output + CacheWrite · ↑In = Input + CacheWrite (as of 0.2.0)","Tokens = In + Out + CacheWrite · ↑In = In + CacheWrite (v0.2.0+)","Tokens & ↑In include CacheWrite (v0.2.0+)","Incl. CacheWrite (v0.2.0+)"]);return[this.theme.fg("dim",Q),""]}renderHelp(J){let Q=this.viewMode==="insights"?["[Tab/←→] period [v] table view [q] close","[Tab] period [v] table [q] close","[v] table [q] close","[q] close"]:["[Tab/←→] period [↑↓] select [Enter] expand [v] insights [q] close","[Tab] period [↑↓] select [Enter] expand [v] insights [q] close","[↑↓] select [Enter] expand [v] insights [q] close","[↑↓] select [v] insights [q] close","[↑↓] select [q] close","[q] close"],Z=u(J,Q);return[this.theme.fg("dim",Z)]}invalidate(){}dispose(){}}function _Q(J){J.registerCommand("usage",{description:`Show usage statistics dashboard and account tools: ${y.join(", ")}`,getArgumentCompletions:ZQ,handler:async(Q,Z)=>{let $=tJ(Q);if($.subcommand){if(!JQ($.subcommand)){Z.ui.notify(`Unknown /usage subcommand: ${$.subcommand}
9
- ${CJ}`,"warning");return}await HQ($.subcommand,$.rest,Z);return}if(!Z.hasUI){await WJ(Z);return}let Y=await Z.ui.custom((X,z,j,F)=>{let V=new vJ(X,(G)=>z.fg("accent",G),(G)=>z.fg("muted",G),"Loading Usage..."),q=!1,K=(G)=>{if(q)return;q=!0,V.dispose(),F(G)};return V.onAbort=()=>K(null),NJ(V.signal).then(K).catch(()=>K(null)),V});if(!Y)return;await Z.ui.custom((X,z,j,F)=>{let V=new xJ;V.addChild(new QJ(1)),V.addChild(new bJ((K)=>z.fg("border",K))),V.addChild(new QJ(1));let q=new wJ(z,Y,()=>X.requestRender(),()=>F());return{render:(K)=>{let G=f(V.render(K),K),H=q.render(K),B=z.fg("border","─".repeat(K));return f([...G,...H,"",B],K)},invalidate:()=>V.invalidate(),handleInput:(K)=>q.handleInput(K),dispose:()=>{}}})}})}export{_Q as default};
1
+ import { DynamicBorder } from "@duckmind/dm-coding-agent";
2
+ import { CancellableLoader, Container, Spacer, matchesKey, visibleWidth, truncateToWidth, wrapTextWithAnsi } from "@duckmind/dm-tui";
3
+ import { constants as fsConstants } from "node:fs";
4
+ import { access, mkdir, readdir, readFile, writeFile } from "node:fs/promises";
5
+ import { dirname, join } from "node:path";
6
+ import { homedir } from "node:os";
7
+ const MAX_NAME_COL_WIDTH = 26;
8
+ const PROVIDER_DISPLAY_NAMES = {
9
+ "openai-codex": "duckmind-ultra",
10
+ openrouter: "duckmind-standard"
11
+ };
12
+ function formatProviderName(provider) {
13
+ return PROVIDER_DISPLAY_NAMES[provider] ?? provider;
14
+ }
15
+ const SESSIONS_COLUMN = {
16
+ label: "Sessions",
17
+ width: 9,
18
+ getValue: (s) => formatNumber(typeof s.sessions === "number" ? s.sessions : s.sessions.size)
19
+ };
20
+ const MSGS_COLUMN = {
21
+ label: "Msgs",
22
+ width: 9,
23
+ getValue: (s) => formatNumber(s.messages)
24
+ };
25
+ const COST_COLUMN = {
26
+ label: "Cost",
27
+ width: 9,
28
+ getValue: (s) => formatCost(s.cost)
29
+ };
30
+ const TOKENS_COLUMN = {
31
+ label: "Tokens",
32
+ width: 9,
33
+ getValue: (s) => formatTokens(s.tokens.total)
34
+ };
35
+ const INPUT_COLUMN = {
36
+ label: "↑In",
37
+ width: 8,
38
+ dimmed: true,
39
+ getValue: (s) => formatTokens(s.tokens.input + s.tokens.cacheWrite)
40
+ };
41
+ const OUTPUT_COLUMN = {
42
+ label: "↓Out",
43
+ width: 8,
44
+ dimmed: true,
45
+ getValue: (s) => formatTokens(s.tokens.output)
46
+ };
47
+ const CACHE_COLUMN = {
48
+ label: "Cache",
49
+ width: 8,
50
+ dimmed: true,
51
+ getValue: (s) => formatTokens(s.tokens.cacheRead + s.tokens.cacheWrite)
52
+ };
53
+ const FULL_DATA_COLUMNS = [
54
+ SESSIONS_COLUMN,
55
+ MSGS_COLUMN,
56
+ COST_COLUMN,
57
+ TOKENS_COLUMN,
58
+ INPUT_COLUMN,
59
+ OUTPUT_COLUMN,
60
+ CACHE_COLUMN
61
+ ];
62
+ const TABLE_LAYOUTS = [
63
+ { columns: FULL_DATA_COLUMNS, minNameWidth: MAX_NAME_COL_WIDTH },
64
+ { columns: [SESSIONS_COLUMN, MSGS_COLUMN, COST_COLUMN, TOKENS_COLUMN], minNameWidth: 14, compact: true },
65
+ { columns: [SESSIONS_COLUMN, COST_COLUMN, TOKENS_COLUMN], minNameWidth: 12, compact: true },
66
+ { columns: [COST_COLUMN, TOKENS_COLUMN], minNameWidth: 10, compact: true },
67
+ { columns: [COST_COLUMN], minNameWidth: 8, compact: true }
68
+ ];
69
+ function getAgentDir() {
70
+ return process.env.DM_CODING_AGENT_DIR || process.env.PI_CODING_AGENT_DIR || join(homedir(), ".dm", "agent");
71
+ }
72
+ function getSessionsDir() {
73
+ return join(getAgentDir(), "sessions");
74
+ }
75
+ function getUsageStateDir() {
76
+ return join(getAgentDir(), "usage");
77
+ }
78
+ function getUsageStatePath() {
79
+ return join(getUsageStateDir(), "state.json");
80
+ }
81
+ function getAgentSettingsPath() {
82
+ return join(getAgentDir(), "settings.json");
83
+ }
84
+ async function collectSessionFilesRecursively(dir, files, signal) {
85
+ try {
86
+ const entries = await readdir(dir, { withFileTypes: true });
87
+ for (const entry of entries) {
88
+ if (signal?.aborted)
89
+ return;
90
+ const entryPath = join(dir, entry.name);
91
+ if (entry.isDirectory()) {
92
+ await collectSessionFilesRecursively(entryPath, files, signal);
93
+ } else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
94
+ files.push(entryPath);
95
+ }
96
+ }
97
+ } catch {}
98
+ }
99
+ async function getAllSessionFiles(signal) {
100
+ const files = [];
101
+ await collectSessionFilesRecursively(getSessionsDir(), files, signal);
102
+ files.sort();
103
+ return files;
104
+ }
105
+ async function parseSessionFile(filePath, seenHashes, signal) {
106
+ try {
107
+ const content = await readFile(filePath, "utf8");
108
+ if (signal?.aborted)
109
+ return null;
110
+ const lines = content.trim().split(`
111
+ `);
112
+ const messages = [];
113
+ let sessionId = "";
114
+ for (let i = 0;i < lines.length; i++) {
115
+ if (signal?.aborted)
116
+ return null;
117
+ if (i % 500 === 0) {
118
+ await new Promise((resolve) => setImmediate(resolve));
119
+ }
120
+ const line = lines[i];
121
+ if (!line.trim())
122
+ continue;
123
+ try {
124
+ const entry = JSON.parse(line);
125
+ if (entry.type === "session") {
126
+ sessionId = entry.id;
127
+ } else if (entry.type === "message" && entry.message?.role === "assistant") {
128
+ const msg = entry.message;
129
+ if (msg.usage && msg.provider && msg.model) {
130
+ const input = msg.usage.input || 0;
131
+ const output = msg.usage.output || 0;
132
+ const cacheRead = msg.usage.cacheRead || 0;
133
+ const cacheWrite = msg.usage.cacheWrite || 0;
134
+ const fallbackTs = entry.timestamp ? new Date(entry.timestamp).getTime() : 0;
135
+ const timestamp = msg.timestamp || (Number.isNaN(fallbackTs) ? 0 : fallbackTs);
136
+ const totalTokens = input + output + cacheRead + cacheWrite;
137
+ const hash = `${timestamp}:${totalTokens}`;
138
+ if (seenHashes.has(hash))
139
+ continue;
140
+ seenHashes.add(hash);
141
+ messages.push({
142
+ provider: msg.provider,
143
+ model: msg.model,
144
+ cost: msg.usage.cost?.total || 0,
145
+ input,
146
+ output,
147
+ cacheRead,
148
+ cacheWrite,
149
+ timestamp
150
+ });
151
+ }
152
+ }
153
+ } catch {}
154
+ }
155
+ return sessionId ? { sessionId, messages } : null;
156
+ } catch {
157
+ return null;
158
+ }
159
+ }
160
+ function accumulateStats(target, cost, tokens) {
161
+ target.messages++;
162
+ target.cost += cost;
163
+ target.tokens.total += tokens.total;
164
+ target.tokens.input += tokens.input;
165
+ target.tokens.output += tokens.output;
166
+ target.tokens.cacheRead += tokens.cacheRead;
167
+ target.tokens.cacheWrite += tokens.cacheWrite;
168
+ }
169
+ function emptyTokens() {
170
+ return { total: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
171
+ }
172
+ function emptyModelStats() {
173
+ return { sessions: new Set, messages: 0, cost: 0, tokens: emptyTokens() };
174
+ }
175
+ function emptyProviderStats() {
176
+ return { sessions: new Set, messages: 0, cost: 0, tokens: emptyTokens(), models: new Map };
177
+ }
178
+ function emptyTimeFilteredStats() {
179
+ return {
180
+ providers: new Map,
181
+ totals: { sessions: 0, messages: 0, cost: 0, tokens: emptyTokens() },
182
+ dailySpend: new Map,
183
+ insights: { insights: [] }
184
+ };
185
+ }
186
+ function emptyPeriodRawData() {
187
+ return { messages: [], sessionCosts: new Map };
188
+ }
189
+ function emptyUsageData() {
190
+ return {
191
+ today: emptyTimeFilteredStats(),
192
+ thisWeek: emptyTimeFilteredStats(),
193
+ lastWeek: emptyTimeFilteredStats(),
194
+ allTime: emptyTimeFilteredStats()
195
+ };
196
+ }
197
+ function getPeriodsForTimestamp(timestamp, todayMs, weekStartMs, lastWeekStartMs) {
198
+ const periods = ["allTime"];
199
+ if (timestamp >= todayMs)
200
+ periods.push("today");
201
+ if (timestamp >= weekStartMs) {
202
+ periods.push("thisWeek");
203
+ } else if (timestamp >= lastWeekStartMs) {
204
+ periods.push("lastWeek");
205
+ }
206
+ return periods;
207
+ }
208
+ function addMessagesToUsageData(data, sessionId, messages, todayMs, weekStartMs, lastWeekStartMs, rawByPeriod, globalSessionSpans) {
209
+ const sessionContributed = { today: false, thisWeek: false, lastWeek: false, allTime: false };
210
+ for (const msg of messages) {
211
+ if (msg.timestamp > 0) {
212
+ const span = globalSessionSpans.get(sessionId);
213
+ if (!span) {
214
+ globalSessionSpans.set(sessionId, { startMs: msg.timestamp, endMs: msg.timestamp });
215
+ } else {
216
+ if (msg.timestamp < span.startMs)
217
+ span.startMs = msg.timestamp;
218
+ if (msg.timestamp > span.endMs)
219
+ span.endMs = msg.timestamp;
220
+ }
221
+ }
222
+ const periods = getPeriodsForTimestamp(msg.timestamp, todayMs, weekStartMs, lastWeekStartMs);
223
+ const tokens = {
224
+ total: msg.input + msg.output + msg.cacheWrite,
225
+ input: msg.input,
226
+ output: msg.output,
227
+ cacheRead: msg.cacheRead,
228
+ cacheWrite: msg.cacheWrite
229
+ };
230
+ for (const period of periods) {
231
+ const stats = data[period];
232
+ let providerStats = stats.providers.get(msg.provider);
233
+ if (!providerStats) {
234
+ providerStats = emptyProviderStats();
235
+ stats.providers.set(msg.provider, providerStats);
236
+ }
237
+ let modelStats = providerStats.models.get(msg.model);
238
+ if (!modelStats) {
239
+ modelStats = emptyModelStats();
240
+ providerStats.models.set(msg.model, modelStats);
241
+ }
242
+ modelStats.sessions.add(sessionId);
243
+ accumulateStats(modelStats, msg.cost, tokens);
244
+ providerStats.sessions.add(sessionId);
245
+ accumulateStats(providerStats, msg.cost, tokens);
246
+ accumulateStats(stats.totals, msg.cost, tokens);
247
+ if (msg.cost > 0 && msg.timestamp > 0) {
248
+ const dayKey = new Date(msg.timestamp).toISOString().slice(0, 10);
249
+ stats.dailySpend.set(dayKey, (stats.dailySpend.get(dayKey) ?? 0) + msg.cost);
250
+ }
251
+ sessionContributed[period] = true;
252
+ const raw = rawByPeriod[period];
253
+ raw.messages.push({
254
+ sessionId,
255
+ timestamp: msg.timestamp,
256
+ cost: msg.cost,
257
+ input: msg.input,
258
+ cacheRead: msg.cacheRead,
259
+ cacheWrite: msg.cacheWrite
260
+ });
261
+ raw.sessionCosts.set(sessionId, (raw.sessionCosts.get(sessionId) ?? 0) + msg.cost);
262
+ }
263
+ }
264
+ if (sessionContributed.today)
265
+ data.today.totals.sessions++;
266
+ if (sessionContributed.thisWeek)
267
+ data.thisWeek.totals.sessions++;
268
+ if (sessionContributed.lastWeek)
269
+ data.lastWeek.totals.sessions++;
270
+ if (sessionContributed.allTime)
271
+ data.allTime.totals.sessions++;
272
+ }
273
+ async function collectUsageData(signal) {
274
+ const startOfToday = new Date;
275
+ startOfToday.setHours(0, 0, 0, 0);
276
+ const todayMs = startOfToday.getTime();
277
+ const startOfWeek = new Date;
278
+ const dayOfWeek = startOfWeek.getDay();
279
+ const daysSinceMonday = dayOfWeek === 0 ? 6 : dayOfWeek - 1;
280
+ startOfWeek.setDate(startOfWeek.getDate() - daysSinceMonday);
281
+ startOfWeek.setHours(0, 0, 0, 0);
282
+ const weekStartMs = startOfWeek.getTime();
283
+ const startOfLastWeek = new Date(startOfWeek);
284
+ startOfLastWeek.setDate(startOfLastWeek.getDate() - 7);
285
+ const lastWeekStartMs = startOfLastWeek.getTime();
286
+ const data = emptyUsageData();
287
+ const rawByPeriod = {
288
+ today: emptyPeriodRawData(),
289
+ thisWeek: emptyPeriodRawData(),
290
+ lastWeek: emptyPeriodRawData(),
291
+ allTime: emptyPeriodRawData()
292
+ };
293
+ const globalSessionSpans = new Map;
294
+ const sessionFiles = await getAllSessionFiles(signal);
295
+ if (signal?.aborted)
296
+ return null;
297
+ const seenHashes = new Set;
298
+ for (const filePath of sessionFiles) {
299
+ if (signal?.aborted)
300
+ return null;
301
+ const parsed = await parseSessionFile(filePath, seenHashes, signal);
302
+ if (signal?.aborted)
303
+ return null;
304
+ if (!parsed)
305
+ continue;
306
+ addMessagesToUsageData(data, parsed.sessionId, parsed.messages, todayMs, weekStartMs, lastWeekStartMs, rawByPeriod, globalSessionSpans);
307
+ await new Promise((resolve) => setImmediate(resolve));
308
+ }
309
+ const longSessionIds = new Set;
310
+ for (const [id, span] of globalSessionSpans) {
311
+ if (span.endMs - span.startMs >= LONG_SESSION_MS)
312
+ longSessionIds.add(id);
313
+ }
314
+ for (const period of TAB_ORDER) {
315
+ data[period].insights = computeInsights(rawByPeriod[period], longSessionIds);
316
+ }
317
+ return data;
318
+ }
319
+ const USAGE_SUBCOMMANDS = ["use", "show", "rotation", "verify", "path", "reset", "help"];
320
+ const USAGE_RESET_TARGETS = ["manual", "quota", "all"];
321
+ const USAGE_HELP_TEXT = [
322
+ "Usage: /usage [use|show|rotation|verify|path|reset|help]",
323
+ "use: select, activate, or remove managed account",
324
+ "show: managed account and usage summary",
325
+ "rotation: current rotation behavior",
326
+ "verify: runtime health checks",
327
+ "path: storage and settings locations",
328
+ "reset: clear manual or quota state"
329
+ ].join(`
330
+ `);
331
+ function parseUsageCommandArgs(args) {
332
+ const trimmed = args.trim();
333
+ if (!trimmed)
334
+ return { rest: "" };
335
+ const firstSpaceIndex = trimmed.indexOf(" ");
336
+ if (firstSpaceIndex < 0)
337
+ return { subcommand: trimmed.toLowerCase(), rest: "" };
338
+ return {
339
+ subcommand: trimmed.slice(0, firstSpaceIndex).toLowerCase(),
340
+ rest: trimmed.slice(firstSpaceIndex + 1).trim()
341
+ };
342
+ }
343
+ function isUsageSubcommand(value) {
344
+ return USAGE_SUBCOMMANDS.some((subcommand) => subcommand === value);
345
+ }
346
+ function parseUsageResetTarget(value) {
347
+ if (value === "manual" || value === "quota" || value === "all")
348
+ return value;
349
+ return;
350
+ }
351
+ function toAutocompleteItems(values) {
352
+ return values.map((value) => ({ value, label: value }));
353
+ }
354
+ function getUsageCommandCompletions(argumentPrefix) {
355
+ const trimmedStart = argumentPrefix.trimStart();
356
+ if (!trimmedStart)
357
+ return toAutocompleteItems(USAGE_SUBCOMMANDS);
358
+ const firstSpaceIndex = trimmedStart.indexOf(" ");
359
+ if (firstSpaceIndex < 0) {
360
+ const matches = USAGE_SUBCOMMANDS.filter((value) => value.startsWith(trimmedStart.toLowerCase()));
361
+ return matches.length > 0 ? toAutocompleteItems(matches) : null;
362
+ }
363
+ const subcommand = trimmedStart.slice(0, firstSpaceIndex).toLowerCase();
364
+ const rest = trimmedStart.slice(firstSpaceIndex + 1).toLowerCase();
365
+ if (subcommand === "reset") {
366
+ const matches = USAGE_RESET_TARGETS.filter((value) => value.startsWith(rest));
367
+ return matches.length > 0 ? matches.map((value) => ({ value: `reset ${value}`, label: value })) : null;
368
+ }
369
+ if (subcommand === "use") {
370
+ const matches = ["clear", "remove"].filter((value) => value.startsWith(rest));
371
+ return matches.length > 0 ? matches.map((value) => ({ value: `use ${value}`, label: value })) : null;
372
+ }
373
+ return null;
374
+ }
375
+ function emptyUsageManagedState() {
376
+ return { version: 1, removedAccountIds: [], accounts: [], updatedAt: new Date(0).toISOString() };
377
+ }
378
+ function asString(value) {
379
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
380
+ }
381
+ function normalizeUsageManagedAccount(value) {
382
+ if (!value || typeof value !== "object")
383
+ return;
384
+ const record = value;
385
+ const id = asString(record.id);
386
+ const provider = asString(record.provider) ?? id;
387
+ if (!id || !provider)
388
+ return;
389
+ const label = asString(record.label) ?? formatProviderName(provider);
390
+ const source = record.source === "manual" ? "manual" : "session";
391
+ const updatedAt = asString(record.updatedAt) ?? new Date(0).toISOString();
392
+ const quota = typeof record.quotaExhaustedUntil === "number" && Number.isFinite(record.quotaExhaustedUntil) ? record.quotaExhaustedUntil : undefined;
393
+ return { id, label, provider, source, updatedAt, quotaExhaustedUntil: quota };
394
+ }
395
+ function normalizeUsageManagedState(value) {
396
+ const empty = emptyUsageManagedState();
397
+ if (!value || typeof value !== "object")
398
+ return empty;
399
+ const record = value;
400
+ const accounts = Array.isArray(record.accounts) ? record.accounts.map(normalizeUsageManagedAccount).filter((account) => Boolean(account)) : [];
401
+ const removedAccountIds = Array.isArray(record.removedAccountIds) ? record.removedAccountIds.map(asString).filter((id) => Boolean(id)) : [];
402
+ return {
403
+ version: 1,
404
+ manualAccountId: asString(record.manualAccountId),
405
+ removedAccountIds,
406
+ accounts,
407
+ updatedAt: asString(record.updatedAt) ?? empty.updatedAt
408
+ };
409
+ }
410
+ function getErrorCode(error) {
411
+ return error && typeof error === "object" && "code" in error ? String(error.code) : undefined;
412
+ }
413
+ async function readUsageManagedState() {
414
+ try {
415
+ const raw = await readFile(getUsageStatePath(), "utf8");
416
+ return { state: normalizeUsageManagedState(JSON.parse(raw)) };
417
+ } catch (error) {
418
+ if (getErrorCode(error) === "ENOENT")
419
+ return { state: emptyUsageManagedState() };
420
+ return { state: emptyUsageManagedState(), error: error instanceof Error ? error.message : String(error) };
421
+ }
422
+ }
423
+ async function saveUsageManagedState(state) {
424
+ state.version = 1;
425
+ state.updatedAt = new Date().toISOString();
426
+ state.removedAccountIds = [...new Set(state.removedAccountIds)].sort();
427
+ state.accounts = state.accounts.filter((account) => !state.removedAccountIds.includes(account.id)).sort((a, b) => a.label.localeCompare(b.label));
428
+ await mkdir(dirname(getUsageStatePath()), { recursive: true });
429
+ await writeFile(getUsageStatePath(), `${JSON.stringify(state, null, 2)}
430
+ `, "utf8");
431
+ }
432
+ function isQuotaActive(account) {
433
+ return typeof account.quotaExhaustedUntil === "number" && account.quotaExhaustedUntil > Date.now();
434
+ }
435
+ function getAccountStats(data, account) {
436
+ return data.allTime.providers.get(account.provider);
437
+ }
438
+ function getManagedAccounts(data, state) {
439
+ const now = new Date().toISOString();
440
+ const accounts = new Map;
441
+ for (const account of state.accounts) {
442
+ if (!state.removedAccountIds.includes(account.id))
443
+ accounts.set(account.id, account);
444
+ }
445
+ for (const provider of data.allTime.providers.keys()) {
446
+ if (state.removedAccountIds.includes(provider) || accounts.has(provider))
447
+ continue;
448
+ accounts.set(provider, {
449
+ id: provider,
450
+ label: formatProviderName(provider),
451
+ provider,
452
+ source: "session",
453
+ updatedAt: now
454
+ });
455
+ }
456
+ return Array.from(accounts.values()).sort((a, b) => {
457
+ const manualId = state.manualAccountId;
458
+ if (a.id === manualId && b.id !== manualId)
459
+ return -1;
460
+ if (b.id === manualId && a.id !== manualId)
461
+ return 1;
462
+ if (isQuotaActive(a) !== isQuotaActive(b))
463
+ return isQuotaActive(a) ? 1 : -1;
464
+ const costDelta = (getAccountStats(data, b)?.cost ?? 0) - (getAccountStats(data, a)?.cost ?? 0);
465
+ if (costDelta !== 0)
466
+ return costDelta;
467
+ return a.label.localeCompare(b.label);
468
+ });
469
+ }
470
+ function getActiveManagedAccount(accounts, state) {
471
+ const manual = accounts.find((account) => account.id === state.manualAccountId && !isQuotaActive(account));
472
+ return manual ?? accounts.find((account) => !isQuotaActive(account));
473
+ }
474
+ function findManagedAccount(accounts, query) {
475
+ const normalized = query.trim().toLowerCase();
476
+ return accounts.find((account) => account.id.toLowerCase() === normalized || account.provider.toLowerCase() === normalized || account.label.toLowerCase() === normalized);
477
+ }
478
+ function formatManagedAccountLine(account, data, state) {
479
+ const stats = getAccountStats(data, account);
480
+ const tags = [
481
+ state.manualAccountId === account.id ? "manual" : undefined,
482
+ isQuotaActive(account) ? "quota" : undefined,
483
+ account.source === "manual" ? "manual-entry" : undefined
484
+ ].filter(Boolean).join(", ");
485
+ const suffix = tags ? ` (${tags})` : "";
486
+ const cost = stats ? formatCost(stats.cost) : formatCost(0);
487
+ const messages = stats ? formatNumber(stats.messages) : "0";
488
+ const sessions = stats ? formatNumber(stats.sessions.size) : "0";
489
+ return `${account.label}${suffix} · id=${account.id} · spend=${cost} · messages=${messages} · sessions=${sessions}`;
490
+ }
491
+ async function loadUsageManagedSummary(ctx) {
492
+ const data = await collectUsageData(ctx.signal);
493
+ if (!data) {
494
+ ctx.ui.notify("Usage data unavailable or cancelled.", "warning");
495
+ return null;
496
+ }
497
+ const { state, error } = await readUsageManagedState();
498
+ if (error) {
499
+ ctx.ui.notify(`dm-usage state unreadable; using empty state: ${error}`, "warning");
500
+ }
501
+ return { data, state, accounts: getManagedAccounts(data, state) };
502
+ }
503
+ function managedAccountNoDataMessage() {
504
+ return "No managed accounts found. Run /usage after at least one provider response, or use /usage use <provider-id> to create a local manual entry.";
505
+ }
506
+ async function activateUsageAccount(ctx, summary, account) {
507
+ summary.state.manualAccountId = account.id;
508
+ summary.state.removedAccountIds = summary.state.removedAccountIds.filter((id) => id !== account.id);
509
+ summary.state.accounts = summary.accounts.map((item) => item.id === account.id ? { ...item, updatedAt: new Date().toISOString() } : item);
510
+ await saveUsageManagedState(summary.state);
511
+ ctx.ui.notify(`dm-usage: now using ${account.label} (${account.id})`, "info");
512
+ }
513
+ async function clearUsageManualAccount(ctx, state) {
514
+ const hadManual = Boolean(state.manualAccountId);
515
+ state.manualAccountId = undefined;
516
+ await saveUsageManagedState(state);
517
+ ctx.ui.notify(`dm-usage: manual account ${hadManual ? "cleared" : "was not set"}`, "info");
518
+ }
519
+ async function removeUsageAccountState(ctx, summary, account) {
520
+ summary.state.removedAccountIds = [...summary.state.removedAccountIds, account.id];
521
+ if (summary.state.manualAccountId === account.id)
522
+ summary.state.manualAccountId = undefined;
523
+ summary.state.accounts = summary.accounts.filter((item) => item.id !== account.id);
524
+ await saveUsageManagedState(summary.state);
525
+ ctx.ui.notify(`dm-usage: removed local managed-account state for ${account.label}; session data was not changed.`, "info");
526
+ }
527
+ async function runUsageUseSubcommand(ctx, rest) {
528
+ const summary = await loadUsageManagedSummary(ctx);
529
+ if (!summary)
530
+ return;
531
+ if (rest === "clear") {
532
+ await clearUsageManualAccount(ctx, summary.state);
533
+ return;
534
+ }
535
+ if (rest.startsWith("remove ")) {
536
+ const account = findManagedAccount(summary.accounts, rest.slice("remove ".length));
537
+ if (!account) {
538
+ ctx.ui.notify(`Unknown managed account: ${rest.slice("remove ".length)}`, "warning");
539
+ return;
540
+ }
541
+ await removeUsageAccountState(ctx, summary, account);
542
+ return;
543
+ }
544
+ if (rest) {
545
+ const account = findManagedAccount(summary.accounts, rest) ?? { id: rest, label: formatProviderName(rest), provider: rest, source: "manual", updatedAt: new Date().toISOString() };
546
+ if (!summary.accounts.some((item) => item.id === account.id))
547
+ summary.accounts.push(account);
548
+ await activateUsageAccount(ctx, summary, account);
549
+ return;
550
+ }
551
+ if (!ctx.hasUI) {
552
+ ctx.ui.notify(summary.accounts.length ? summary.accounts.map((account) => formatManagedAccountLine(account, summary.data, summary.state)).join(`
553
+ `) : managedAccountNoDataMessage(), summary.accounts.length ? "info" : "warning");
554
+ return;
555
+ }
556
+ const options = [
557
+ ...summary.accounts.map((account) => formatManagedAccountLine(account, summary.data, summary.state)),
558
+ "add manual account",
559
+ "clear manual account"
560
+ ];
561
+ const selected = await ctx.ui.select("DuckMind Usage Accounts", options);
562
+ if (!selected)
563
+ return;
564
+ if (selected === "add manual account") {
565
+ const id = (await ctx.ui.input("Managed account id (provider key, no secret)"))?.trim();
566
+ if (!id)
567
+ return;
568
+ await runUsageUseSubcommand(ctx, id);
569
+ return;
570
+ }
571
+ if (selected === "clear manual account") {
572
+ await clearUsageManualAccount(ctx, summary.state);
573
+ return;
574
+ }
575
+ const account = summary.accounts[options.indexOf(selected)];
576
+ if (!account)
577
+ return;
578
+ const action = await ctx.ui.select("Account Action", ["activate", "remove local state", "cancel"]);
579
+ if (action === "activate")
580
+ await activateUsageAccount(ctx, summary, account);
581
+ if (action === "remove local state")
582
+ await removeUsageAccountState(ctx, summary, account);
583
+ }
584
+ async function runUsageShowSubcommand(ctx) {
585
+ const summary = await loadUsageManagedSummary(ctx);
586
+ if (!summary)
587
+ return;
588
+ const active = getActiveManagedAccount(summary.accounts, summary.state);
589
+ const hiddenCount = summary.state.removedAccountIds.length;
590
+ const lines = [
591
+ "DuckMind Usage managed account summary",
592
+ `active: ${active ? `${active.label} (${active.id})` : "none"}`,
593
+ `manual: ${summary.state.manualAccountId ?? "none"}`,
594
+ `accounts: ${summary.accounts.length}${hiddenCount ? ` · hidden=${hiddenCount}` : ""}`,
595
+ `all-time: spend=${formatCost(summary.data.allTime.totals.cost)} · messages=${formatNumber(summary.data.allTime.totals.messages)} · sessions=${formatNumber(summary.data.allTime.totals.sessions)} · tokens=${formatTokens(summary.data.allTime.totals.tokens.total)}`,
596
+ "",
597
+ ...summary.accounts.length ? summary.accounts.map((account) => formatManagedAccountLine(account, summary.data, summary.state)) : [managedAccountNoDataMessage()]
598
+ ];
599
+ if (ctx.hasUI) {
600
+ await ctx.ui.select("DuckMind Usage Summary", lines);
601
+ } else {
602
+ ctx.ui.notify(lines.join(`
603
+ `), "info");
604
+ }
605
+ }
606
+ async function runUsageRotationSubcommand(ctx) {
607
+ const lines = [
608
+ "Current policy: manual account first, then accounts without quota cooldown, then highest all-time local spend, then alphabetical fallback.",
609
+ "dm-usage is observational: this state changes /usage account summaries only and does not route provider requests or store API secrets.",
610
+ "Use /usage reset manual to clear manual selection, or /usage reset quota to clear quota cooldown markers."
611
+ ];
612
+ if (ctx.hasUI) {
613
+ await ctx.ui.select("DuckMind Usage Rotation", lines);
614
+ } else {
615
+ ctx.ui.notify(lines.join(" "), "info");
616
+ }
617
+ }
618
+ async function isDirectoryReadable(path) {
619
+ try {
620
+ await access(path, fsConstants.R_OK);
621
+ return true;
622
+ } catch {
623
+ return false;
624
+ }
625
+ }
626
+ async function isWritableDirectoryFor(filePath) {
627
+ try {
628
+ const directory = dirname(filePath);
629
+ await mkdir(directory, { recursive: true });
630
+ await access(directory, fsConstants.R_OK | fsConstants.W_OK);
631
+ return true;
632
+ } catch {
633
+ return false;
634
+ }
635
+ }
636
+ async function runUsageVerifySubcommand(ctx) {
637
+ const sessionsReadable = await isDirectoryReadable(getSessionsDir());
638
+ const stateWritable = await isWritableDirectoryFor(getUsageStatePath());
639
+ const settingsWritable = await isWritableDirectoryFor(getAgentSettingsPath());
640
+ const stateRead = await readUsageManagedState();
641
+ const summary = await loadUsageManagedSummary(ctx);
642
+ const accounts = summary?.accounts.length ?? 0;
643
+ const active = summary ? getActiveManagedAccount(summary.accounts, summary.state)?.id ?? "none" : "none";
644
+ const ok = sessionsReadable && stateWritable && settingsWritable && !stateRead.error;
645
+ const lines = [
646
+ `verify: ${ok ? "PASS" : "WARN"}`,
647
+ `sessions readable: ${sessionsReadable ? "yes" : "no"}`,
648
+ `state directory writable: ${stateWritable ? "yes" : "no"}`,
649
+ `settings directory writable: ${settingsWritable ? "yes" : "no"}`,
650
+ `state json: ${stateRead.error ? `invalid (${stateRead.error})` : "ok"}`,
651
+ `managed accounts: ${accounts}`,
652
+ `active account: ${active}`
653
+ ];
654
+ if (ctx.hasUI) {
655
+ await ctx.ui.select(`DuckMind Usage Verify (${ok ? "PASS" : "WARN"})`, lines);
656
+ } else {
657
+ ctx.ui.notify(lines.join(`
658
+ `), ok ? "info" : "warning");
659
+ }
660
+ }
661
+ async function runUsagePathSubcommand(ctx) {
662
+ const lines = [
663
+ `Session storage: ${getSessionsDir()}`,
664
+ `Managed account state: ${getUsageStatePath()}`,
665
+ `Agent settings: ${getAgentSettingsPath()}`
666
+ ];
667
+ if (ctx.hasUI) {
668
+ await ctx.ui.select("DuckMind Usage Paths", lines);
669
+ } else {
670
+ ctx.ui.notify(lines.join(`
671
+ `), "info");
672
+ }
673
+ }
674
+ async function chooseUsageResetTarget(ctx, argument) {
675
+ const explicitTarget = parseUsageResetTarget(argument.toLowerCase());
676
+ if (explicitTarget)
677
+ return explicitTarget;
678
+ if (argument) {
679
+ ctx.ui.notify("Unknown reset target. Use: /usage reset [manual|quota|all]", "warning");
680
+ return;
681
+ }
682
+ if (!ctx.hasUI)
683
+ return "all";
684
+ const selected = await ctx.ui.select("Reset DuckMind Usage State", [
685
+ "manual - clear manual account override",
686
+ "quota - clear quota cooldown markers",
687
+ "all - clear manual override, quota cooldown markers, and hidden account state"
688
+ ]);
689
+ if (!selected)
690
+ return;
691
+ if (selected.startsWith("manual"))
692
+ return "manual";
693
+ if (selected.startsWith("quota"))
694
+ return "quota";
695
+ return "all";
696
+ }
697
+ async function runUsageResetSubcommand(ctx, rest) {
698
+ const target = await chooseUsageResetTarget(ctx, rest);
699
+ if (!target)
700
+ return;
701
+ const { state } = await readUsageManagedState();
702
+ const hadManual = Boolean(state.manualAccountId);
703
+ if (target === "manual" || target === "all")
704
+ state.manualAccountId = undefined;
705
+ let quotaCleared = 0;
706
+ if (target === "quota" || target === "all") {
707
+ state.accounts = state.accounts.map((account) => {
708
+ if (account.quotaExhaustedUntil === undefined)
709
+ return account;
710
+ quotaCleared++;
711
+ const { quotaExhaustedUntil: _quotaExhaustedUntil, ...restAccount } = account;
712
+ return restAccount;
713
+ });
714
+ }
715
+ const hiddenCleared = target === "all" ? state.removedAccountIds.length : 0;
716
+ if (target === "all")
717
+ state.removedAccountIds = [];
718
+ await saveUsageManagedState(state);
719
+ ctx.ui.notify(`reset: target=${target} manualCleared=${hadManual && !state.manualAccountId ? "yes" : "no"} quotaCleared=${quotaCleared} hiddenCleared=${hiddenCleared}`, "info");
720
+ }
721
+ async function runUsageSubcommand(subcommand, rest, ctx) {
722
+ if (subcommand === "use")
723
+ return runUsageUseSubcommand(ctx, rest);
724
+ if (subcommand === "show")
725
+ return runUsageShowSubcommand(ctx);
726
+ if (subcommand === "rotation")
727
+ return runUsageRotationSubcommand(ctx);
728
+ if (subcommand === "verify")
729
+ return runUsageVerifySubcommand(ctx);
730
+ if (subcommand === "path")
731
+ return runUsagePathSubcommand(ctx);
732
+ if (subcommand === "reset")
733
+ return runUsageResetSubcommand(ctx, rest);
734
+ ctx.ui.notify(USAGE_HELP_TEXT, "info");
735
+ }
736
+ const PARALLEL_WINDOW_MS = 2 * 60000;
737
+ const PARALLEL_SESSION_THRESHOLD = 4;
738
+ const LARGE_CONTEXT_THRESHOLD = 150000;
739
+ const LARGE_CACHE_MISS_THRESHOLD = 1e5;
740
+ const LONG_SESSION_MS = 8 * 60 * 60 * 1000;
741
+ const TOP_SESSION_COUNT = 5;
742
+ const MIN_MESSAGES_FOR_PARALLEL_INSIGHT = 10;
743
+ const MIN_PERCENT_TO_SHOW = 1;
744
+ function computeInsights(raw, longSessionIds) {
745
+ if (raw.messages.length === 0) {
746
+ return { insights: [] };
747
+ }
748
+ const total = raw.messages.reduce((sum, m) => sum + m.cost, 0);
749
+ if (total <= 0) {
750
+ return { insights: [] };
751
+ }
752
+ const candidates = [];
753
+ const parallelWeight = computeParallelCostWeight(raw.messages);
754
+ if (parallelWeight !== null) {
755
+ candidates.push({
756
+ percent: parallelWeight / total * 100,
757
+ headline: `of your cost was while ${PARALLEL_SESSION_THRESHOLD}+ sessions ran in parallel`,
758
+ advice: "All sessions share one rate limit. If you don't need them all at once, queueing uses capacity more evenly."
759
+ });
760
+ }
761
+ const largeContextWeight = raw.messages.filter((m) => m.input + m.cacheRead + m.cacheWrite > LARGE_CONTEXT_THRESHOLD).reduce((sum, m) => sum + m.cost, 0);
762
+ candidates.push({
763
+ percent: largeContextWeight / total * 100,
764
+ headline: `of your cost was at >${formatThresholdTokens(LARGE_CONTEXT_THRESHOLD)} context`,
765
+ advice: "Longer sessions are more expensive even when cached. /compact mid-task, /clear when switching to new tasks."
766
+ });
767
+ const uncachedWeight = raw.messages.filter((m) => m.input + m.cacheWrite > LARGE_CACHE_MISS_THRESHOLD).reduce((sum, m) => sum + m.cost, 0);
768
+ candidates.push({
769
+ percent: uncachedWeight / total * 100,
770
+ headline: `of your cost came from >${formatThresholdTokens(LARGE_CACHE_MISS_THRESHOLD)}-token uncached prompts`,
771
+ advice: "Uncached input is expensive, and often happens when sending a message to a session that has gone idle. /compact before stepping away keeps the cold-start small."
772
+ });
773
+ const longWeight = raw.messages.filter((m) => longSessionIds.has(m.sessionId)).reduce((sum, m) => sum + m.cost, 0);
774
+ if (longWeight > 0) {
775
+ candidates.push({
776
+ percent: longWeight / total * 100,
777
+ headline: `of your cost came from sessions active for ${LONG_SESSION_MS / 3600000}+ hours`,
778
+ advice: "These are often background/loop sessions. Continuous usage can add up quickly so make sure it is intentional."
779
+ });
780
+ }
781
+ if (raw.sessionCosts.size > TOP_SESSION_COUNT) {
782
+ const sortedSessions = Array.from(raw.sessionCosts.values()).sort((a, b) => b - a);
783
+ const topN = Math.min(TOP_SESSION_COUNT, sortedSessions.length);
784
+ const topWeight = sortedSessions.slice(0, topN).reduce((sum, c) => sum + c, 0);
785
+ candidates.push({
786
+ percent: topWeight / total * 100,
787
+ headline: `of your cost came from your top ${topN} session${topN === 1 ? "" : "s"}`,
788
+ advice: "A small number of sessions drives most of your spend. The table view can help pinpoint which ones."
789
+ });
790
+ }
791
+ const insights = candidates.filter((i) => i.percent >= MIN_PERCENT_TO_SHOW).sort((a, b) => b.percent - a.percent);
792
+ return { insights };
793
+ }
794
+ function computeParallelCostWeight(messages) {
795
+ const timed = messages.filter((m) => m.timestamp > 0);
796
+ if (timed.length < MIN_MESSAGES_FOR_PARALLEL_INSIGHT)
797
+ return null;
798
+ const distinctSessions = new Set(timed.map((m) => m.sessionId));
799
+ if (distinctSessions.size < PARALLEL_SESSION_THRESHOLD)
800
+ return null;
801
+ const sorted = timed.slice().sort((a, b) => a.timestamp - b.timestamp);
802
+ const sidCount = new Map;
803
+ let uniqueCount = 0;
804
+ let left = 0;
805
+ let right = 0;
806
+ let parallelCost = 0;
807
+ for (let i = 0;i < sorted.length; i++) {
808
+ const current = sorted[i];
809
+ const high = current.timestamp + PARALLEL_WINDOW_MS;
810
+ const low = current.timestamp - PARALLEL_WINDOW_MS;
811
+ while (right < sorted.length && sorted[right].timestamp <= high) {
812
+ const sid = sorted[right].sessionId;
813
+ const next = (sidCount.get(sid) ?? 0) + 1;
814
+ sidCount.set(sid, next);
815
+ if (next === 1)
816
+ uniqueCount++;
817
+ right++;
818
+ }
819
+ while (left < right && sorted[left].timestamp < low) {
820
+ const sid = sorted[left].sessionId;
821
+ const next = (sidCount.get(sid) ?? 0) - 1;
822
+ if (next === 0) {
823
+ sidCount.delete(sid);
824
+ uniqueCount--;
825
+ } else {
826
+ sidCount.set(sid, next);
827
+ }
828
+ left++;
829
+ }
830
+ if (uniqueCount >= PARALLEL_SESSION_THRESHOLD)
831
+ parallelCost += current.cost;
832
+ }
833
+ return parallelCost;
834
+ }
835
+ function formatThresholdTokens(n) {
836
+ if (n >= 1e6)
837
+ return `${n / 1e6}M`;
838
+ if (n >= 1000)
839
+ return `${n / 1000}k`;
840
+ return String(n);
841
+ }
842
+ function formatInsightPercent(p) {
843
+ if (p >= 10)
844
+ return `${Math.round(p)}%`;
845
+ return `${Math.round(p * 10) / 10}%`;
846
+ }
847
+ function formatCost(cost) {
848
+ if (cost === 0)
849
+ return "-";
850
+ if (cost < 0.01)
851
+ return `$${cost.toFixed(4)}`;
852
+ if (cost < 1)
853
+ return `$${cost.toFixed(2)}`;
854
+ if (cost < 10)
855
+ return `$${cost.toFixed(2)}`;
856
+ if (cost < 100)
857
+ return `$${cost.toFixed(1)}`;
858
+ return `$${Math.round(cost)}`;
859
+ }
860
+ function formatTokens(count) {
861
+ if (count === 0)
862
+ return "-";
863
+ if (count < 1000)
864
+ return count.toString();
865
+ if (count < 1e4)
866
+ return `${(count / 1000).toFixed(1)}k`;
867
+ if (count < 1e6)
868
+ return `${Math.round(count / 1000)}k`;
869
+ if (count < 1e7)
870
+ return `${(count / 1e6).toFixed(1)}M`;
871
+ return `${Math.round(count / 1e6)}M`;
872
+ }
873
+ function formatNumber(n) {
874
+ if (n === 0)
875
+ return "-";
876
+ return n.toLocaleString();
877
+ }
878
+ function padLeft(s, len) {
879
+ const vis = visibleWidth(s);
880
+ if (vis >= len)
881
+ return s;
882
+ return " ".repeat(len - vis) + s;
883
+ }
884
+ function padRight(s, len) {
885
+ const vis = visibleWidth(s);
886
+ if (vis >= len)
887
+ return s;
888
+ return s + " ".repeat(len - vis);
889
+ }
890
+ function sumColumnWidths(columns) {
891
+ return columns.reduce((sum, col) => sum + col.width, 0);
892
+ }
893
+ function fitCell(s, len, align = "left") {
894
+ if (len <= 0)
895
+ return "";
896
+ const truncated = truncateToWidth(s, len);
897
+ return align === "right" ? padLeft(truncated, len) : padRight(truncated, len);
898
+ }
899
+ function clampLines(lines, width) {
900
+ return lines.map((line) => truncateToWidth(line, Math.max(width, 0)));
901
+ }
902
+ function pickFittingText(width, variants) {
903
+ for (const variant of variants) {
904
+ if (visibleWidth(variant) <= width)
905
+ return variant;
906
+ }
907
+ return variants[variants.length - 1] || "";
908
+ }
909
+ function getTableLayout(width) {
910
+ const safeWidth = Math.max(width, 0);
911
+ for (const candidate of TABLE_LAYOUTS) {
912
+ const columnsWidth = sumColumnWidths(candidate.columns);
913
+ const nameWidth = Math.min(MAX_NAME_COL_WIDTH, Math.max(safeWidth - columnsWidth, 0));
914
+ if (nameWidth >= candidate.minNameWidth) {
915
+ return {
916
+ columns: candidate.columns,
917
+ nameWidth,
918
+ tableWidth: nameWidth + columnsWidth,
919
+ compact: candidate.compact ?? false
920
+ };
921
+ }
922
+ }
923
+ const fallback = TABLE_LAYOUTS[TABLE_LAYOUTS.length - 1];
924
+ const fallbackColumnsWidth = sumColumnWidths(fallback.columns);
925
+ const fallbackNameWidth = Math.min(MAX_NAME_COL_WIDTH, Math.max(safeWidth - fallbackColumnsWidth, 0));
926
+ return {
927
+ columns: fallback.columns,
928
+ nameWidth: fallbackNameWidth,
929
+ tableWidth: fallbackNameWidth + fallbackColumnsWidth,
930
+ compact: fallback.compact ?? false
931
+ };
932
+ }
933
+ function renderPanel(title, rows, width, theme) {
934
+ const safeWidth = Math.max(Math.floor(width), 0);
935
+ if (safeWidth < 12) {
936
+ return clampLines([theme.fg("accent", theme.bold(title)), ...rows, ""], safeWidth);
937
+ }
938
+ const innerWidth = Math.max(safeWidth - 2, 1);
939
+ const titleText = ` ${title} `;
940
+ const visibleTitle = truncateToWidth(titleText, Math.max(innerWidth - 1, 1), "");
941
+ const titleWidth = visibleWidth(visibleTitle);
942
+ const topRight = Math.max(innerWidth - titleWidth, 0);
943
+ const top = `╭${visibleTitle}${"─".repeat(topRight)}╮`;
944
+ const bottom = `╰${"─".repeat(innerWidth)}╯`;
945
+ const body = rows.length > 0 ? rows : [theme.fg("dim", "No data")];
946
+ return [
947
+ theme.fg("border", top),
948
+ ...body.map((line) => panelRow(line, innerWidth, theme)),
949
+ theme.fg("border", bottom),
950
+ ""
951
+ ];
952
+ }
953
+ function panelRow(content, innerWidth, theme) {
954
+ const fitted = truncateToWidth(content, innerWidth);
955
+ const padding = Math.max(innerWidth - visibleWidth(fitted), 0);
956
+ return `${theme.fg("border", "│")}${fitted}${" ".repeat(padding)}${theme.fg("border", "│")}`;
957
+ }
958
+ function renderSpendSparkline(dailySpend, maxWidth) {
959
+ if (dailySpend.size === 0)
960
+ return "No spend data";
961
+ const sortedDays = Array.from(dailySpend.keys()).sort();
962
+ const visibleDays = sortedDays.slice(-Math.max(1, Math.min(30, maxWidth)));
963
+ const values = visibleDays.map((day) => dailySpend.get(day) ?? 0);
964
+ const max = Math.max(...values);
965
+ if (max <= 0)
966
+ return "No spend";
967
+ const blocks = ["▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"];
968
+ return values.map((value) => {
969
+ const idx = Math.max(0, Math.min(blocks.length - 1, Math.ceil(value / max * blocks.length) - 1));
970
+ return blocks[idx];
971
+ }).join("");
972
+ }
973
+ function formatDailyAverage(stats) {
974
+ const days = stats.dailySpend.size;
975
+ if (days === 0)
976
+ return "avg/day -";
977
+ return `avg/day ${formatCost(stats.totals.cost / days)}`;
978
+ }
979
+ const TAB_LABELS = {
980
+ today: "Today",
981
+ thisWeek: "This Week",
982
+ lastWeek: "Last Week",
983
+ allTime: "All Time"
984
+ };
985
+ const TAB_ORDER = ["today", "thisWeek", "lastWeek", "allTime"];
986
+
987
+ class UsageComponent {
988
+ activeTab = "allTime";
989
+ viewMode = "table";
990
+ data;
991
+ selectedIndex = 0;
992
+ expanded = new Set;
993
+ providerOrder = [];
994
+ theme;
995
+ requestRender;
996
+ done;
997
+ constructor(theme, data, requestRender, done) {
998
+ this.theme = theme;
999
+ this.requestRender = requestRender;
1000
+ this.done = done;
1001
+ this.data = data;
1002
+ this.updateProviderOrder();
1003
+ }
1004
+ updateProviderOrder() {
1005
+ const stats = this.data[this.activeTab];
1006
+ this.providerOrder = Array.from(stats.providers.entries()).sort((a, b) => b[1].cost - a[1].cost).map(([name]) => name);
1007
+ this.selectedIndex = Math.min(this.selectedIndex, Math.max(0, this.providerOrder.length - 1));
1008
+ }
1009
+ handleInput(data) {
1010
+ if (matchesKey(data, "escape") || matchesKey(data, "q")) {
1011
+ this.done();
1012
+ return;
1013
+ }
1014
+ if (matchesKey(data, "v")) {
1015
+ this.viewMode = this.viewMode === "table" ? "insights" : "table";
1016
+ this.requestRender();
1017
+ return;
1018
+ }
1019
+ if (matchesKey(data, "tab") || matchesKey(data, "right")) {
1020
+ const idx = TAB_ORDER.indexOf(this.activeTab);
1021
+ this.activeTab = TAB_ORDER[(idx + 1) % TAB_ORDER.length];
1022
+ this.updateProviderOrder();
1023
+ this.requestRender();
1024
+ } else if (matchesKey(data, "shift+tab") || matchesKey(data, "left")) {
1025
+ const idx = TAB_ORDER.indexOf(this.activeTab);
1026
+ this.activeTab = TAB_ORDER[(idx - 1 + TAB_ORDER.length) % TAB_ORDER.length];
1027
+ this.updateProviderOrder();
1028
+ this.requestRender();
1029
+ } else if (this.viewMode === "table" && matchesKey(data, "up")) {
1030
+ if (this.selectedIndex > 0) {
1031
+ this.selectedIndex--;
1032
+ this.requestRender();
1033
+ }
1034
+ } else if (this.viewMode === "table" && matchesKey(data, "down")) {
1035
+ if (this.selectedIndex < this.providerOrder.length - 1) {
1036
+ this.selectedIndex++;
1037
+ this.requestRender();
1038
+ }
1039
+ } else if (this.viewMode === "table" && (matchesKey(data, "enter") || matchesKey(data, "space"))) {
1040
+ const provider = this.providerOrder[this.selectedIndex];
1041
+ if (provider) {
1042
+ if (this.expanded.has(provider)) {
1043
+ this.expanded.delete(provider);
1044
+ } else {
1045
+ this.expanded.add(provider);
1046
+ }
1047
+ this.requestRender();
1048
+ }
1049
+ }
1050
+ }
1051
+ render(width) {
1052
+ if (this.viewMode === "insights") {
1053
+ return clampLines([
1054
+ ...this.renderTitle(width),
1055
+ ...this.renderTabs(width, getTableLayout(width)),
1056
+ ...this.renderSpendChart(width),
1057
+ ...this.renderInsights(width),
1058
+ ...this.renderHelp(width)
1059
+ ], width);
1060
+ }
1061
+ const layout = getTableLayout(width);
1062
+ return clampLines([
1063
+ ...this.renderTitle(width),
1064
+ ...this.renderTabs(width, layout),
1065
+ ...this.renderHeader(layout),
1066
+ ...this.renderRows(layout),
1067
+ ...this.renderTotals(layout),
1068
+ ...this.renderFormulaNote(width),
1069
+ ...this.renderHelp(width)
1070
+ ], width);
1071
+ }
1072
+ renderTitle(width) {
1073
+ const th = this.theme;
1074
+ const stats = this.data[this.activeTab];
1075
+ const label = this.viewMode === "insights" ? "Usage Insights" : "Usage Statistics";
1076
+ return renderPanel("DuckMind Usage", [
1077
+ `${th.fg("accent", th.bold(label))} · ${TAB_LABELS[this.activeTab]}`,
1078
+ `Spend ${formatCost(stats.totals.cost)} · ${formatNumber(stats.totals.messages)} messages · ${formatTokens(stats.totals.tokens.total)} tokens`,
1079
+ `Providers ${formatNumber(stats.providers.size)} · Sessions ${formatNumber(stats.totals.sessions)} · ${formatDailyAverage(stats)}`
1080
+ ], width, th);
1081
+ }
1082
+ renderSpendChart(width) {
1083
+ const th = this.theme;
1084
+ const stats = this.data[this.activeTab];
1085
+ return renderPanel("Daily Spend", [
1086
+ th.fg("accent", renderSpendSparkline(stats.dailySpend, Math.max(width - 4, 10))),
1087
+ th.fg("dim", "Local session spend, last 30 active UTC days")
1088
+ ], width, th);
1089
+ }
1090
+ renderInsights(width) {
1091
+ const th = this.theme;
1092
+ const stats = this.data[this.activeTab];
1093
+ const { insights } = stats.insights;
1094
+ const hasMessages = stats.totals.messages > 0;
1095
+ const hasCost = stats.totals.cost > 0;
1096
+ const lines = [];
1097
+ lines.push("What's contributing to your cost?");
1098
+ lines.push(th.fg("dim", "Approximate, based on local sessions on this machine."));
1099
+ lines.push("");
1100
+ const note = `${TAB_LABELS[this.activeTab]} · weighted by cost (USD) · these overlap and can sum to >100%`;
1101
+ lines.push(th.fg("dim", note));
1102
+ lines.push("");
1103
+ if (!hasMessages) {
1104
+ lines.push(th.fg("dim", " No usage recorded for this period."));
1105
+ lines.push("");
1106
+ return lines;
1107
+ }
1108
+ if (!hasCost) {
1109
+ lines.push(th.fg("dim", " No cost data recorded for this period."));
1110
+ lines.push("");
1111
+ return lines;
1112
+ }
1113
+ if (insights.length === 0) {
1114
+ lines.push(th.fg("dim", " No insights above 1% for this period."));
1115
+ lines.push("");
1116
+ return lines;
1117
+ }
1118
+ const indent = " ";
1119
+ const adviceWidth = Math.max(width - indent.length, 30);
1120
+ for (const insight of insights) {
1121
+ const pct = th.fg("accent", th.bold(formatInsightPercent(insight.percent)));
1122
+ lines.push(`${pct} ${insight.headline}`);
1123
+ for (const wrapped of wrapTextWithAnsi(insight.advice, adviceWidth)) {
1124
+ lines.push(`${indent}${th.fg("dim", wrapped)}`);
1125
+ }
1126
+ lines.push("");
1127
+ }
1128
+ return lines;
1129
+ }
1130
+ renderTabs(width, layout) {
1131
+ const th = this.theme;
1132
+ const fullTabs = TAB_ORDER.map((tab) => {
1133
+ const label = TAB_LABELS[tab];
1134
+ return tab === this.activeTab ? th.fg("accent", `[${label}]`) : th.fg("dim", ` ${label} `);
1135
+ }).join(" ");
1136
+ const activeTabOnly = th.fg("accent", `[${TAB_LABELS[this.activeTab]}]`);
1137
+ const tabLine = pickFittingText(width, [
1138
+ fullTabs,
1139
+ `${activeTabOnly} ${th.fg("dim", "[Tab/←→]")}`,
1140
+ activeTabOnly
1141
+ ]);
1142
+ const infoLines = this.viewMode === "table" && layout.compact ? wrapTextWithAnsi(th.fg("dim", "Compact view. Widen the terminal for more columns."), Math.max(width, 1)) : [];
1143
+ return [tabLine, ...infoLines, ""];
1144
+ }
1145
+ renderHeader(layout) {
1146
+ const th = this.theme;
1147
+ let headerLine = fitCell("Provider / Model", layout.nameWidth);
1148
+ for (const col of layout.columns) {
1149
+ const label = fitCell(col.label, col.width, "right");
1150
+ headerLine += col.dimmed ? th.fg("dim", label) : label;
1151
+ }
1152
+ return [th.fg("muted", headerLine), th.fg("border", "─".repeat(layout.tableWidth))];
1153
+ }
1154
+ renderDataRow(name, stats, layout, options = {}) {
1155
+ const th = this.theme;
1156
+ const { indent = 0, selected = false, dimAll = false, prefix } = options;
1157
+ const rawPrefix = prefix ?? " ".repeat(indent);
1158
+ const safePrefix = layout.nameWidth > 0 ? truncateToWidth(rawPrefix, layout.nameWidth, "") : "";
1159
+ const prefixWidth = visibleWidth(safePrefix);
1160
+ const innerNameWidth = Math.max(layout.nameWidth - prefixWidth, 0);
1161
+ const truncName = innerNameWidth > 0 ? truncateToWidth(name, innerNameWidth) : "";
1162
+ const styledName = selected ? th.fg("accent", truncName) : dimAll ? th.fg("dim", truncName) : truncName;
1163
+ let row = safePrefix + (innerNameWidth > 0 ? padRight(styledName, innerNameWidth) : "");
1164
+ for (const col of layout.columns) {
1165
+ const value = fitCell(col.getValue(stats), col.width, "right");
1166
+ const shouldDim = col.dimmed || dimAll;
1167
+ row += shouldDim ? th.fg("dim", value) : value;
1168
+ }
1169
+ return row;
1170
+ }
1171
+ renderRows(layout) {
1172
+ const th = this.theme;
1173
+ const stats = this.data[this.activeTab];
1174
+ const lines = [];
1175
+ if (this.providerOrder.length === 0) {
1176
+ lines.push(th.fg("dim", " No usage data for this period"));
1177
+ return lines;
1178
+ }
1179
+ for (let i = 0;i < this.providerOrder.length; i++) {
1180
+ const providerName = this.providerOrder[i];
1181
+ const providerStats = stats.providers.get(providerName);
1182
+ const isSelected = i === this.selectedIndex;
1183
+ const isExpanded = this.expanded.has(providerName);
1184
+ const arrow = isExpanded ? "▾" : "▸";
1185
+ const prefix = isSelected ? th.fg("accent", `${arrow} `) : th.fg("dim", `${arrow} `);
1186
+ lines.push(this.renderDataRow(formatProviderName(providerName), providerStats, layout, {
1187
+ selected: isSelected,
1188
+ prefix
1189
+ }));
1190
+ if (isExpanded) {
1191
+ const models = Array.from(providerStats.models.entries()).sort((a, b) => b[1].cost - a[1].cost);
1192
+ for (const [modelName, modelStats] of models) {
1193
+ lines.push(this.renderDataRow(modelName, modelStats, layout, { indent: 4, dimAll: true }));
1194
+ }
1195
+ }
1196
+ }
1197
+ return lines;
1198
+ }
1199
+ renderTotals(layout) {
1200
+ const th = this.theme;
1201
+ const stats = this.data[this.activeTab];
1202
+ let totalRow = fitCell(th.bold("Total"), layout.nameWidth);
1203
+ for (const col of layout.columns) {
1204
+ const value = fitCell(col.getValue(stats.totals), col.width, "right");
1205
+ totalRow += col.dimmed ? th.fg("dim", value) : value;
1206
+ }
1207
+ return [th.fg("border", "─".repeat(layout.tableWidth)), totalRow, ""];
1208
+ }
1209
+ renderFormulaNote(width) {
1210
+ const line = pickFittingText(width, [
1211
+ "Tokens = Input + Output + CacheWrite · ↑In = Input + CacheWrite (as of 0.2.0)",
1212
+ "Tokens = In + Out + CacheWrite · ↑In = In + CacheWrite (v0.2.0+)",
1213
+ "Tokens & ↑In include CacheWrite (v0.2.0+)",
1214
+ "Incl. CacheWrite (v0.2.0+)"
1215
+ ]);
1216
+ return [this.theme.fg("dim", line), ""];
1217
+ }
1218
+ renderHelp(width) {
1219
+ const variants = this.viewMode === "insights" ? [
1220
+ "[Tab/←→] period [v] table view [q] close",
1221
+ "[Tab] period [v] table [q] close",
1222
+ "[v] table [q] close",
1223
+ "[q] close"
1224
+ ] : [
1225
+ "[Tab/←→] period [↑↓] select [Enter] expand [v] insights [q] close",
1226
+ "[Tab] period [↑↓] select [Enter] expand [v] insights [q] close",
1227
+ "[↑↓] select [Enter] expand [v] insights [q] close",
1228
+ "[↑↓] select [v] insights [q] close",
1229
+ "[↑↓] select [q] close",
1230
+ "[q] close"
1231
+ ];
1232
+ const line = pickFittingText(width, variants);
1233
+ return [this.theme.fg("dim", line)];
1234
+ }
1235
+ invalidate() {}
1236
+ dispose() {}
1237
+ }
1238
+ export default function dm_usage_default(pi) {
1239
+ pi.registerCommand("usage", {
1240
+ description: `Show usage statistics dashboard and account tools: ${USAGE_SUBCOMMANDS.join(", ")}`,
1241
+ getArgumentCompletions: getUsageCommandCompletions,
1242
+ handler: async (args, ctx) => {
1243
+ const parsed = parseUsageCommandArgs(args);
1244
+ if (parsed.subcommand) {
1245
+ if (!isUsageSubcommand(parsed.subcommand)) {
1246
+ ctx.ui.notify(`Unknown /usage subcommand: ${parsed.subcommand}
1247
+ ${USAGE_HELP_TEXT}`, "warning");
1248
+ return;
1249
+ }
1250
+ await runUsageSubcommand(parsed.subcommand, parsed.rest, ctx);
1251
+ return;
1252
+ }
1253
+ if (!ctx.hasUI) {
1254
+ await runUsageShowSubcommand(ctx);
1255
+ return;
1256
+ }
1257
+ const data = await ctx.ui.custom((tui, theme, _kb, done) => {
1258
+ const loader = new CancellableLoader(tui, (s) => theme.fg("accent", s), (s) => theme.fg("muted", s), "Loading Usage...");
1259
+ let finished = false;
1260
+ const finish = (value) => {
1261
+ if (finished)
1262
+ return;
1263
+ finished = true;
1264
+ loader.dispose();
1265
+ done(value);
1266
+ };
1267
+ loader.onAbort = () => finish(null);
1268
+ collectUsageData(loader.signal).then(finish).catch(() => finish(null));
1269
+ return loader;
1270
+ });
1271
+ if (!data) {
1272
+ return;
1273
+ }
1274
+ await ctx.ui.custom((tui, theme, _kb, done) => {
1275
+ const container = new Container;
1276
+ container.addChild(new Spacer(1));
1277
+ container.addChild(new DynamicBorder((s) => theme.fg("border", s)));
1278
+ container.addChild(new Spacer(1));
1279
+ const usage = new UsageComponent(theme, data, () => tui.requestRender(), () => done());
1280
+ return {
1281
+ render: (w) => {
1282
+ const borderLines = clampLines(container.render(w), w);
1283
+ const usageLines = usage.render(w);
1284
+ const bottomBorder = theme.fg("border", "─".repeat(w));
1285
+ return clampLines([...borderLines, ...usageLines, "", bottomBorder], w);
1286
+ },
1287
+ invalidate: () => container.invalidate(),
1288
+ handleInput: (input) => usage.handleInput(input),
1289
+ dispose: () => {}
1290
+ };
1291
+ });
1292
+ }
1293
+ });
1294
+ }