synabun 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (187) hide show
  1. package/CHANGELOG.md +665 -0
  2. package/LICENSE +191 -0
  3. package/README.md +853 -0
  4. package/hooks/claude-code/post-plan.mjs +391 -0
  5. package/hooks/claude-code/post-remember.mjs +239 -0
  6. package/hooks/claude-code/pre-compact.mjs +241 -0
  7. package/hooks/claude-code/pre-websearch.mjs +90 -0
  8. package/hooks/claude-code/prompt-submit.mjs +861 -0
  9. package/hooks/claude-code/session-start.mjs +494 -0
  10. package/hooks/claude-code/shared.mjs +368 -0
  11. package/hooks/claude-code/stop.mjs +646 -0
  12. package/hooks/claude-code/test-user-learning.mjs +531 -0
  13. package/mcp-server/package.json +38 -0
  14. package/mcp-server/run.mjs +10 -0
  15. package/mcp-server/src/config.ts +70 -0
  16. package/mcp-server/src/http.ts +93 -0
  17. package/mcp-server/src/index.ts +203 -0
  18. package/mcp-server/src/preload-http.ts +25 -0
  19. package/mcp-server/src/preload.ts +29 -0
  20. package/mcp-server/src/scripts/migrate-qdrant-to-sqlite.ts +506 -0
  21. package/mcp-server/src/services/categories.ts +429 -0
  22. package/mcp-server/src/services/discord.ts +702 -0
  23. package/mcp-server/src/services/file-checksums.ts +33 -0
  24. package/mcp-server/src/services/local-embeddings.ts +77 -0
  25. package/mcp-server/src/services/neural-interface.ts +344 -0
  26. package/mcp-server/src/services/sqlite.ts +823 -0
  27. package/mcp-server/src/tools/browser-advanced.ts +123 -0
  28. package/mcp-server/src/tools/browser-interact.ts +255 -0
  29. package/mcp-server/src/tools/browser-navigate.ts +77 -0
  30. package/mcp-server/src/tools/browser-observe.ts +1330 -0
  31. package/mcp-server/src/tools/browser.ts +107 -0
  32. package/mcp-server/src/tools/card-tools.ts +193 -0
  33. package/mcp-server/src/tools/card.ts +22 -0
  34. package/mcp-server/src/tools/category.ts +379 -0
  35. package/mcp-server/src/tools/discord-channel.ts +274 -0
  36. package/mcp-server/src/tools/discord-guild.ts +188 -0
  37. package/mcp-server/src/tools/discord-member.ts +158 -0
  38. package/mcp-server/src/tools/discord-message.ts +231 -0
  39. package/mcp-server/src/tools/discord-onboarding.ts +228 -0
  40. package/mcp-server/src/tools/discord-role.ts +209 -0
  41. package/mcp-server/src/tools/discord-thread.ts +110 -0
  42. package/mcp-server/src/tools/discord-webhook.ts +176 -0
  43. package/mcp-server/src/tools/discord.ts +40 -0
  44. package/mcp-server/src/tools/forget.ts +38 -0
  45. package/mcp-server/src/tools/git-tools.ts +141 -0
  46. package/mcp-server/src/tools/git.ts +13 -0
  47. package/mcp-server/src/tools/image-tools.ts +98 -0
  48. package/mcp-server/src/tools/image.ts +17 -0
  49. package/mcp-server/src/tools/leonardo-browser-tools.ts +281 -0
  50. package/mcp-server/src/tools/leonardo.ts +28 -0
  51. package/mcp-server/src/tools/loop.ts +348 -0
  52. package/mcp-server/src/tools/memories.ts +118 -0
  53. package/mcp-server/src/tools/recall.ts +323 -0
  54. package/mcp-server/src/tools/reflect.ts +145 -0
  55. package/mcp-server/src/tools/remember.ts +115 -0
  56. package/mcp-server/src/tools/response.ts +26 -0
  57. package/mcp-server/src/tools/restore.ts +34 -0
  58. package/mcp-server/src/tools/sync.ts +114 -0
  59. package/mcp-server/src/tools/tictactoe-tools.ts +98 -0
  60. package/mcp-server/src/tools/tictactoe.ts +13 -0
  61. package/mcp-server/src/tools/utils.ts +25 -0
  62. package/mcp-server/src/tools/whiteboard-tools.ts +260 -0
  63. package/mcp-server/src/tools/whiteboard.ts +22 -0
  64. package/mcp-server/src/tui.ts +940 -0
  65. package/mcp-server/src/types.ts +85 -0
  66. package/mcp-server/tsconfig.json +19 -0
  67. package/neural-interface/package.json +27 -0
  68. package/neural-interface/public/apple-touch-icon.png +0 -0
  69. package/neural-interface/public/category-logos/claude.svg +1 -0
  70. package/neural-interface/public/category-logos/gemini-color.svg +1 -0
  71. package/neural-interface/public/category-logos/openai.svg +1 -0
  72. package/neural-interface/public/category-logos/synabun.png +0 -0
  73. package/neural-interface/public/claude-chat.css +878 -0
  74. package/neural-interface/public/claude-chat.html +92 -0
  75. package/neural-interface/public/claude-chat.js +1168 -0
  76. package/neural-interface/public/favicon-16x16.png +0 -0
  77. package/neural-interface/public/favicon-32x32.png +0 -0
  78. package/neural-interface/public/gallery.svg +6 -0
  79. package/neural-interface/public/icon-192.png +0 -0
  80. package/neural-interface/public/icon-512.png +0 -0
  81. package/neural-interface/public/index-legacy.html +14677 -0
  82. package/neural-interface/public/index.html +212 -0
  83. package/neural-interface/public/index2d-legacy.html +8193 -0
  84. package/neural-interface/public/index2d.html +127 -0
  85. package/neural-interface/public/invite.html +351 -0
  86. package/neural-interface/public/leonardoai.svg +1 -0
  87. package/neural-interface/public/logo-synabun.png +0 -0
  88. package/neural-interface/public/logoHD.png +0 -0
  89. package/neural-interface/public/manifest.json +14 -0
  90. package/neural-interface/public/offline.html +299 -0
  91. package/neural-interface/public/onboarding.html +2335 -0
  92. package/neural-interface/public/openclaw-logo-text.png +0 -0
  93. package/neural-interface/public/pin-svgrepo-com.svg +25 -0
  94. package/neural-interface/public/sessionicon.svg +8 -0
  95. package/neural-interface/public/shared/ansi-parser.js +305 -0
  96. package/neural-interface/public/shared/api.js +1043 -0
  97. package/neural-interface/public/shared/colors.js +153 -0
  98. package/neural-interface/public/shared/constants.js +141 -0
  99. package/neural-interface/public/shared/html-shell.js +777 -0
  100. package/neural-interface/public/shared/html-term-renderer.js +1022 -0
  101. package/neural-interface/public/shared/i18n.js +147 -0
  102. package/neural-interface/public/shared/registry.js +82 -0
  103. package/neural-interface/public/shared/state.js +86 -0
  104. package/neural-interface/public/shared/storage.js +150 -0
  105. package/neural-interface/public/shared/styles.css +21212 -0
  106. package/neural-interface/public/shared/ui-automation-studio.js +3573 -0
  107. package/neural-interface/public/shared/ui-bookmarks.js +231 -0
  108. package/neural-interface/public/shared/ui-claude-panel.js +6333 -0
  109. package/neural-interface/public/shared/ui-command-runner.js +534 -0
  110. package/neural-interface/public/shared/ui-cost-widget.js +602 -0
  111. package/neural-interface/public/shared/ui-detail.js +1454 -0
  112. package/neural-interface/public/shared/ui-explorer.js +1356 -0
  113. package/neural-interface/public/shared/ui-file-explorer.js +2750 -0
  114. package/neural-interface/public/shared/ui-games.js +411 -0
  115. package/neural-interface/public/shared/ui-help.js +107 -0
  116. package/neural-interface/public/shared/ui-image-gallery.js +461 -0
  117. package/neural-interface/public/shared/ui-invite.js +519 -0
  118. package/neural-interface/public/shared/ui-keybinds.js +452 -0
  119. package/neural-interface/public/shared/ui-layouts.js +297 -0
  120. package/neural-interface/public/shared/ui-link.js +731 -0
  121. package/neural-interface/public/shared/ui-loading.js +255 -0
  122. package/neural-interface/public/shared/ui-menubar.js +499 -0
  123. package/neural-interface/public/shared/ui-multiselect.js +93 -0
  124. package/neural-interface/public/shared/ui-navbar.js +222 -0
  125. package/neural-interface/public/shared/ui-notifications.js +287 -0
  126. package/neural-interface/public/shared/ui-panels.js +309 -0
  127. package/neural-interface/public/shared/ui-resume.js +653 -0
  128. package/neural-interface/public/shared/ui-search.js +145 -0
  129. package/neural-interface/public/shared/ui-sessions.js +531 -0
  130. package/neural-interface/public/shared/ui-settings.js +5987 -0
  131. package/neural-interface/public/shared/ui-sidebar.js +1905 -0
  132. package/neural-interface/public/shared/ui-skills.js +2805 -0
  133. package/neural-interface/public/shared/ui-stats.js +38 -0
  134. package/neural-interface/public/shared/ui-sync.js +201 -0
  135. package/neural-interface/public/shared/ui-terminal.js +6385 -0
  136. package/neural-interface/public/shared/ui-tooltip.js +123 -0
  137. package/neural-interface/public/shared/ui-trash.js +439 -0
  138. package/neural-interface/public/shared/ui-tutorial-draw.js +875 -0
  139. package/neural-interface/public/shared/ui-tutorial-steps.js +2276 -0
  140. package/neural-interface/public/shared/ui-tutorial.js +1117 -0
  141. package/neural-interface/public/shared/ui-update.js +244 -0
  142. package/neural-interface/public/shared/ui-whiteboard.js +3262 -0
  143. package/neural-interface/public/shared/ui-workspaces.js +722 -0
  144. package/neural-interface/public/shared/utils.js +360 -0
  145. package/neural-interface/public/shared/vterm-buffer.js +1102 -0
  146. package/neural-interface/public/sw.js +52 -0
  147. package/neural-interface/public/synabun.png +0 -0
  148. package/neural-interface/public/toolbar-album.svg +8 -0
  149. package/neural-interface/public/toolbar-question.svg +6 -0
  150. package/neural-interface/public/toolbar-sessions.svg +6 -0
  151. package/neural-interface/public/variant/2d/background.js +113 -0
  152. package/neural-interface/public/variant/2d/context-menu.js +133 -0
  153. package/neural-interface/public/variant/2d/gfx.js +45 -0
  154. package/neural-interface/public/variant/2d/graph.js +1635 -0
  155. package/neural-interface/public/variant/2d/hulls.js +207 -0
  156. package/neural-interface/public/variant/2d/lasso.js +135 -0
  157. package/neural-interface/public/variant/2d/layout.js +594 -0
  158. package/neural-interface/public/variant/2d/main.js +521 -0
  159. package/neural-interface/public/variant/2d/minimap.js +312 -0
  160. package/neural-interface/public/variant/2d/settings-gfx.js +98 -0
  161. package/neural-interface/public/variant/3d/background.js +884 -0
  162. package/neural-interface/public/variant/3d/camera.js +661 -0
  163. package/neural-interface/public/variant/3d/gfx.js +116 -0
  164. package/neural-interface/public/variant/3d/graph.js +2399 -0
  165. package/neural-interface/public/variant/3d/main.js +500 -0
  166. package/neural-interface/public/variant/3d/settings-gfx.js +228 -0
  167. package/neural-interface/scripts/rebuild-pty.js +100 -0
  168. package/neural-interface/server.js +15000 -0
  169. package/neural-interface/templates/CLAUDE-template.md +211 -0
  170. package/package.json +53 -0
  171. package/postinstall.js +135 -0
  172. package/setup.js +260 -0
  173. package/skills/leonardo/SKILL.md +159 -0
  174. package/skills/leonardo/icon.svg +1 -0
  175. package/skills/leonardo/modules/browser-guide.md +378 -0
  176. package/skills/leonardo/modules/image-prompter.md +277 -0
  177. package/skills/leonardo/modules/model-advisor.md +100 -0
  178. package/skills/leonardo/modules/prompt-library.md +110 -0
  179. package/skills/leonardo/modules/style-guide.md +186 -0
  180. package/skills/leonardo/modules/video-prompter.md +311 -0
  181. package/skills/synabun/SKILL.md +185 -0
  182. package/skills/synabun/icon.png +0 -0
  183. package/skills/synabun/modules/audit.md +374 -0
  184. package/skills/synabun/modules/changelog.md +194 -0
  185. package/skills/synabun/modules/idea.md +144 -0
  186. package/skills/synabun/modules/memorize.md +188 -0
  187. package/skills/synabun/synabunicon.png +0 -0
@@ -0,0 +1,602 @@
1
+ // ═══════════════════════════════════════════
2
+ // UI-COST-WIDGET — Cost tracker (dockable + floating)
3
+ // ═══════════════════════════════════════════
4
+ //
5
+ // Glass panel showing cost breakdown with daily bar chart,
6
+ // filter tabs, and drag/collapse persistence.
7
+ // Can dock to navbar (next to ?) or float freely.
8
+
9
+ import { on, emit } from './state.js';
10
+ import { KEYS } from './constants.js';
11
+ import { storage } from './storage.js';
12
+
13
+ // ── State ──
14
+
15
+ let _widget = null; // Root DOM element
16
+ let _costData = null; // Cached API response
17
+ let _activeTab = 'today'; // 'today' | 'month' | 'last30' | 'all'
18
+ let _collapsed = false;
19
+ let _drag = null;
20
+ let _pinnedOpen = false; // Toggled open from Claude Panel (independent of focus mode)
21
+ let _docked = false; // Docked to navbar
22
+
23
+ // ── Init ──
24
+
25
+ export function initCostWidget() {
26
+ const container = document.getElementById('static-bg');
27
+ console.log('[cost-widget] init, container:', !!container);
28
+ if (!container) return;
29
+
30
+ _collapsed = storage.getItem(KEYS.COST_WIDGET_COLLAPSED) === 'true';
31
+ const _dockedPref = storage.getItem(KEYS.COST_WIDGET_DOCKED);
32
+ _docked = _dockedPref === null ? true : _dockedPref === 'true';
33
+
34
+ _widget = document.createElement('div');
35
+ _widget.id = 'cost-widget';
36
+ _widget.className = 'glass cost-widget' + (_collapsed ? ' collapsed' : '') + (_docked ? ' cw-docked' : '');
37
+
38
+ // Restore position or default to bottom-right (clamped to viewport)
39
+ if (!_docked) {
40
+ const savedPos = _loadPos();
41
+ if (savedPos) {
42
+ const maxLeft = window.innerWidth - 270;
43
+ const maxTop = window.innerHeight - 60;
44
+ _widget.style.left = Math.max(0, Math.min(savedPos.left, maxLeft)) + 'px';
45
+ _widget.style.top = Math.max(0, Math.min(savedPos.top, maxTop)) + 'px';
46
+ } else {
47
+ _widget.style.right = '24px';
48
+ _widget.style.bottom = '24px';
49
+ }
50
+ }
51
+
52
+ _widget.innerHTML = `
53
+ <div class="cw-header">
54
+ <span class="cw-drag" title="Drag to undock and move">⠿</span>
55
+ <span class="cw-total">$0.00</span>
56
+ <span class="cw-month-label"></span>
57
+ <button class="cw-scan" title="Scan CLI sessions">⟳</button>
58
+ <button class="cw-collapse" title="Collapse">${_collapsed ? '▸' : '▾'}</button>
59
+ </div>
60
+ <div class="cw-body">
61
+ <div class="cw-tabs">
62
+ <button class="cw-tab active" data-tab="today">Today</button>
63
+ <button class="cw-tab" data-tab="month">Month</button>
64
+ <button class="cw-tab" data-tab="last30">30d</button>
65
+ <button class="cw-tab" data-tab="all">All</button>
66
+ </div>
67
+ <div class="cw-chart"></div>
68
+ <div class="cw-list"></div>
69
+ <div class="cw-footer"></div>
70
+ </div>
71
+ `;
72
+
73
+ document.body.appendChild(_widget);
74
+
75
+ // Show/hide based on current state
76
+ const focusOn = container.classList.contains('visible');
77
+ if (_docked) {
78
+ _widget.style.display = 'none'; // starts hidden, toggled by navbar click
79
+ } else {
80
+ _widget.style.display = focusOn ? '' : 'none';
81
+ }
82
+
83
+ // Events
84
+ _widget.querySelector('.cw-collapse').addEventListener('click', _toggleCollapse);
85
+ _widget.querySelector('.cw-scan').addEventListener('click', _scanCliSessions);
86
+ _widget.querySelector('.cw-tabs').addEventListener('click', _onTabClick);
87
+ _makeDraggable(_widget.querySelector('.cw-header'));
88
+
89
+ // Close docked widget when clicking outside
90
+ document.addEventListener('mousedown', (e) => {
91
+ if (!_docked || !_widget || _widget.style.display === 'none') return;
92
+ const costBtn = document.getElementById('titlebar-cost-btn');
93
+ if (_widget.contains(e.target) || costBtn?.contains(e.target)) return;
94
+ _widget.style.display = 'none';
95
+ _pinnedOpen = false;
96
+ });
97
+
98
+ // Listen for focus mode changes
99
+ on('viz:toggle', (vizEnabled) => {
100
+ if (_docked) return; // dock mode ignores focus mode
101
+ // vizEnabled=false means focus mode is ON
102
+ if (_widget) {
103
+ if (!vizEnabled) { _widget.style.display = ''; _pinnedOpen = false; }
104
+ else if (!_pinnedOpen) _widget.style.display = 'none';
105
+ }
106
+ if (!vizEnabled) _fetchAndRender();
107
+ });
108
+
109
+ // Toggle from Claude Panel cost label (floating mode)
110
+ on('cost:toggle', () => {
111
+ if (!_widget || _docked) return;
112
+ _pinnedOpen = !_pinnedOpen;
113
+ _widget.style.display = _pinnedOpen ? '' : 'none';
114
+ if (_pinnedOpen) _fetchAndRender();
115
+ });
116
+
117
+ // Toggle from navbar button (dock mode)
118
+ on('cost:dock-toggle', () => {
119
+ if (!_widget) return;
120
+ if (!_docked) {
121
+ // First click docks it
122
+ _dock();
123
+ _pinnedOpen = true;
124
+ _widget.style.display = '';
125
+ _fetchAndRender();
126
+ } else {
127
+ // Already docked — toggle visibility
128
+ _pinnedOpen = !_pinnedOpen;
129
+ _widget.style.display = _pinnedOpen ? '' : 'none';
130
+ if (_pinnedOpen) {
131
+ _positionDocked();
132
+ _fetchAndRender();
133
+ }
134
+ }
135
+ });
136
+
137
+ // Real-time cost updates from Claude Panel
138
+ on('cost:updated', (data) => {
139
+ if (!_costData || !data) return;
140
+ _optimisticUpdate(data.amount);
141
+ });
142
+
143
+ // Emit initial dock state for navbar button
144
+ emit('cost:dock-state', _docked);
145
+
146
+ // Initial fetch if already in focus mode or docked
147
+ if (_docked) { _positionDocked(); _fetchAndRender(); }
148
+ else if (focusOn) _fetchAndRender();
149
+ }
150
+
151
+ // ── Dock / Undock ──
152
+
153
+ function _dock() {
154
+ _docked = true;
155
+ storage.setItem(KEYS.COST_WIDGET_DOCKED, 'true');
156
+ _widget.classList.add('cw-docked');
157
+ // Clear inline position styles — CSS handles docked positioning
158
+ _widget.style.left = '';
159
+ _widget.style.top = '';
160
+ _widget.style.right = '';
161
+ _widget.style.bottom = '';
162
+ _positionDocked();
163
+ emit('cost:dock-state', true);
164
+ }
165
+
166
+ function _undock() {
167
+ _docked = false;
168
+ storage.setItem(KEYS.COST_WIDGET_DOCKED, 'false');
169
+ _widget.classList.remove('cw-docked');
170
+ emit('cost:dock-state', false);
171
+ }
172
+
173
+ function _positionDocked() {
174
+ const anchor = document.getElementById('titlebar-cost-btn');
175
+ if (!anchor || !_widget) return;
176
+ const rect = anchor.getBoundingClientRect();
177
+ _widget.style.top = (rect.bottom + 6) + 'px';
178
+ _widget.style.right = (window.innerWidth - rect.right) + 'px';
179
+ _widget.style.left = '';
180
+ _widget.style.bottom = '';
181
+ }
182
+
183
+ // ── Data ──
184
+
185
+ async function _fetchAndRender() {
186
+ try {
187
+ const res = await fetch('/api/claude-skin/cost');
188
+ if (!res.ok) return;
189
+ _costData = await res.json();
190
+ _render();
191
+ } catch {}
192
+ }
193
+
194
+ function _optimisticUpdate(amount) {
195
+ if (!_costData || typeof amount !== 'number') return;
196
+ const month = _costData.month;
197
+ if (!month) return;
198
+ month.totalUsd = Math.round((month.totalUsd + amount) * 1e6) / 1e6;
199
+ month.queries = (month.queries || 0) + 1;
200
+ // Update daily
201
+ if (!month.days) month.days = {};
202
+ const day = new Date().getDate().toString().padStart(2, '0');
203
+ if (!month.days[day]) month.days[day] = { totalUsd: 0, queries: 0 };
204
+ month.days[day].totalUsd = Math.round((month.days[day].totalUsd + amount) * 1e6) / 1e6;
205
+ month.days[day].queries += 1;
206
+ // Also update history
207
+ if (_costData.history && _costData.currentMonth) {
208
+ _costData.history[_costData.currentMonth] = month;
209
+ }
210
+ _render();
211
+ // Flash the total
212
+ const total = _widget?.querySelector('.cw-total');
213
+ if (total) { total.classList.add('flash'); setTimeout(() => total.classList.remove('flash'), 800); }
214
+ }
215
+
216
+ // ── Render ──
217
+
218
+ function _render() {
219
+ if (!_widget || !_costData) return;
220
+
221
+ const prepared = _prepareData();
222
+ const { dataPoints, listRows, totalUsd, label, avgPerDay, projected } = prepared;
223
+
224
+ // Header
225
+ _widget.querySelector('.cw-total').textContent = '$' + totalUsd.toFixed(2);
226
+ _widget.querySelector('.cw-month-label').textContent = label;
227
+
228
+ // Chart
229
+ _renderBarChart(_widget.querySelector('.cw-chart'), dataPoints);
230
+
231
+ // List
232
+ _renderList(_widget.querySelector('.cw-list'), listRows);
233
+
234
+ // Footer
235
+ const footer = _widget.querySelector('.cw-footer');
236
+ const parts = [];
237
+ if (prepared.queries != null) parts.push(`${prepared.queries} queries`);
238
+ if (avgPerDay != null) parts.push(`avg $${avgPerDay.toFixed(2)}/day`);
239
+ if (projected != null) parts.push(`~$${projected.toFixed(0)}/mo`);
240
+ footer.textContent = parts.join(' · ');
241
+
242
+ // Session count in header tooltip
243
+ const totalEl = _widget.querySelector('.cw-total');
244
+ if (totalEl && _costData) {
245
+ const currentMonth = _costData.currentMonth;
246
+ const month = _costData.history?.[currentMonth] || _costData.month;
247
+ const sessions = month?.sessions?.length || 0;
248
+ totalEl.title = `${sessions} sessions · ${month?.queries || 0} queries`;
249
+ }
250
+
251
+ // Navbar cost label is managed by ui-claude-panel._updateCostLabel() (session cost)
252
+ }
253
+
254
+ function _prepareData() {
255
+ const history = _costData.history || {};
256
+ const currentMonth = _costData.currentMonth;
257
+
258
+ if (_activeTab === 'today') {
259
+ return _prepareTodayData(history[currentMonth] || _costData.month);
260
+ } else if (_activeTab === 'month') {
261
+ return _prepareMonthData(history[currentMonth] || _costData.month, currentMonth);
262
+ } else if (_activeTab === 'last30') {
263
+ return _prepareLast30Data(history);
264
+ } else {
265
+ return _prepareAllData(history);
266
+ }
267
+ }
268
+
269
+ function _prepareTodayData(month) {
270
+ const now = new Date();
271
+ const dayKey = now.getDate().toString().padStart(2, '0');
272
+ const today = month?.days?.[dayKey] || { totalUsd: 0, queries: 0 };
273
+ const totalUsd = today.totalUsd || 0;
274
+ const queries = today.queries || 0;
275
+ const dateLabel = now.toLocaleDateString('en', { weekday: 'short', month: 'short', day: 'numeric' });
276
+
277
+ // Show last 7 days as bar chart for context
278
+ const dataPoints = [];
279
+ for (let i = 6; i >= 0; i--) {
280
+ const d = new Date(now);
281
+ d.setDate(d.getDate() - i);
282
+ const mk = d.toISOString().slice(0, 7);
283
+ const dk = d.getDate().toString().padStart(2, '0');
284
+ const mData = _costData.history?.[mk];
285
+ const val = mData?.days?.[dk]?.totalUsd || 0;
286
+ const q = mData?.days?.[dk]?.queries || 0;
287
+ const dayLabel = d.toLocaleDateString('en', { weekday: 'short' }).slice(0, 2);
288
+ dataPoints.push({
289
+ label: dayLabel,
290
+ value: val,
291
+ queries: q,
292
+ tooltip: `$${val.toFixed(2)} · ${d.toLocaleDateString('en', { month: 'short', day: 'numeric' })}`,
293
+ highlight: i === 0,
294
+ });
295
+ }
296
+
297
+ return {
298
+ dataPoints,
299
+ listRows: [],
300
+ totalUsd,
301
+ label: dateLabel,
302
+ queries,
303
+ avgPerDay: null,
304
+ projected: null,
305
+ };
306
+ }
307
+
308
+ function _prepareMonthData(month, monthKey) {
309
+ const days = month?.days || {};
310
+ const now = new Date();
311
+ const year = parseInt(monthKey?.slice(0, 4) || now.getFullYear());
312
+ const mo = parseInt(monthKey?.slice(5, 7) || (now.getMonth() + 1));
313
+ const daysInMonth = new Date(year, mo, 0).getDate();
314
+ const todayDay = now.getDate().toString().padStart(2, '0');
315
+ const monthLabel = new Date(year, mo - 1).toLocaleString('en', { month: 'short', year: 'numeric' });
316
+
317
+ const dataPoints = [];
318
+ for (let d = 1; d <= daysInMonth; d++) {
319
+ const key = d.toString().padStart(2, '0');
320
+ const val = days[key]?.totalUsd || 0;
321
+ dataPoints.push({
322
+ label: d.toString(),
323
+ value: val,
324
+ queries: days[key]?.queries || 0,
325
+ tooltip: `$${val.toFixed(2)} on ${monthLabel.split(' ')[0]} ${d}`,
326
+ highlight: key === todayDay,
327
+ });
328
+ }
329
+
330
+ const totalUsd = month?.totalUsd || 0;
331
+ const activeDays = Object.keys(days).length || 1;
332
+ const avgPerDay = totalUsd / activeDays;
333
+ const dayOfMonth = now.getDate();
334
+ const projected = (totalUsd / dayOfMonth) * daysInMonth;
335
+
336
+ // List rows — most recent days first
337
+ const listRows = Object.entries(days)
338
+ .sort((a, b) => b[0].localeCompare(a[0]))
339
+ .slice(0, 10)
340
+ .map(([d, data]) => ({
341
+ label: `${monthLabel.split(' ')[0]} ${parseInt(d)}`,
342
+ cost: data.totalUsd,
343
+ queries: data.queries,
344
+ }));
345
+
346
+ return { dataPoints, listRows, totalUsd, label: monthLabel, avgPerDay, projected };
347
+ }
348
+
349
+ function _prepareLast30Data(history) {
350
+ const now = new Date();
351
+ const dataPoints = [];
352
+ const listRows = [];
353
+ let total = 0;
354
+
355
+ for (let i = 29; i >= 0; i--) {
356
+ const d = new Date(now);
357
+ d.setDate(d.getDate() - i);
358
+ const monthKey = d.toISOString().slice(0, 7);
359
+ const dayKey = d.getDate().toString().padStart(2, '0');
360
+ const month = history[monthKey];
361
+ const val = month?.days?.[dayKey]?.totalUsd || 0;
362
+ const queries = month?.days?.[dayKey]?.queries || 0;
363
+ total += val;
364
+
365
+ const dayLabel = `${d.getMonth() + 1}/${d.getDate()}`;
366
+ dataPoints.push({
367
+ label: i % 5 === 0 ? dayLabel : '',
368
+ value: val,
369
+ queries,
370
+ tooltip: `$${val.toFixed(2)} on ${dayLabel}`,
371
+ highlight: i === 0,
372
+ });
373
+
374
+ if (val > 0) {
375
+ listRows.push({
376
+ label: d.toLocaleDateString('en', { month: 'short', day: 'numeric' }),
377
+ cost: val,
378
+ queries,
379
+ });
380
+ }
381
+ }
382
+
383
+ const activeDays = dataPoints.filter(p => p.value > 0).length || 1;
384
+ const avgPerDay = total / activeDays;
385
+
386
+ return {
387
+ dataPoints,
388
+ listRows: listRows.slice(0, 10),
389
+ totalUsd: Math.round(total * 1e6) / 1e6,
390
+ label: 'Last 30 days',
391
+ avgPerDay,
392
+ projected: avgPerDay * 30,
393
+ };
394
+ }
395
+
396
+ function _prepareAllData(history) {
397
+ const months = Object.entries(history).sort((a, b) => a[0].localeCompare(b[0]));
398
+ let total = 0;
399
+ let totalQueries = 0;
400
+
401
+ const dataPoints = months.map(([key, data]) => {
402
+ const val = data.totalUsd || 0;
403
+ total += val;
404
+ totalQueries += data.queries || 0;
405
+ const [y, m] = key.split('-');
406
+ const label = new Date(parseInt(y), parseInt(m) - 1).toLocaleString('en', { month: 'short' });
407
+ return {
408
+ label,
409
+ value: val,
410
+ queries: data.queries || 0,
411
+ tooltip: `$${val.toFixed(2)} in ${label} ${y}`,
412
+ highlight: key === new Date().toISOString().slice(0, 7),
413
+ };
414
+ });
415
+
416
+ const listRows = months.reverse().slice(0, 10).map(([key, data]) => {
417
+ const [y, m] = key.split('-');
418
+ return {
419
+ label: new Date(parseInt(y), parseInt(m) - 1).toLocaleString('en', { month: 'short', year: 'numeric' }),
420
+ cost: data.totalUsd || 0,
421
+ queries: data.queries || 0,
422
+ };
423
+ });
424
+
425
+ return {
426
+ dataPoints,
427
+ listRows,
428
+ totalUsd: Math.round(total * 1e6) / 1e6,
429
+ label: 'All time',
430
+ avgPerDay: null,
431
+ projected: null,
432
+ };
433
+ }
434
+
435
+ // ── Bar Chart (SVG) ──
436
+
437
+ function _renderBarChart(container, dataPoints) {
438
+ if (!container) return;
439
+ const W = 230, H = 72, PAD_BOTTOM = 14, PAD_TOP = 4;
440
+ const chartH = H - PAD_BOTTOM - PAD_TOP;
441
+ const barCount = dataPoints.length;
442
+ if (barCount === 0) { container.innerHTML = ''; return; }
443
+
444
+ const maxVal = Math.max(...dataPoints.map(p => p.value), 0.01);
445
+ const barW = Math.max(2, Math.min(8, (W - 8) / barCount - 1));
446
+ const gap = Math.max(1, ((W - 8) - barW * barCount) / barCount);
447
+ const totalW = barCount * (barW + gap);
448
+ const offsetX = (W - totalW) / 2;
449
+
450
+ let svg = `<svg width="${W}" height="${H}" viewBox="0 0 ${W} ${H}" xmlns="http://www.w3.org/2000/svg">`;
451
+
452
+ // Bars
453
+ dataPoints.forEach((p, i) => {
454
+ const x = offsetX + i * (barW + gap);
455
+ const h = Math.max(1, (p.value / maxVal) * chartH);
456
+ const y = PAD_TOP + chartH - h;
457
+ const fill = p.highlight ? 'var(--accent-gold)' : 'var(--accent-blue)';
458
+ const opacity = p.value > 0 ? (p.highlight ? 0.9 : 0.6) : 0.15;
459
+ const rx = Math.min(barW / 2, 2);
460
+ svg += `<rect class="cw-bar" x="${x}" y="${y}" width="${barW}" height="${h}" rx="${rx}" fill="${fill}" opacity="${opacity}" data-idx="${i}"><title>${p.tooltip}</title></rect>`;
461
+
462
+ // X-axis labels (sparse)
463
+ if (p.label) {
464
+ svg += `<text x="${x + barW / 2}" y="${H - 1}" text-anchor="middle" fill="var(--t-faint)" font-size="7" font-family="'JetBrains Mono', monospace">${p.label}</text>`;
465
+ }
466
+ });
467
+
468
+ svg += '</svg>';
469
+ container.innerHTML = svg;
470
+ }
471
+
472
+ // ── List ──
473
+
474
+ function _renderList(container, rows) {
475
+ if (!container) return;
476
+ if (rows.length === 0) {
477
+ container.innerHTML = '<div class="cw-empty">No data yet</div>';
478
+ return;
479
+ }
480
+ container.innerHTML = rows.map(r =>
481
+ `<div class="cw-row">
482
+ <span class="cw-row-label">${r.label}</span>
483
+ <span class="cw-row-cost">$${r.cost.toFixed(2)}</span>
484
+ <span class="cw-row-queries">${r.queries}q</span>
485
+ </div>`
486
+ ).join('');
487
+ }
488
+
489
+ // ── CLI Scan ──
490
+
491
+ async function _scanCliSessions() {
492
+ const btn = _widget?.querySelector('.cw-scan');
493
+ if (!btn) return;
494
+ btn.classList.add('spinning');
495
+ btn.disabled = true;
496
+ try {
497
+ const res = await fetch('/api/claude-skin/cost/scan', { method: 'POST' });
498
+ const data = await res.json();
499
+ if (data.added > 0) {
500
+ // Refetch to get updated totals
501
+ await _fetchAndRender();
502
+ const total = _widget?.querySelector('.cw-total');
503
+ if (total) { total.classList.add('flash'); setTimeout(() => total.classList.remove('flash'), 800); }
504
+ }
505
+ } catch {} finally {
506
+ btn.classList.remove('spinning');
507
+ btn.disabled = false;
508
+ }
509
+ }
510
+
511
+ // ── Tabs ──
512
+
513
+ function _onTabClick(e) {
514
+ const btn = e.target.closest('.cw-tab');
515
+ if (!btn) return;
516
+ const tab = btn.dataset.tab;
517
+ if (tab === _activeTab) return;
518
+ _activeTab = tab;
519
+ _widget.querySelectorAll('.cw-tab').forEach(t => t.classList.toggle('active', t.dataset.tab === tab));
520
+ _render();
521
+ }
522
+
523
+ // ── Collapse ──
524
+
525
+ function _toggleCollapse() {
526
+ _collapsed = !_collapsed;
527
+ _widget.classList.toggle('collapsed', _collapsed);
528
+ _widget.querySelector('.cw-collapse').textContent = _collapsed ? '▸' : '▾';
529
+ storage.setItem(KEYS.COST_WIDGET_COLLAPSED, _collapsed ? 'true' : 'false');
530
+ }
531
+
532
+ // ── Drag ──
533
+
534
+ function _makeDraggable(header) {
535
+ header.style.cursor = 'grab';
536
+
537
+ header.addEventListener('mousedown', (e) => {
538
+ if (e.target.closest('.cw-collapse') || e.target.closest('.cw-scan')) return;
539
+ e.preventDefault();
540
+
541
+ // If docked, undock on drag
542
+ if (_docked) _undock();
543
+
544
+ const rect = _widget.getBoundingClientRect();
545
+ _drag = {
546
+ startX: e.clientX,
547
+ startY: e.clientY,
548
+ origLeft: rect.left,
549
+ origTop: rect.top,
550
+ };
551
+ header.style.cursor = 'grabbing';
552
+
553
+ // Reset right/bottom positioning and switch to left/top
554
+ _widget.style.right = 'auto';
555
+ _widget.style.bottom = 'auto';
556
+ _widget.style.left = _drag.origLeft + 'px';
557
+ _widget.style.top = _drag.origTop + 'px';
558
+
559
+ const onMove = (ev) => {
560
+ if (!_drag) return;
561
+ const dx = ev.clientX - _drag.startX;
562
+ const dy = ev.clientY - _drag.startY;
563
+ let newLeft = _drag.origLeft + dx;
564
+ let newTop = _drag.origTop + dy;
565
+ // Constrain to viewport
566
+ const ww = _widget.offsetWidth;
567
+ const wh = _widget.offsetHeight;
568
+ newLeft = Math.max(0, Math.min(window.innerWidth - ww, newLeft));
569
+ newTop = Math.max(0, Math.min(window.innerHeight - wh, newTop));
570
+ _widget.style.left = newLeft + 'px';
571
+ _widget.style.top = newTop + 'px';
572
+ };
573
+
574
+ const onUp = () => {
575
+ document.removeEventListener('mousemove', onMove);
576
+ document.removeEventListener('mouseup', onUp);
577
+ header.style.cursor = 'grab';
578
+ if (_drag) {
579
+ _savePos(parseInt(_widget.style.left), parseInt(_widget.style.top));
580
+ _drag = null;
581
+ }
582
+ };
583
+
584
+ document.addEventListener('mousemove', onMove);
585
+ document.addEventListener('mouseup', onUp);
586
+ });
587
+ }
588
+
589
+ // ── Persistence ──
590
+
591
+ function _loadPos() {
592
+ try {
593
+ const raw = storage.getItem(KEYS.COST_WIDGET_POS);
594
+ if (!raw) return null;
595
+ const pos = JSON.parse(raw);
596
+ return (typeof pos.left === 'number' && typeof pos.top === 'number') ? pos : null;
597
+ } catch { return null; }
598
+ }
599
+
600
+ function _savePos(left, top) {
601
+ storage.setItem(KEYS.COST_WIDGET_POS, JSON.stringify({ left, top }));
602
+ }