@songsid/agend 2.1.1-beta.2 → 2.1.1-beta.21

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 (156) hide show
  1. package/README.md +1 -0
  2. package/README.zh-TW.md +1 -0
  3. package/dist/access-path.js +15 -6
  4. package/dist/access-path.js.map +1 -1
  5. package/dist/agent-cli-instructions.md +9 -0
  6. package/dist/backend/antigravity.d.ts +4 -1
  7. package/dist/backend/antigravity.js +24 -13
  8. package/dist/backend/antigravity.js.map +1 -1
  9. package/dist/backend/claude-code.d.ts +37 -0
  10. package/dist/backend/claude-code.js +47 -8
  11. package/dist/backend/claude-code.js.map +1 -1
  12. package/dist/backend/codex.d.ts +17 -0
  13. package/dist/backend/codex.js +279 -62
  14. package/dist/backend/codex.js.map +1 -1
  15. package/dist/backend/gemini-cli.js +4 -7
  16. package/dist/backend/gemini-cli.js.map +1 -1
  17. package/dist/backend/grok.d.ts +24 -0
  18. package/dist/backend/grok.js +57 -8
  19. package/dist/backend/grok.js.map +1 -1
  20. package/dist/backend/kiro.d.ts +27 -0
  21. package/dist/backend/kiro.js +96 -15
  22. package/dist/backend/kiro.js.map +1 -1
  23. package/dist/backend/opencode.js +4 -7
  24. package/dist/backend/opencode.js.map +1 -1
  25. package/dist/backend/types.d.ts +44 -0
  26. package/dist/backend/types.js +3 -3
  27. package/dist/backend/types.js.map +1 -1
  28. package/dist/channel/adapters/discord.d.ts +15 -0
  29. package/dist/channel/adapters/discord.js +94 -6
  30. package/dist/channel/adapters/discord.js.map +1 -1
  31. package/dist/channel/adapters/telegram.d.ts +21 -0
  32. package/dist/channel/adapters/telegram.js +132 -15
  33. package/dist/channel/adapters/telegram.js.map +1 -1
  34. package/dist/channel/ipc-bridge.d.ts +17 -1
  35. package/dist/channel/ipc-bridge.js +53 -17
  36. package/dist/channel/ipc-bridge.js.map +1 -1
  37. package/dist/channel/ipc-timeouts.d.ts +40 -0
  38. package/dist/channel/ipc-timeouts.js +58 -0
  39. package/dist/channel/ipc-timeouts.js.map +1 -0
  40. package/dist/channel/mcp-server.js +30 -14
  41. package/dist/channel/mcp-server.js.map +1 -1
  42. package/dist/channel/mcp-tools.js +28 -1
  43. package/dist/channel/mcp-tools.js.map +1 -1
  44. package/dist/channel/message-queue.d.ts +1 -0
  45. package/dist/channel/message-queue.js +49 -17
  46. package/dist/channel/message-queue.js.map +1 -1
  47. package/dist/channel/reconnect-backoff.d.ts +17 -0
  48. package/dist/channel/reconnect-backoff.js +21 -0
  49. package/dist/channel/reconnect-backoff.js.map +1 -0
  50. package/dist/channel/types.d.ts +29 -0
  51. package/dist/classic-channel-manager.js +1 -5
  52. package/dist/classic-channel-manager.js.map +1 -1
  53. package/dist/cli.js +148 -60
  54. package/dist/cli.js.map +1 -1
  55. package/dist/completion.d.ts +27 -0
  56. package/dist/completion.js +121 -0
  57. package/dist/completion.js.map +1 -0
  58. package/dist/config-validator.js +27 -0
  59. package/dist/config-validator.js.map +1 -1
  60. package/dist/config.js +6 -0
  61. package/dist/config.js.map +1 -1
  62. package/dist/cost-guard.d.ts +3 -1
  63. package/dist/cost-guard.js +3 -1
  64. package/dist/cost-guard.js.map +1 -1
  65. package/dist/daemon.d.ts +230 -13
  66. package/dist/daemon.js +975 -335
  67. package/dist/daemon.js.map +1 -1
  68. package/dist/event-log.d.ts +31 -0
  69. package/dist/event-log.js +96 -0
  70. package/dist/event-log.js.map +1 -1
  71. package/dist/fleet-context.d.ts +2 -0
  72. package/dist/fleet-manager.d.ts +209 -4
  73. package/dist/fleet-manager.js +813 -84
  74. package/dist/fleet-manager.js.map +1 -1
  75. package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +22 -0
  76. package/dist/general-knowledge/skills/model-discovery/SKILL.md +14 -23
  77. package/dist/general-knowledge/skills/session-management/SKILL.md +15 -34
  78. package/dist/hang-detector.d.ts +19 -13
  79. package/dist/hang-detector.js +19 -49
  80. package/dist/hang-detector.js.map +1 -1
  81. package/dist/instance-lifecycle.d.ts +21 -1
  82. package/dist/instance-lifecycle.js +141 -7
  83. package/dist/instance-lifecycle.js.map +1 -1
  84. package/dist/instructions.d.ts +5 -0
  85. package/dist/instructions.js +10 -5
  86. package/dist/instructions.js.map +1 -1
  87. package/dist/locale.js +3 -0
  88. package/dist/locale.js.map +1 -1
  89. package/dist/logger.js +14 -0
  90. package/dist/logger.js.map +1 -1
  91. package/dist/mcp-liveness.d.ts +21 -0
  92. package/dist/mcp-liveness.js +27 -0
  93. package/dist/mcp-liveness.js.map +1 -0
  94. package/dist/outbound-handlers.d.ts +16 -0
  95. package/dist/outbound-handlers.js +121 -27
  96. package/dist/outbound-handlers.js.map +1 -1
  97. package/dist/outbound-schemas.d.ts +7 -2
  98. package/dist/outbound-schemas.js +13 -2
  99. package/dist/outbound-schemas.js.map +1 -1
  100. package/dist/pane-write-lock.d.ts +48 -0
  101. package/dist/pane-write-lock.js +73 -0
  102. package/dist/pane-write-lock.js.map +1 -0
  103. package/dist/process-memory.d.ts +31 -0
  104. package/dist/process-memory.js +79 -0
  105. package/dist/process-memory.js.map +1 -0
  106. package/dist/quickstart.js +17 -16
  107. package/dist/quickstart.js.map +1 -1
  108. package/dist/scheduler/db.js +3 -0
  109. package/dist/scheduler/db.js.map +1 -1
  110. package/dist/sd-notify.d.ts +45 -0
  111. package/dist/sd-notify.js +74 -3
  112. package/dist/sd-notify.js.map +1 -1
  113. package/dist/secret-file.d.ts +33 -0
  114. package/dist/secret-file.js +36 -0
  115. package/dist/secret-file.js.map +1 -0
  116. package/dist/setup-wizard.js +9 -7
  117. package/dist/setup-wizard.js.map +1 -1
  118. package/dist/tmux-control.d.ts +58 -5
  119. package/dist/tmux-control.js +107 -13
  120. package/dist/tmux-control.js.map +1 -1
  121. package/dist/tmux-manager.d.ts +47 -2
  122. package/dist/tmux-manager.js +103 -8
  123. package/dist/tmux-manager.js.map +1 -1
  124. package/dist/topic-commands.d.ts +43 -1
  125. package/dist/topic-commands.js +196 -66
  126. package/dist/topic-commands.js.map +1 -1
  127. package/dist/tui-glyphs.d.ts +25 -0
  128. package/dist/tui-glyphs.js +26 -0
  129. package/dist/tui-glyphs.js.map +1 -0
  130. package/dist/types.d.ts +19 -0
  131. package/dist/ui/view.html +291 -33
  132. package/dist/update-check.d.ts +22 -0
  133. package/dist/update-check.js +44 -0
  134. package/dist/update-check.js.map +1 -0
  135. package/dist/usage/providers.d.ts +50 -0
  136. package/dist/usage/providers.js +611 -0
  137. package/dist/usage/providers.js.map +1 -0
  138. package/dist/usage/usage-api.d.ts +50 -0
  139. package/dist/usage/usage-api.js +111 -0
  140. package/dist/usage/usage-api.js.map +1 -0
  141. package/dist/view-api.d.ts +10 -0
  142. package/dist/view-api.js +67 -14
  143. package/dist/view-api.js.map +1 -1
  144. package/dist/web-api.js +5 -2
  145. package/dist/web-api.js.map +1 -1
  146. package/package.json +4 -1
  147. package/templates/systemd.service.ejs +9 -1
  148. package/dist/channel/tool-tracker.d.ts +0 -13
  149. package/dist/channel/tool-tracker.js +0 -58
  150. package/dist/channel/tool-tracker.js.map +0 -1
  151. package/dist/daemon-entry.d.ts +0 -1
  152. package/dist/daemon-entry.js +0 -30
  153. package/dist/daemon-entry.js.map +0 -1
  154. package/dist/fleet-system-prompt.d.ts +0 -11
  155. package/dist/fleet-system-prompt.js +0 -61
  156. package/dist/fleet-system-prompt.js.map +0 -1
package/dist/types.d.ts CHANGED
@@ -47,6 +47,17 @@ export interface HangDetectorConfig {
47
47
  enabled: boolean;
48
48
  timeout_minutes: number;
49
49
  }
50
+ /**
51
+ * Stable logical terminal geometry for an instance's tmux window.
52
+ *
53
+ * `enabled: false` is the compatibility escape hatch: the window is pinned to
54
+ * tmux's historical 80x24 geometry rather than inheriting the larger defaults.
55
+ */
56
+ export interface TerminalConfig {
57
+ enabled?: boolean;
58
+ columns?: number;
59
+ rows?: number;
60
+ }
50
61
  export interface DailySummaryConfig {
51
62
  enabled: boolean;
52
63
  hour: number;
@@ -130,6 +141,8 @@ export interface InstanceConfig {
130
141
  agent_mode?: "mcp" | "cli";
131
142
  /** Hang detector override for this instance. */
132
143
  hang_detector?: HangDetectorConfig;
144
+ /** Logical terminal size. Defaults to 120x36; set enabled=false for 80x24 compatibility. */
145
+ terminal?: TerminalConfig;
133
146
  /** Command to paste raw before each user message (e.g. "/chat load base.json") */
134
147
  pre_task_command?: string;
135
148
  }
@@ -219,6 +232,11 @@ export interface FleetTemplate {
219
232
  team?: boolean;
220
233
  instances: Record<string, TemplateInstanceDef>;
221
234
  }
235
+ /** Web UI feature toggles (fleet.yaml `web:` section). */
236
+ export interface WebConfig {
237
+ /** Show the AI subscription usage panel on /view and serve /api/ai-usage (default true). */
238
+ usage_panel?: boolean;
239
+ }
222
240
  export interface FleetConfig {
223
241
  channel?: ChannelConfig;
224
242
  channels?: ChannelConfig[];
@@ -229,6 +247,7 @@ export interface FleetConfig {
229
247
  templates?: Record<string, FleetTemplate>;
230
248
  profiles?: Record<string, ProfileConfig>;
231
249
  health_port?: number;
250
+ web?: WebConfig;
232
251
  }
233
252
  /**
234
253
  * User-authored fleet.yaml before defaults are merged into instances.
package/dist/ui/view.html CHANGED
@@ -7,8 +7,8 @@
7
7
  <style>
8
8
  * { margin: 0; padding: 0; box-sizing: border-box; }
9
9
  :root { --bg:#0d1117; --panel:#161b22; --border:#21262d; --fg:#c9d1d9; --dim:#8b949e; --accent:#58a6ff; }
10
- html, body { height: 100%; }
11
- body { background: var(--bg); color: var(--fg); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; display: flex; height: 100vh; overflow: hidden; }
10
+ html, body { width: 100%; height: 100%; }
11
+ body { background: var(--bg); color: var(--fg); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; display: flex; width: 100vw; min-width: 0; height: 100vh; overflow: hidden; }
12
12
 
13
13
  /* Sidebar */
14
14
  #sidebar { width: 240px; flex: 0 0 240px; background: var(--panel); border-right: 1px solid var(--border); display: flex; flex-direction: column; overflow-y: auto; }
@@ -31,21 +31,35 @@
31
31
 
32
32
  /* Main */
33
33
  #main { flex: 1; display: flex; flex-direction: column; min-width: 0; }
34
- #term { flex: 1; background: #000; overflow: auto; padding: 10px 12px; }
35
- #term pre { font-family: "SFMono-Regular", Menlo, Consolas, "Liberation Mono", monospace; line-height: 1.2; white-space: pre; color: #d0d0d0; }
36
-
37
- /* Card */
38
- #card { background: var(--panel); border-top: 1px solid var(--border); padding: 14px 18px; display: flex; gap: 16px; align-items: flex-start; }
39
- #avatarBox { width: 56px; height: 56px; flex: 0 0 56px; }
40
- .avatar-img { width: 56px; height: 56px; border-radius: 50%; object-fit: cover; display: block; }
41
- .avatar-ph { width: 56px; height: 56px; border-radius: 50%; background: #30363d; color: var(--dim); display: flex; align-items: center; justify-content: center; font-size: 24px; font-weight: 600; text-transform: uppercase; user-select: none; }
42
- #cardBody { flex: 1; min-width: 0; }
43
- #cardBody .row1 { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
44
- #cardBody .dname { font-size: 16px; font-weight: 600; }
45
- #cardBody .role { color: var(--accent); font-size: 13px; }
46
- #cardBody .meta { color: var(--dim); font-size: 12px; margin-top: 3px; }
47
- #cardBody .desc { font-size: 13px; margin-top: 6px; color: #adbac7; white-space: pre-wrap; }
48
- #editBtn { background: #21262d; color: var(--fg); border: 1px solid var(--border); border-radius: 6px; padding: 5px 12px; cursor: pointer; font-size: 13px; }
34
+ /* Padding kept at 5/6px: enough that glyphs don't touch the edge, small
35
+ enough that the terminal keeps essentially the whole main area. */
36
+ #term { flex: 1; background: #000; overflow: auto; scrollbar-gutter: stable; padding: 5px 6px; }
37
+ #term pre { min-width: 100%; min-height: 100%; font-family: "SFMono-Regular", Menlo, Consolas, "Liberation Mono", monospace; line-height: 1.2; white-space: pre; color: #d0d0d0; }
38
+
39
+ /* Card a single compact bar by default so the terminal keeps the height.
40
+ `.expanded` restores the tall layout with avatar + description. */
41
+ #card { background: var(--panel); border-top: 1px solid var(--border); padding: 5px 10px; display: flex; gap: 10px; align-items: center; }
42
+ #avatarBox { width: 24px; height: 24px; flex: 0 0 24px; }
43
+ .avatar-img { width: 100%; height: 100%; border-radius: 50%; object-fit: cover; display: block; }
44
+ .avatar-ph { width: 100%; height: 100%; border-radius: 50%; background: #30363d; color: var(--dim); display: flex; align-items: center; justify-content: center; font-size: 12px; font-weight: 600; text-transform: uppercase; user-select: none; }
45
+ #cardBody { flex: 1; min-width: 0; display: flex; align-items: baseline; gap: 10px; overflow: hidden; }
46
+ #cardBody .row1 { display: flex; align-items: baseline; gap: 10px; flex: 0 0 auto; }
47
+ #cardBody .dname { font-size: 13px; font-weight: 600; white-space: nowrap; }
48
+ #cardBody .role { color: var(--accent); font-size: 12px; white-space: nowrap; }
49
+ #cardBody .meta { color: var(--dim); font-size: 11px; flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
50
+ #cardBody .desc { display: none; }
51
+ #cardToggle { background: none; border: none; color: var(--dim); cursor: pointer; font-size: 12px; padding: 2px 4px; flex: 0 0 auto; }
52
+ #cardToggle:hover { color: var(--fg); }
53
+
54
+ #card.expanded { align-items: flex-start; padding: 12px 14px; }
55
+ #card.expanded #avatarBox { width: 48px; height: 48px; flex: 0 0 48px; }
56
+ #card.expanded .avatar-ph { font-size: 20px; }
57
+ #card.expanded #cardBody { display: block; }
58
+ #card.expanded #cardBody .dname { font-size: 16px; }
59
+ #card.expanded #cardBody .role { font-size: 13px; }
60
+ #card.expanded #cardBody .meta { font-size: 12px; margin-top: 3px; white-space: normal; overflow: visible; }
61
+ #card.expanded #cardBody .desc { display: block; font-size: 13px; margin-top: 6px; color: #adbac7; white-space: pre-wrap; }
62
+ #editBtn { background: #21262d; color: var(--fg); border: 1px solid var(--border); border-radius: 6px; padding: 3px 10px; cursor: pointer; font-size: 12px; flex: 0 0 auto; }
49
63
  #editBtn:hover { border-color: var(--accent); }
50
64
 
51
65
  /* Edit form */
@@ -68,9 +82,43 @@
68
82
  .help-panel ul { margin: 8px 0 0 18px; }
69
83
  .help-panel li { font-size: 13px; margin: 6px 0; color: var(--fg); }
70
84
  .help-panel .close { position: absolute; top: 14px; right: 16px; cursor: pointer; color: var(--dim); font-size: 22px; line-height: 1; background: none; border: none; }
85
+
86
+ /* AI usage panel (shares .help-panel chrome) */
87
+ .u-provider { margin: 14px 0 4px; padding-top: 10px; border-top: 1px solid var(--border); }
88
+ .u-provider:first-of-type { border-top: none; padding-top: 0; }
89
+ .u-head { display: flex; align-items: baseline; gap: 8px; }
90
+ .u-head .u-name { font-size: 14px; font-weight: 600; }
91
+ .u-head .u-plan { color: var(--dim); font-size: 11px; }
92
+ .u-metric { margin: 8px 0; }
93
+ .u-row { display: flex; align-items: baseline; gap: 6px; font-size: 12px; }
94
+ .u-row .u-label { color: var(--dim); flex: 1; min-width: 0; }
95
+ .u-row .u-val { font-variant-numeric: tabular-nums; }
96
+ .u-row .u-status { font-size: 11px; font-weight: 600; }
97
+ .u-status.warn { color: #d29922; } .u-status.crit { color: #f85149; }
98
+ .u-meter { height: 6px; background: var(--border); border-radius: 3px; overflow: hidden; margin-top: 4px; }
99
+ .u-fill { height: 100%; border-radius: 3px; background: var(--accent); min-width: 2px; }
100
+ .u-fill.warn { background: #d29922; } .u-fill.crit { background: #f85149; }
101
+ .u-sub { color: var(--dim); font-size: 11px; margin-top: 2px; font-variant-numeric: tabular-nums; }
102
+ .u-err { color: #d29922; font-size: 12px; margin: 6px 0; }
103
+ /* Neutral note (subscription plan, or "sign-in needed") — deliberately not the
104
+ amber error colour, since neither state is a failure. */
105
+ .u-note { color: var(--dim); font-size: 12px; margin: 6px 0; }
106
+ .u-foot { color: var(--dim); font-size: 11px; margin-top: 14px; display: flex; gap: 8px; align-items: baseline; }
107
+ .u-foot button { background: none; border: 1px solid var(--border); border-radius: 5px; color: var(--dim); cursor: pointer; font-size: 11px; padding: 2px 8px; }
108
+ .u-foot button:hover { color: var(--fg); border-color: var(--accent); }
109
+
110
+ #densityBtn { font-size: 14px; font-weight: 600; letter-spacing: -.5px; }
111
+ @media (max-width: 700px) {
112
+ #sidebar { width: 180px; flex-basis: 180px; }
113
+ /* The compact bar must stay one line; only the expanded form wraps. */
114
+ #card.expanded { flex-wrap: wrap; padding: 10px 12px; }
115
+ #edit { flex-basis: 100%; }
116
+ }
71
117
  </style>
72
118
  </head>
73
119
  <body>
120
+ <button class="help-btn" id="usageBtn" title="AI usage" style="right:140px; display:none">📊</button>
121
+ <button class="help-btn" id="densityBtn" title="Font density" style="right:98px">⤢</button>
74
122
  <button class="help-btn" id="langBtn" title="Language" style="right:56px">🌐</button>
75
123
  <button class="help-btn" id="helpBtn" title="Help">ℹ️</button>
76
124
  <div class="help-backdrop" id="helpBackdrop"></div>
@@ -78,6 +126,10 @@
78
126
  <button class="close" id="helpClose" title="Close">×</button>
79
127
  <div id="helpContent"></div>
80
128
  </aside>
129
+ <aside class="help-panel" id="usagePanel">
130
+ <button class="close" id="usageClose" title="Close">×</button>
131
+ <div id="usageContent"></div>
132
+ </aside>
81
133
  <div id="sidebar"><h1 data-i18n="viewTitle">AgEnD · View</h1><div id="list"></div></div>
82
134
  <div id="main">
83
135
  <div id="term"><pre id="pre"></pre></div>
@@ -88,6 +140,7 @@
88
140
  <div class="meta" id="meta"></div>
89
141
  <div class="desc" id="desc"></div>
90
142
  </div>
143
+ <button id="cardToggle" title="Expand profile" aria-expanded="false">▲</button>
91
144
  <button id="editBtn" data-i18n="editProfile">✏️ Edit</button>
92
145
  <div id="edit">
93
146
  <input id="fDisplay" data-i18n-ph="displayName" placeholder="Display name">
@@ -121,13 +174,23 @@
121
174
  saveHint: "Viewing is open (read-only). Saving requires the web.token (~/.agend/web.token).", helpTitle: "📖 Help",
122
175
  help1: "Pick an instance on the left → its live terminal shows on the right",
123
176
  help2: "Click a group header ▼/▶ to collapse/expand", help3: "✏️ Edit changes display name, role, description",
124
- help4: "Drag to reorder (needs web.token permission)" },
177
+ help4: "Drag to reorder (needs web.token permission)",
178
+ help5: "Click ⤢ to cycle font density: Fit → Comfortable → Compact",
179
+ densityFit: "Fit", densityComfortable: "Comfortable", densityCompact: "Compact", densityLabel: "Font density",
180
+ usageTitle: "📊 AI Usage", usageEmpty: "No AI CLI logins found on this machine.",
181
+ usageUpdated: "updated", usageRefresh: "Refresh", usageHigh: "high", usageNear: "near limit",
182
+ usageResets: "resets in {t}", usageSoon: "resets soon", usageFail: "Could not load usage data." },
125
183
  "zh-TW": { viewTitle: "AgEnD · 檢視", editProfile: "✏️ 編輯", save: "儲存", cancel: "取消",
126
184
  displayName: "顯示名稱", role: "角色", description: "描述", tokenPh: "web.token(儲存時需要)",
127
185
  saveHint: "檢視為公開(唯讀)。儲存需要 web.token(~/.agend/web.token)。", helpTitle: "📖 使用說明",
128
186
  help1: "左側選 instance → 右側顯示即時 terminal 畫面",
129
187
  help2: "點 group header ▼/▶ 收合/展開", help3: "✏️ Edit 可修改顯示名稱、角色、描述",
130
- help4: "拖曳可調整順序(需 web.token 權限)" },
188
+ help4: "拖曳可調整順序(需 web.token 權限)",
189
+ help5: "點 ⤢ 循環切換字級密度:剛好填滿 → 舒適 → 精簡",
190
+ densityFit: "剛好填滿", densityComfortable: "舒適", densityCompact: "精簡", densityLabel: "字級密度",
191
+ usageTitle: "📊 AI 訂閱用量", usageEmpty: "本機沒有偵測到已登入的 AI CLI。",
192
+ usageUpdated: "更新於", usageRefresh: "重新整理", usageHigh: "偏高", usageNear: "接近上限",
193
+ usageResets: "{t} 後重置", usageSoon: "即將重置", usageFail: "無法載入用量資料。" },
131
194
  };
132
195
  let lang = localStorage.getItem("agend_lang") || (navigator.language && navigator.language.toLowerCase().startsWith("zh") ? "zh-TW" : "en");
133
196
  if (!I18N[lang]) lang = "en";
@@ -137,14 +200,28 @@
137
200
  document.querySelectorAll("[data-i18n-ph]").forEach(e => { e.setAttribute("placeholder", T(e.getAttribute("data-i18n-ph"))); });
138
201
  const hc = q("helpContent");
139
202
  if (hc) { hc.innerHTML = ""; const h3 = document.createElement("h3"); h3.textContent = T("helpTitle"); hc.append(h3);
140
- const ul = document.createElement("ul"); for (const k of ["help1","help2","help3","help4"]) { const li = document.createElement("li"); li.textContent = T(k); ul.append(li); } hc.append(ul); }
203
+ const ul = document.createElement("ul"); for (const k of ["help1","help2","help3","help4","help5"]) { const li = document.createElement("li"); li.textContent = T(k); ul.append(li); } hc.append(ul); }
204
+ renderDensityBtn();
141
205
  }
142
206
 
143
207
  let roster = [];
144
208
  let rosterByName = new Map();
145
209
  let current = null;
146
210
  let poll = null;
147
- let fitted = false;
211
+ // Pane grid in cells, reported by /api/pane's X-Pane-Cols / X-Pane-Rows.
212
+ // This — not the captured text — is the font-size input.
213
+ let paneCols = 0, paneRows = 0;
214
+ let lastFitKey = ""; // skip redundant recalcs
215
+ let appliedSize = 0; // last font-size actually written to #pre
216
+ let resizeTimer = null;
217
+ // Font density — the manual escape hatch. Once the tmux grid is large
218
+ // (120x32), the auto-fit size is necessarily small, so the user needs a way
219
+ // to trade "everything visible" against "readable glyphs".
220
+ const DENSITY = { fit: 1, comfortable: 1.25, compact: 0.8 };
221
+ const DENSITY_GLYPH = { fit: "⤢", comfortable: "A+", compact: "A−" };
222
+ const DENSITY_ORDER = ["fit", "comfortable", "compact"];
223
+ let density = localStorage.getItem("agend_view_density");
224
+ if (!DENSITY[density]) density = "fit";
148
225
  const collapsed = new Set(); // group names the user has collapsed
149
226
  // Drag-sort state
150
227
  let sortGroups = new Map(); // groupName -> sort_index (persisted override)
@@ -456,32 +533,114 @@
456
533
  }
457
534
  const text = await r.text();
458
535
  if (target !== current) return;
536
+ // Pane grid comes from tmux, so a sparse frame can't distort the fit.
537
+ const c = Number(r.headers.get("X-Pane-Cols")), rw = Number(r.headers.get("X-Pane-Rows"));
538
+ if (c >= 1 && rw >= 1 && (c !== paneCols || rw !== paneRows)) { paneCols = c; paneRows = rw; lastFitKey = ""; }
459
539
  q("pre").innerHTML = ansiToHtml(text);
460
- if (!fitted) { fitFont(text); fitted = true; }
540
+ fitFont();
461
541
  } catch { /* transient network error — keep last frame */ }
462
542
  }
463
543
 
464
- // Fit font size so the widest line fits the terminal width. Recomputed only
465
- // on instance switch (fitted flag), not every poll.
466
- function fitFont(text) {
467
- const cols = text.split("\n").reduce((m, l) => Math.max(m, l.replace(/\x1b\[[0-9;]*m/g, "").length), 0);
468
- if (cols < 1) return;
469
- const termW = q("term").clientWidth - 24;
470
- // monospace char width ≈ 0.6 × font-size
471
- let size = Math.floor(termW / (cols * 0.6));
472
- size = Math.max(8, Math.min(size, 16));
473
- q("pre").style.fontSize = size + "px";
544
+ // ── Font sizing ───────────────────────────────────────
545
+ // Measured advance width of one monospace cell as a fraction of font-size.
546
+ // The old hardcoded .6em was only an estimate and drifts per resolved font.
547
+ // The ratio is font-size independent, so measuring once is enough.
548
+ let cellRatio = 0;
549
+ function glyphRatio() {
550
+ if (cellRatio) return cellRatio;
551
+ const probe = document.createElement("span");
552
+ probe.style.cssText = "position:absolute;visibility:hidden;white-space:pre;left:-9999px;top:0;font-size:100px";
553
+ probe.style.fontFamily = getComputedStyle(q("pre")).fontFamily;
554
+ probe.textContent = "0".repeat(100);
555
+ document.body.appendChild(probe);
556
+ const w = probe.getBoundingClientRect().width;
557
+ probe.remove();
558
+ if (w > 0) cellRatio = w / 100 / 100; // ÷100 chars, ÷100px font-size
559
+ return cellRatio || 0.6; // headless / odd env → old estimate
560
+ }
561
+
562
+ // The removed 48px ceiling was what made an 80x24 pane render at ~33px on a
563
+ // 1080p screen — the "font is huge" complaint. A single desktop range is
564
+ // enough; /view is not targeting phones.
565
+ const MIN_PX = 12, MAX_PX = 22;
566
+
567
+ // Order: fit → clamp → user density → absolute readability bounds. When the
568
+ // clamped size can't show all `cols`, #term scrolls rather than shrinking
569
+ // below the floor.
570
+ function fitFont() {
571
+ if (paneCols < 1 || paneRows < 1) return; // no trustworthy size yet
572
+ const term = q("term"), pre = q("pre");
573
+ const cs = getComputedStyle(term);
574
+ const padX = parseFloat(cs.paddingLeft) + parseFloat(cs.paddingRight);
575
+ const padY = parseFloat(cs.paddingTop) + parseFloat(cs.paddingBottom);
576
+ const innerW = Math.max(1, term.clientWidth - (padX || 0));
577
+ const innerH = Math.max(1, term.clientHeight - (padY || 0));
578
+ const key = `${paneCols}x${paneRows}@${Math.round(innerW)}x${Math.round(innerH)}/${density}`;
579
+ if (key === lastFitKey) return;
580
+ const pcs = getComputedStyle(pre);
581
+ const lhRatio = parseFloat(pcs.lineHeight) / parseFloat(pcs.fontSize);
582
+ const lineH = Number.isFinite(lhRatio) && lhRatio > 0 ? lhRatio : 1.2;
583
+ const widthFit = innerW / (paneCols * glyphRatio());
584
+ const heightFit = innerH / (paneRows * lineH);
585
+ // 0.5% slack keeps an exact fit from rounding into a scrollbar (paired with
586
+ // scrollbar-gutter:stable, that's what stops resize→scrollbar→resize loops).
587
+ let size = Math.min(widthFit, heightFit) * 0.995;
588
+ size = Math.max(MIN_PX, Math.min(size, MAX_PX));
589
+ size *= DENSITY[density] || 1;
590
+ size = Math.max(8, Math.min(size, 32));
591
+ lastFitKey = key;
592
+ // Sub-pixel churn (a scrollbar appearing shrinks the box, which would shrink
593
+ // the font, which would hide the scrollbar again) resolves to a no-op here.
594
+ if (Math.abs(size - appliedSize) < 0.75) return;
595
+ appliedSize = size;
596
+ pre.style.fontSize = size.toFixed(2) + "px";
597
+ }
598
+
599
+ function scheduleFit() {
600
+ clearTimeout(resizeTimer);
601
+ resizeTimer = setTimeout(() => { lastFitKey = ""; fitFont(); }, 80);
602
+ }
603
+
604
+ function renderDensityBtn() {
605
+ const b = q("densityBtn");
606
+ if (!b) return;
607
+ b.textContent = DENSITY_GLYPH[density];
608
+ b.title = `${T("densityLabel")}: ${T("density" + density.charAt(0).toUpperCase() + density.slice(1))}`;
609
+ }
610
+
611
+ function cycleDensity() {
612
+ density = DENSITY_ORDER[(DENSITY_ORDER.indexOf(density) + 1) % DENSITY_ORDER.length];
613
+ localStorage.setItem("agend_view_density", density);
614
+ renderDensityBtn();
615
+ lastFitKey = "";
616
+ fitFont();
474
617
  }
475
618
 
476
619
  function select(name) {
477
620
  if (name === current) return;
478
- current = name; fitted = false;
621
+ current = name;
622
+ lastFitKey = ""; // force a refit once the new pane's size lands
479
623
  exitEdit();
480
624
  q("pre").textContent = ""; // blank the old instance's output immediately
481
625
  renderList(); renderCard(); refreshPane();
482
626
  }
483
627
 
484
628
  // ── Edit ──────────────────────────────────────────────
629
+ // The card is a one-line bar by default; expanding it (or editing) is what
630
+ // borrows height back from the terminal.
631
+ let cardExpanded = localStorage.getItem("agend_view_card_expanded") === "1";
632
+ function applyCardExpanded(on) {
633
+ q("card").classList.toggle("expanded", on);
634
+ const t = q("cardToggle");
635
+ t.textContent = on ? "▼" : "▲";
636
+ t.setAttribute("aria-expanded", on ? "true" : "false");
637
+ scheduleFit(); // the terminal just gained or lost height
638
+ }
639
+ function setCardExpanded(on, persist) {
640
+ if (persist) { cardExpanded = on; localStorage.setItem("agend_view_card_expanded", on ? "1" : "0"); }
641
+ applyCardExpanded(on);
642
+ }
643
+
485
644
  function enterEdit() {
486
645
  const it = roster.find(r => r.instance_name === current);
487
646
  if (!it) return;
@@ -490,14 +649,18 @@
490
649
  q("fDesc").value = it.description || "";
491
650
  q("fToken").value = localStorage.getItem("agend_web_token") || "";
492
651
  q("msg").textContent = "";
652
+ applyCardExpanded(true); // form needs the room; don't persist it
493
653
  q("cardBody").style.display = "none";
654
+ q("cardToggle").style.display = "none";
494
655
  q("editBtn").style.display = "none";
495
656
  q("edit").style.display = "flex";
496
657
  }
497
658
  function exitEdit() {
498
659
  q("edit").style.display = "none";
499
660
  q("cardBody").style.display = "";
661
+ q("cardToggle").style.display = "";
500
662
  q("editBtn").style.display = "";
663
+ applyCardExpanded(cardExpanded); // back to the user's own preference
501
664
  }
502
665
 
503
666
  async function save() {
@@ -529,13 +692,108 @@
529
692
  q("editBtn").onclick = enterEdit;
530
693
  q("cancelBtn").onclick = exitEdit;
531
694
  q("saveBtn").onclick = save;
695
+ q("cardToggle").onclick = () => setCardExpanded(!q("card").classList.contains("expanded"), true);
532
696
 
533
697
  const setHelp = (open) => { q("helpPanel").classList.toggle("open", open); q("helpBackdrop").classList.toggle("open", open); };
534
698
  q("helpBtn").onclick = () => setHelp(!q("helpPanel").classList.contains("open"));
535
699
  q("helpClose").onclick = () => setHelp(false);
536
700
  q("helpBackdrop").onclick = () => setHelp(false);
537
701
  q("langBtn").onclick = () => { lang = lang === "en" ? "zh-TW" : "en"; localStorage.setItem("agend_lang", lang); applyI18n(); };
702
+ q("densityBtn").onclick = cycleDensity;
703
+ window.addEventListener("keydown", (e) => { if (e.key === "Escape") setHelp(false); });
704
+
705
+ // ── AI usage panel ────────────────────────────────────
706
+ let usageData = null;
707
+ let usageTimer = null;
708
+
709
+ function usageResetText(iso) {
710
+ if (!iso) return "";
711
+ const ms = new Date(iso).getTime() - Date.now();
712
+ if (isNaN(ms)) return "";
713
+ if (ms <= 0) return T("usageSoon");
714
+ const h = Math.floor(ms / 3600000), m = Math.floor((ms % 3600000) / 60000);
715
+ const t = h >= 48 ? `${Math.floor(h / 24)}d ${h % 24}h` : h > 0 ? `${h}h ${m}m` : `${m}m`;
716
+ return T("usageResets").replace("{t}", t);
717
+ }
718
+
719
+ function usageMetricHtml(m) {
720
+ if (m.type === "percent") {
721
+ const pct = Math.min(100, Math.max(0, m.used ?? 0));
722
+ const cls = pct >= 90 ? "crit" : pct >= 70 ? "warn" : "";
723
+ const word = pct >= 90 ? T("usageNear") : pct >= 70 ? T("usageHigh") : "";
724
+ const sub = [m.note, usageResetText(m.resetsAt)].filter(Boolean).join(" · ");
725
+ return `<div class="u-metric"><div class="u-row"><span class="u-label">${esc(m.label)}</span>` +
726
+ (word ? `<span class="u-status ${cls}">${esc(word)}</span>` : "") +
727
+ `<span class="u-val">${pct.toFixed(0)}%</span></div>` +
728
+ `<div class="u-meter"><div class="u-fill ${cls}" style="width:${pct}%"></div></div>` +
729
+ (sub ? `<div class="u-sub">${esc(sub)}</div>` : "") + `</div>`;
730
+ }
731
+ if (m.type === "dollars") {
732
+ const val = m.limit ? `$${(m.used ?? 0).toFixed(2)} / $${m.limit.toFixed(2)}` : `$${(m.used ?? 0).toFixed(2)}`;
733
+ return `<div class="u-metric"><div class="u-row"><span class="u-label">${esc(m.label)}</span><span class="u-val">${esc(val)}</span></div></div>`;
734
+ }
735
+ const val = `${m.value ?? ""} ${m.unit ?? ""}`.trim();
736
+ return `<div class="u-metric"><div class="u-row"><span class="u-label">${esc(m.label)}</span><span class="u-val">${esc(val)}</span></div>` +
737
+ (m.note ? `<div class="u-sub">${esc(m.note)}</div>` : "") + `</div>`;
738
+ }
739
+
740
+ function renderUsage() {
741
+ const box = q("usageContent");
742
+ if (!usageData || !Array.isArray(usageData.providers)) { box.innerHTML = `<h3>${T("usageTitle")}</h3><div class="u-err">${T("usageFail")}</div>`; return; }
743
+ let html = `<h3>${T("usageTitle")}</h3>`;
744
+ if (!usageData.providers.length) html += `<div class="u-err">${T("usageEmpty")}</div>`;
745
+ for (const p of usageData.providers) {
746
+ html += `<div class="u-provider"><div class="u-head"><span class="u-name">${esc(p.name)}</span>` +
747
+ (p.plan ? `<span class="u-plan">${esc(p.plan)}</span>` : "") + `</div>`;
748
+ if (p.status === "error") html += `<div class="u-err">${esc(p.error || "error")}</div>`;
749
+ else {
750
+ html += (p.metrics || []).map(usageMetricHtml).join("");
751
+ // A provider with nothing to meter (subscription plan) or one awaiting
752
+ // sign-in carries a hint instead of metrics. Without this the panel
753
+ // rendered an empty box, so those states looked broken.
754
+ if (p.hint) html += `<div class="u-note">${esc(p.hint)}</div>`;
755
+ }
756
+ html += `</div>`;
757
+ }
758
+ const at = new Date(usageData.fetchedAt).toLocaleTimeString();
759
+ html += `<div class="u-foot"><span>${T("usageUpdated")} ${esc(at)}</span><button id="usageRefresh">${T("usageRefresh")}</button></div>`;
760
+ box.innerHTML = html;
761
+ const rb = q("usageRefresh");
762
+ if (rb) rb.onclick = () => loadUsage(true);
763
+ }
764
+
765
+ async function loadUsage(force) {
766
+ try {
767
+ const r = await fetch(`/api/ai-usage${force ? "?force=1" : ""}`);
768
+ if (r.status === 404) { q("usageBtn").style.display = "none"; return false; }
769
+ usageData = await r.json();
770
+ } catch { usageData = null; }
771
+ renderUsage();
772
+ return true;
773
+ }
774
+
775
+ const setUsage = (open) => {
776
+ q("usagePanel").classList.toggle("open", open);
777
+ q("helpBackdrop").classList.toggle("open", open);
778
+ if (open) { setHelp(false); loadUsage(false); if (!usageTimer) usageTimer = setInterval(() => loadUsage(false), 60_000); }
779
+ else if (usageTimer) { clearInterval(usageTimer); usageTimer = null; }
780
+ };
781
+ q("usageBtn").onclick = () => setUsage(!q("usagePanel").classList.contains("open"));
782
+ q("usageClose").onclick = () => setUsage(false);
783
+ q("helpBackdrop").addEventListener("click", () => setUsage(false));
784
+ q("helpBtn").addEventListener("click", () => setUsage(false));
785
+ window.addEventListener("keydown", (e) => { if (e.key === "Escape") setUsage(false); });
786
+ q("langBtn").addEventListener("click", () => { if (usageData) renderUsage(); });
787
+ // Probe once at boot: shows the 📊 button only when the endpoint is enabled,
788
+ // and warms the server-side 5-minute cache so the first open is instant.
789
+ loadUsage(false).then(ok => { if (ok !== false) q("usageBtn").style.display = ""; });
790
+
791
+ window.addEventListener("resize", scheduleFit);
792
+ // Catches container changes the window resize event doesn't see — chiefly the
793
+ // profile card expanding or collapsing.
794
+ if (window.ResizeObserver) new ResizeObserver(scheduleFit).observe(q("term"));
538
795
  applyI18n();
796
+ applyCardExpanded(cardExpanded);
539
797
 
540
798
  // ── Boot ──────────────────────────────────────────────
541
799
  loadRoster().then(() => { refreshPane(); });
@@ -0,0 +1,22 @@
1
+ import { execFileSync } from "node:child_process";
2
+ export interface UpdateVersionOptions {
3
+ version?: string;
4
+ beta?: boolean;
5
+ force?: boolean;
6
+ }
7
+ /** Resolve the npm selector used by `agend update`. */
8
+ export declare function getUpdateSelector(opts: UpdateVersionOptions): string;
9
+ /**
10
+ * Query the registry without making update availability a hard dependency.
11
+ * A failed lookup deliberately returns null so the existing install flow can
12
+ * continue during registry/network outages.
13
+ */
14
+ export declare function lookupTargetVersion(selector: string, run?: typeof execFileSync): string | null;
15
+ export declare function shouldSkipUpdate(currentVersion: string, targetVersion: string | null, force?: boolean): boolean;
16
+ interface UpdateOutput {
17
+ log(message: string): void;
18
+ error(message: string): void;
19
+ }
20
+ /** Report restart status and let the CLI translate failure into exit code 1. */
21
+ export declare function reportUpdateRestart(status: number | null, output?: UpdateOutput): boolean;
22
+ export {};
@@ -0,0 +1,44 @@
1
+ import { execFileSync } from "node:child_process";
2
+ /** Resolve the npm selector used by `agend update`. */
3
+ export function getUpdateSelector(opts) {
4
+ return opts.version ?? (opts.beta ? "beta" : "latest");
5
+ }
6
+ /**
7
+ * Query the registry without making update availability a hard dependency.
8
+ * A failed lookup deliberately returns null so the existing install flow can
9
+ * continue during registry/network outages.
10
+ */
11
+ export function lookupTargetVersion(selector, run = execFileSync) {
12
+ try {
13
+ const output = run("npm", ["view", `@songsid/agend@${selector}`, "version"], {
14
+ encoding: "utf-8",
15
+ stdio: ["ignore", "pipe", "ignore"],
16
+ timeout: 15_000,
17
+ });
18
+ const version = String(output).trim();
19
+ return version || null;
20
+ }
21
+ catch {
22
+ return null;
23
+ }
24
+ }
25
+ function normalizeVersion(version) {
26
+ return version.trim().replace(/^v/i, "");
27
+ }
28
+ export function shouldSkipUpdate(currentVersion, targetVersion, force = false) {
29
+ return !force
30
+ && targetVersion !== null
31
+ && normalizeVersion(currentVersion) === normalizeVersion(targetVersion);
32
+ }
33
+ /** Report restart status and let the CLI translate failure into exit code 1. */
34
+ export function reportUpdateRestart(status, output = console) {
35
+ if (status === 0) {
36
+ output.log(" ✓ Service restarted");
37
+ return true;
38
+ }
39
+ output.error("\n ✗ Auto-restart FAILED. Fleet may be stopped.");
40
+ output.error(" Run: agend start");
41
+ output.error(" Service status: agend status\n");
42
+ return false;
43
+ }
44
+ //# sourceMappingURL=update-check.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"update-check.js","sourceRoot":"","sources":["../src/update-check.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAQlD,uDAAuD;AACvD,MAAM,UAAU,iBAAiB,CAAC,IAA0B;IAC1D,OAAO,IAAI,CAAC,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;AACzD,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,mBAAmB,CACjC,QAAgB,EAChB,MAA2B,YAAY;IAEvC,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,GAAG,CAChB,KAAK,EACL,CAAC,MAAM,EAAE,kBAAkB,QAAQ,EAAE,EAAE,SAAS,CAAC,EACjD;YACE,QAAQ,EAAE,OAAO;YACjB,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,CAAC;YACnC,OAAO,EAAE,MAAM;SAChB,CACF,CAAC;QACF,MAAM,OAAO,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC;QACtC,OAAO,OAAO,IAAI,IAAI,CAAC;IACzB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,SAAS,gBAAgB,CAAC,OAAe;IACvC,OAAO,OAAO,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;AAC3C,CAAC;AAED,MAAM,UAAU,gBAAgB,CAC9B,cAAsB,EACtB,aAA4B,EAC5B,KAAK,GAAG,KAAK;IAEb,OAAO,CAAC,KAAK;WACR,aAAa,KAAK,IAAI;WACtB,gBAAgB,CAAC,cAAc,CAAC,KAAK,gBAAgB,CAAC,aAAa,CAAC,CAAC;AAC5E,CAAC;AAOD,gFAAgF;AAChF,MAAM,UAAU,mBAAmB,CACjC,MAAqB,EACrB,SAAuB,OAAO;IAE9B,IAAI,MAAM,KAAK,CAAC,EAAE,CAAC;QACjB,MAAM,CAAC,GAAG,CAAC,uBAAuB,CAAC,CAAC;QACpC,OAAO,IAAI,CAAC;IACd,CAAC;IAED,MAAM,CAAC,KAAK,CAAC,kDAAkD,CAAC,CAAC;IACjE,MAAM,CAAC,KAAK,CAAC,oBAAoB,CAAC,CAAC;IACnC,MAAM,CAAC,KAAK,CAAC,kCAAkC,CAAC,CAAC;IACjD,OAAO,KAAK,CAAC;AACf,CAAC"}
@@ -0,0 +1,50 @@
1
+ export interface UsageMetric {
2
+ label: string;
3
+ type: "percent" | "dollars" | "count" | "text";
4
+ used?: number;
5
+ limit?: number;
6
+ value?: number | string;
7
+ unit?: string;
8
+ note?: string;
9
+ resetsAt?: string | null;
10
+ windowMs?: number | null;
11
+ }
12
+ export interface ProviderUsage {
13
+ id: string;
14
+ name: string;
15
+ status: "ok" | "error" | "no-credentials";
16
+ plan?: string | null;
17
+ error?: string;
18
+ hint?: string;
19
+ metrics: UsageMetric[];
20
+ }
21
+ interface KiroToken {
22
+ access_token?: string;
23
+ expires_at?: string;
24
+ region?: string;
25
+ profile_arn?: string;
26
+ /** IAM Identity Center portal URL — present only for Q Developer Pro logins. */
27
+ start_url?: string;
28
+ /** Social login provider (google/github/…) — present only for free-tier logins. */
29
+ provider?: string;
30
+ }
31
+ /**
32
+ * Which Kiro login is in use. Verified against a real kiro-cli auth store:
33
+ * - IAM Identity Center (Amazon Q Developer Pro) tokens carry `start_url`
34
+ * (e.g. `https://d-xxxx.awsapps.com/start/`) and no social `provider`.
35
+ * - Free-tier social logins (Builder ID / Google / GitHub) carry `provider`
36
+ * and a `profile_arn`.
37
+ * Q Developer Pro is a seat-licensed subscription, so the credit/usage endpoint
38
+ * that Builder ID reports against does not describe it.
39
+ */
40
+ export type KiroAuthKind = "q-developer-pro" | "builder-id";
41
+ export declare function kiroAuthKind(token: KiroToken): KiroAuthKind;
42
+ /** Exported for tests: the Q-Pro and expired paths return without any network call. */
43
+ export declare function fetchKiroUsage(): Promise<Omit<ProviderUsage, "id" | "name">>;
44
+ /** Fetch every provider in parallel. Providers without local credentials are
45
+ * skipped (per the panel's contract: absent CLIs simply don't show). */
46
+ export declare function fetchAllUsage(): Promise<{
47
+ fetchedAt: string;
48
+ providers: ProviderUsage[];
49
+ }>;
50
+ export {};