neuro-cli 4.1.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 (175) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +510 -0
  3. package/dist/advisor/advisor.d.ts +50 -0
  4. package/dist/advisor/advisor.js +178 -0
  5. package/dist/agents/base.d.ts +62 -0
  6. package/dist/agents/base.js +215 -0
  7. package/dist/agents/orchestrator.d.ts +46 -0
  8. package/dist/agents/orchestrator.js +192 -0
  9. package/dist/agents/team.d.ts +51 -0
  10. package/dist/agents/team.js +210 -0
  11. package/dist/api/models.d.ts +23 -0
  12. package/dist/api/models.js +514 -0
  13. package/dist/api/ollama.d.ts +153 -0
  14. package/dist/api/ollama.js +751 -0
  15. package/dist/api/openrouter.d.ts +55 -0
  16. package/dist/api/openrouter.js +223 -0
  17. package/dist/commands/commands.d.ts +43 -0
  18. package/dist/commands/commands.js +308 -0
  19. package/dist/config/config.d.ts +8 -0
  20. package/dist/config/config.js +311 -0
  21. package/dist/context/compaction.d.ts +54 -0
  22. package/dist/context/compaction.js +251 -0
  23. package/dist/context/custom-agents.d.ts +107 -0
  24. package/dist/context/custom-agents.js +397 -0
  25. package/dist/context/custom-tools.d.ts +120 -0
  26. package/dist/context/custom-tools.js +564 -0
  27. package/dist/context/git-checkpoint.d.ts +52 -0
  28. package/dist/context/git-checkpoint.js +240 -0
  29. package/dist/context/neuro-md.d.ts +48 -0
  30. package/dist/context/neuro-md.js +202 -0
  31. package/dist/context/neuroignore.d.ts +102 -0
  32. package/dist/context/neuroignore.js +441 -0
  33. package/dist/context/repo-map.d.ts +38 -0
  34. package/dist/context/repo-map.js +220 -0
  35. package/dist/context/skill-standard.d.ts +262 -0
  36. package/dist/context/skill-standard.js +1156 -0
  37. package/dist/context/skill-system.d.ts +75 -0
  38. package/dist/context/skill-system.js +578 -0
  39. package/dist/context/tree-sitter.d.ts +99 -0
  40. package/dist/context/tree-sitter.js +1956 -0
  41. package/dist/core/acp.d.ts +325 -0
  42. package/dist/core/acp.js +1498 -0
  43. package/dist/core/api-server.d.ts +143 -0
  44. package/dist/core/api-server.js +550 -0
  45. package/dist/core/approval.d.ts +81 -0
  46. package/dist/core/approval.js +432 -0
  47. package/dist/core/auto-compact.d.ts +127 -0
  48. package/dist/core/auto-compact.js +436 -0
  49. package/dist/core/auto-mode.d.ts +232 -0
  50. package/dist/core/auto-mode.js +831 -0
  51. package/dist/core/background-session.d.ts +166 -0
  52. package/dist/core/background-session.js +696 -0
  53. package/dist/core/cicd.d.ts +257 -0
  54. package/dist/core/cicd.js +1443 -0
  55. package/dist/core/cloud-sync.d.ts +156 -0
  56. package/dist/core/cloud-sync.js +582 -0
  57. package/dist/core/code-review.d.ts +132 -0
  58. package/dist/core/code-review.js +1191 -0
  59. package/dist/core/completion.d.ts +49 -0
  60. package/dist/core/completion.js +384 -0
  61. package/dist/core/context.d.ts +38 -0
  62. package/dist/core/context.js +144 -0
  63. package/dist/core/diff-preview.d.ts +35 -0
  64. package/dist/core/diff-preview.js +173 -0
  65. package/dist/core/doom-loop.d.ts +51 -0
  66. package/dist/core/doom-loop.js +179 -0
  67. package/dist/core/engine.d.ts +183 -0
  68. package/dist/core/engine.js +942 -0
  69. package/dist/core/extended-thinking.d.ts +103 -0
  70. package/dist/core/extended-thinking.js +269 -0
  71. package/dist/core/fallback.d.ts +54 -0
  72. package/dist/core/fallback.js +104 -0
  73. package/dist/core/git-worktree.d.ts +139 -0
  74. package/dist/core/git-worktree.js +614 -0
  75. package/dist/core/headless.d.ts +30 -0
  76. package/dist/core/headless.js +101 -0
  77. package/dist/core/i18n.d.ts +83 -0
  78. package/dist/core/i18n.js +739 -0
  79. package/dist/core/linting.d.ts +129 -0
  80. package/dist/core/linting.js +699 -0
  81. package/dist/core/model-router.d.ts +109 -0
  82. package/dist/core/model-router.js +581 -0
  83. package/dist/core/multi-model.d.ts +243 -0
  84. package/dist/core/multi-model.js +1099 -0
  85. package/dist/core/multi-session.d.ts +144 -0
  86. package/dist/core/multi-session.js +442 -0
  87. package/dist/core/multimodal.d.ts +125 -0
  88. package/dist/core/multimodal.js +286 -0
  89. package/dist/core/observability.d.ts +93 -0
  90. package/dist/core/observability.js +737 -0
  91. package/dist/core/os-sandbox.d.ts +122 -0
  92. package/dist/core/os-sandbox.js +1193 -0
  93. package/dist/core/outcome-grading.d.ts +228 -0
  94. package/dist/core/outcome-grading.js +1123 -0
  95. package/dist/core/output-styles.d.ts +57 -0
  96. package/dist/core/output-styles.js +382 -0
  97. package/dist/core/parallel-agents.d.ts +183 -0
  98. package/dist/core/parallel-agents.js +563 -0
  99. package/dist/core/plugin-bundle.d.ts +236 -0
  100. package/dist/core/plugin-bundle.js +887 -0
  101. package/dist/core/plugin-sdk.d.ts +139 -0
  102. package/dist/core/plugin-sdk.js +273 -0
  103. package/dist/core/prompt-cache.d.ts +163 -0
  104. package/dist/core/prompt-cache.js +599 -0
  105. package/dist/core/sandbox.d.ts +127 -0
  106. package/dist/core/sandbox.js +369 -0
  107. package/dist/core/scheduled-tasks.d.ts +151 -0
  108. package/dist/core/scheduled-tasks.js +502 -0
  109. package/dist/core/security-scanner.d.ts +160 -0
  110. package/dist/core/security-scanner.js +1494 -0
  111. package/dist/core/session.d.ts +83 -0
  112. package/dist/core/session.js +269 -0
  113. package/dist/core/shell-completion.d.ts +51 -0
  114. package/dist/core/shell-completion.js +674 -0
  115. package/dist/core/smart-monitor.d.ts +146 -0
  116. package/dist/core/smart-monitor.js +1199 -0
  117. package/dist/core/spec-driven.d.ts +233 -0
  118. package/dist/core/spec-driven.js +1485 -0
  119. package/dist/core/spending-warnings.d.ts +123 -0
  120. package/dist/core/spending-warnings.js +456 -0
  121. package/dist/core/sub-agent.d.ts +298 -0
  122. package/dist/core/sub-agent.js +1023 -0
  123. package/dist/core/telemetry.d.ts +157 -0
  124. package/dist/core/telemetry.js +412 -0
  125. package/dist/core/terminal-ux.d.ts +134 -0
  126. package/dist/core/terminal-ux.js +649 -0
  127. package/dist/core/testing.d.ts +146 -0
  128. package/dist/core/testing.js +1200 -0
  129. package/dist/core/types.d.ts +439 -0
  130. package/dist/core/types.js +6 -0
  131. package/dist/core/undo-redo.d.ts +112 -0
  132. package/dist/core/undo-redo.js +290 -0
  133. package/dist/core/updater.d.ts +159 -0
  134. package/dist/core/updater.js +608 -0
  135. package/dist/core/vim-mode.d.ts +151 -0
  136. package/dist/core/vim-mode.js +771 -0
  137. package/dist/core/voice.d.ts +137 -0
  138. package/dist/core/voice.js +538 -0
  139. package/dist/core/web-dashboard.d.ts +109 -0
  140. package/dist/core/web-dashboard.js +484 -0
  141. package/dist/hooks/hooks.d.ts +74 -0
  142. package/dist/hooks/hooks.js +160 -0
  143. package/dist/hooks/llm-evaluator.d.ts +165 -0
  144. package/dist/hooks/llm-evaluator.js +560 -0
  145. package/dist/index.d.ts +3 -0
  146. package/dist/index.js +1186 -0
  147. package/dist/lsp/lsp-manager.d.ts +63 -0
  148. package/dist/lsp/lsp-manager.js +351 -0
  149. package/dist/mcp/client.d.ts +133 -0
  150. package/dist/mcp/client.js +684 -0
  151. package/dist/mcp/mcp-apps.d.ts +70 -0
  152. package/dist/mcp/mcp-apps.js +1007 -0
  153. package/dist/tools/bash.d.ts +5 -0
  154. package/dist/tools/bash.js +195 -0
  155. package/dist/tools/browser.d.ts +92 -0
  156. package/dist/tools/browser.js +1570 -0
  157. package/dist/tools/extended.d.ts +6 -0
  158. package/dist/tools/extended.js +191 -0
  159. package/dist/tools/file.d.ts +10 -0
  160. package/dist/tools/file.js +382 -0
  161. package/dist/tools/github.d.ts +389 -0
  162. package/dist/tools/github.js +759 -0
  163. package/dist/tools/index.d.ts +9 -0
  164. package/dist/tools/index.js +40 -0
  165. package/dist/tools/memory.d.ts +6 -0
  166. package/dist/tools/memory.js +197 -0
  167. package/dist/tools/registry.d.ts +29 -0
  168. package/dist/tools/registry.js +64 -0
  169. package/dist/tools/web.d.ts +6 -0
  170. package/dist/tools/web.js +150 -0
  171. package/dist/ui/renderer.d.ts +97 -0
  172. package/dist/ui/renderer.js +279 -0
  173. package/dist/ui/theme.d.ts +27 -0
  174. package/dist/ui/theme.js +106 -0
  175. package/package.json +83 -0
@@ -0,0 +1,123 @@
1
+ export interface SpendingConfig {
2
+ dailyLimit: number;
3
+ sessionLimit: number;
4
+ warnAtPercent: number[];
5
+ autoStopAtLimit: boolean;
6
+ trackByModel: boolean;
7
+ }
8
+ export interface SpendingEntry {
9
+ timestamp: number;
10
+ model: string;
11
+ inputTokens: number;
12
+ outputTokens: number;
13
+ cost: number;
14
+ sessionId: string;
15
+ }
16
+ export interface SpendingReport {
17
+ todayTotal: number;
18
+ todayByModel: Record<string, number>;
19
+ sessionTotal: number;
20
+ sessionByModel: Record<string, number>;
21
+ dailyLimit: number;
22
+ sessionLimit: number;
23
+ dailyPercentUsed: number;
24
+ sessionPercentUsed: number;
25
+ estimatedDailySpend: number;
26
+ entries: number;
27
+ }
28
+ export declare class SpendingMonitor {
29
+ private config;
30
+ private sessionEntries;
31
+ private todayEntries;
32
+ private warnedThresholds;
33
+ private dataPath;
34
+ private sessionStartTime;
35
+ constructor(config?: Partial<SpendingConfig>);
36
+ /**
37
+ * Record a spending entry. Returns whether the spend is allowed
38
+ * and optional warning / limit-reached information.
39
+ */
40
+ record(entry: Omit<SpendingEntry, 'timestamp'>): {
41
+ allowed: boolean;
42
+ warning?: string;
43
+ limitReached?: 'daily' | 'session';
44
+ };
45
+ /**
46
+ * Check whether spending is within limits without recording anything.
47
+ */
48
+ checkLimit(): {
49
+ allowed: boolean;
50
+ remaining: {
51
+ daily: number;
52
+ session: number;
53
+ };
54
+ limitReached?: 'daily' | 'session';
55
+ };
56
+ /**
57
+ * Build a full spending report.
58
+ */
59
+ getReport(): SpendingReport;
60
+ /**
61
+ * Print a formatted spending report to the terminal.
62
+ */
63
+ printReport(): void;
64
+ /**
65
+ * Reset session-level tracking (e.g. on new conversation).
66
+ */
67
+ resetSession(): void;
68
+ /**
69
+ * Reset daily tracking. Called automatically at midnight.
70
+ */
71
+ resetDaily(): void;
72
+ /**
73
+ * Whether the daily spending limit has been reached.
74
+ */
75
+ isDailyLimitReached(): boolean;
76
+ /**
77
+ * Whether the session spending limit has been reached.
78
+ */
79
+ isSessionLimitReached(): boolean;
80
+ /**
81
+ * Current spending rate in USD per hour, estimated from the current session.
82
+ */
83
+ getSpendingRate(): number;
84
+ /**
85
+ * Export the full history of today's spending entries.
86
+ */
87
+ exportHistory(): SpendingEntry[];
88
+ /**
89
+ * Sum the cost of an array of entries.
90
+ */
91
+ private sumCost;
92
+ /**
93
+ * Sum costs grouped by model name.
94
+ */
95
+ private sumByModel;
96
+ /**
97
+ * Estimate total daily spend by extrapolating the current session rate
98
+ * over the remaining portion of the day.
99
+ */
100
+ private estimateDailySpend;
101
+ /**
102
+ * Check whether a warning should be emitted for the current usage percent.
103
+ * Returns the warning message or undefined.
104
+ */
105
+ private checkWarnings;
106
+ /**
107
+ * Load today's spending data from disk.
108
+ */
109
+ private loadTodayData;
110
+ /**
111
+ * Persist today's spending data to disk.
112
+ */
113
+ private saveTodayData;
114
+ /**
115
+ * Schedule an automatic reset at the next midnight.
116
+ */
117
+ private scheduleDailyReset;
118
+ /**
119
+ * Remove spending data files older than 30 days to avoid unbounded disk use.
120
+ */
121
+ private cleanupOldFiles;
122
+ }
123
+ //# sourceMappingURL=spending-warnings.d.ts.map
@@ -0,0 +1,456 @@
1
+ // ============================================================
2
+ // NeuroCLI - Spending Warnings System
3
+ // Monitors and alerts about API spending across sessions
4
+ // ============================================================
5
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, unlinkSync } from 'fs';
6
+ import { join } from 'path';
7
+ import { homedir } from 'os';
8
+ // -----------------------------------------------------------
9
+ // Internal helpers
10
+ // -----------------------------------------------------------
11
+ const SPENDING_DIR = join(homedir(), '.neuro', 'spending');
12
+ function todayDateStr() {
13
+ const d = new Date();
14
+ const yyyy = d.getFullYear().toString();
15
+ const mm = (d.getMonth() + 1).toString().padStart(2, '0');
16
+ const dd = d.getDate().toString().padStart(2, '0');
17
+ return `${yyyy}-${mm}-${dd}`;
18
+ }
19
+ function defaultConfig() {
20
+ return {
21
+ dailyLimit: 0,
22
+ sessionLimit: 0,
23
+ warnAtPercent: [50, 75, 90],
24
+ autoStopAtLimit: true,
25
+ trackByModel: true,
26
+ };
27
+ }
28
+ // -----------------------------------------------------------
29
+ // SpendingMonitor
30
+ // -----------------------------------------------------------
31
+ export class SpendingMonitor {
32
+ config;
33
+ sessionEntries;
34
+ todayEntries;
35
+ warnedThresholds;
36
+ dataPath;
37
+ sessionStartTime;
38
+ constructor(config) {
39
+ this.config = { ...defaultConfig(), ...config };
40
+ this.sessionEntries = [];
41
+ this.todayEntries = [];
42
+ this.warnedThresholds = new Set();
43
+ this.sessionStartTime = Date.now();
44
+ // Ensure spending directory exists
45
+ if (!existsSync(SPENDING_DIR)) {
46
+ mkdirSync(SPENDING_DIR, { recursive: true });
47
+ }
48
+ this.dataPath = join(SPENDING_DIR, `${todayDateStr()}.json`);
49
+ this.loadTodayData();
50
+ // Schedule daily reset at midnight
51
+ this.scheduleDailyReset();
52
+ }
53
+ // ----------------------------------------------------------
54
+ // Public API
55
+ // ----------------------------------------------------------
56
+ /**
57
+ * Record a spending entry. Returns whether the spend is allowed
58
+ * and optional warning / limit-reached information.
59
+ */
60
+ record(entry) {
61
+ const fullEntry = {
62
+ ...entry,
63
+ timestamp: Date.now(),
64
+ };
65
+ // Check limits before recording
66
+ const limitCheck = this.checkLimit();
67
+ if (limitCheck.limitReached && this.config.autoStopAtLimit) {
68
+ return {
69
+ allowed: false,
70
+ limitReached: limitCheck.limitReached,
71
+ };
72
+ }
73
+ // Record the entry
74
+ this.sessionEntries.push(fullEntry);
75
+ this.todayEntries.push(fullEntry);
76
+ // Persist today's data
77
+ this.saveTodayData();
78
+ // Evaluate warnings after recording
79
+ const sessionTotal = this.sumCost(this.sessionEntries);
80
+ const todayTotal = this.sumCost(this.todayEntries);
81
+ let warning;
82
+ let limitReached;
83
+ // Check session limit
84
+ if (this.config.sessionLimit > 0) {
85
+ const sessionPercent = (sessionTotal / this.config.sessionLimit) * 100;
86
+ const sessionWarning = this.checkWarnings(sessionPercent, 'session');
87
+ if (sessionWarning)
88
+ warning = sessionWarning;
89
+ if (sessionTotal >= this.config.sessionLimit) {
90
+ limitReached = 'session';
91
+ }
92
+ }
93
+ // Check daily limit (daily takes precedence if both are hit)
94
+ if (this.config.dailyLimit > 0) {
95
+ const dailyPercent = (todayTotal / this.config.dailyLimit) * 100;
96
+ const dailyWarning = this.checkWarnings(dailyPercent, 'daily');
97
+ if (dailyWarning) {
98
+ warning = warning ? `${warning} | ${dailyWarning}` : dailyWarning;
99
+ }
100
+ if (todayTotal >= this.config.dailyLimit) {
101
+ limitReached = 'daily';
102
+ }
103
+ }
104
+ return {
105
+ allowed: !limitReached || !this.config.autoStopAtLimit,
106
+ warning,
107
+ limitReached,
108
+ };
109
+ }
110
+ /**
111
+ * Check whether spending is within limits without recording anything.
112
+ */
113
+ checkLimit() {
114
+ const sessionTotal = this.sumCost(this.sessionEntries);
115
+ const todayTotal = this.sumCost(this.todayEntries);
116
+ let limitReached;
117
+ const sessionRemaining = this.config.sessionLimit > 0
118
+ ? Math.max(0, this.config.sessionLimit - sessionTotal)
119
+ : Infinity;
120
+ const dailyRemaining = this.config.dailyLimit > 0
121
+ ? Math.max(0, this.config.dailyLimit - todayTotal)
122
+ : Infinity;
123
+ if (this.config.sessionLimit > 0 && sessionTotal >= this.config.sessionLimit) {
124
+ limitReached = 'session';
125
+ }
126
+ if (this.config.dailyLimit > 0 && todayTotal >= this.config.dailyLimit) {
127
+ limitReached = 'daily'; // daily takes precedence
128
+ }
129
+ return {
130
+ allowed: !limitReached || !this.config.autoStopAtLimit,
131
+ remaining: { daily: dailyRemaining, session: sessionRemaining },
132
+ limitReached,
133
+ };
134
+ }
135
+ /**
136
+ * Build a full spending report.
137
+ */
138
+ getReport() {
139
+ const todayTotal = this.sumCost(this.todayEntries);
140
+ const sessionTotal = this.sumCost(this.sessionEntries);
141
+ const todayByModel = this.sumByModel(this.todayEntries);
142
+ const sessionByModel = this.sumByModel(this.sessionEntries);
143
+ const dailyPercentUsed = this.config.dailyLimit > 0 ? (todayTotal / this.config.dailyLimit) * 100 : 0;
144
+ const sessionPercentUsed = this.config.sessionLimit > 0 ? (sessionTotal / this.config.sessionLimit) * 100 : 0;
145
+ return {
146
+ todayTotal,
147
+ todayByModel,
148
+ sessionTotal,
149
+ sessionByModel,
150
+ dailyLimit: this.config.dailyLimit,
151
+ sessionLimit: this.config.sessionLimit,
152
+ dailyPercentUsed,
153
+ sessionPercentUsed,
154
+ estimatedDailySpend: this.estimateDailySpend(),
155
+ entries: this.todayEntries.length,
156
+ };
157
+ }
158
+ /**
159
+ * Print a formatted spending report to the terminal.
160
+ */
161
+ printReport() {
162
+ const report = this.getReport();
163
+ const bold = '\x1b[1m';
164
+ const reset = '\x1b[0m';
165
+ const yellow = '\x1b[33m';
166
+ const red = '\x1b[31m';
167
+ const green = '\x1b[32m';
168
+ const cyan = '\x1b[36m';
169
+ const dim = '\x1b[2m';
170
+ console.log('');
171
+ console.log(`${bold}--- NeuroCLI Spending Report ---${reset}`);
172
+ console.log('');
173
+ // Today's total
174
+ const todayColor = report.dailyPercentUsed >= 90 ? red : report.dailyPercentUsed >= 75 ? yellow : green;
175
+ console.log(` Today's Total: ${todayColor}$${report.todayTotal.toFixed(4)}${reset}`);
176
+ if (report.dailyLimit > 0) {
177
+ console.log(` Daily Limit: $${report.dailyLimit.toFixed(2)} (${todayColor}${report.dailyPercentUsed.toFixed(1)}%${reset} used)`);
178
+ const dailyRemaining = Math.max(0, report.dailyLimit - report.todayTotal);
179
+ console.log(` Daily Remaining: $${dailyRemaining.toFixed(4)}`);
180
+ }
181
+ else {
182
+ console.log(` Daily Limit: ${dim}unlimited${reset}`);
183
+ }
184
+ console.log('');
185
+ // Session total
186
+ const sessionColor = report.sessionPercentUsed >= 90 ? red : report.sessionPercentUsed >= 75 ? yellow : green;
187
+ console.log(` Session Total: ${sessionColor}$${report.sessionTotal.toFixed(4)}${reset}`);
188
+ if (report.sessionLimit > 0) {
189
+ console.log(` Session Limit: $${report.sessionLimit.toFixed(2)} (${sessionColor}${report.sessionPercentUsed.toFixed(1)}%${reset} used)`);
190
+ const sessionRemaining = Math.max(0, report.sessionLimit - report.sessionTotal);
191
+ console.log(` Session Remaining: $${sessionRemaining.toFixed(4)}`);
192
+ }
193
+ else {
194
+ console.log(` Session Limit: ${dim}unlimited${reset}`);
195
+ }
196
+ console.log('');
197
+ // Estimated daily spend
198
+ if (report.estimatedDailySpend > 0) {
199
+ const estColor = report.dailyLimit > 0 && report.estimatedDailySpend > report.dailyLimit ? red : cyan;
200
+ console.log(` Est. Daily Spend: ${estColor}$${report.estimatedDailySpend.toFixed(4)}${reset}`);
201
+ }
202
+ console.log(` Total Entries: ${report.entries}`);
203
+ console.log('');
204
+ // Model breakdown
205
+ if (this.config.trackByModel) {
206
+ console.log(`${bold} Breakdown by Model:${reset}`);
207
+ console.log('');
208
+ const allModels = new Set([
209
+ ...Object.keys(report.todayByModel),
210
+ ...Object.keys(report.sessionByModel),
211
+ ]);
212
+ for (const model of allModels) {
213
+ const todayCost = report.todayByModel[model] ?? 0;
214
+ const sessionCost = report.sessionByModel[model] ?? 0;
215
+ console.log(` ${cyan}${model}${reset}`);
216
+ console.log(` Today: $${todayCost.toFixed(4)}`);
217
+ console.log(` Session: $${sessionCost.toFixed(4)}`);
218
+ }
219
+ if (allModels.size === 0) {
220
+ console.log(` ${dim}No spending recorded yet${reset}`);
221
+ }
222
+ console.log('');
223
+ }
224
+ console.log(`${bold}--------------------------------${reset}`);
225
+ console.log('');
226
+ }
227
+ /**
228
+ * Reset session-level tracking (e.g. on new conversation).
229
+ */
230
+ resetSession() {
231
+ this.sessionEntries = [];
232
+ this.warnedThresholds = new Set();
233
+ this.sessionStartTime = Date.now();
234
+ }
235
+ /**
236
+ * Reset daily tracking. Called automatically at midnight.
237
+ */
238
+ resetDaily() {
239
+ this.todayEntries = [];
240
+ this.warnedThresholds = new Set();
241
+ this.dataPath = join(SPENDING_DIR, `${todayDateStr()}.json`);
242
+ }
243
+ /**
244
+ * Whether the daily spending limit has been reached.
245
+ */
246
+ isDailyLimitReached() {
247
+ if (this.config.dailyLimit <= 0)
248
+ return false;
249
+ return this.sumCost(this.todayEntries) >= this.config.dailyLimit;
250
+ }
251
+ /**
252
+ * Whether the session spending limit has been reached.
253
+ */
254
+ isSessionLimitReached() {
255
+ if (this.config.sessionLimit <= 0)
256
+ return false;
257
+ return this.sumCost(this.sessionEntries) >= this.config.sessionLimit;
258
+ }
259
+ /**
260
+ * Current spending rate in USD per hour, estimated from the current session.
261
+ */
262
+ getSpendingRate() {
263
+ if (this.sessionEntries.length === 0)
264
+ return 0;
265
+ const elapsed = Date.now() - this.sessionStartTime;
266
+ if (elapsed <= 0)
267
+ return 0;
268
+ const sessionTotal = this.sumCost(this.sessionEntries);
269
+ const hoursElapsed = elapsed / (1000 * 60 * 60);
270
+ return hoursElapsed > 0 ? sessionTotal / hoursElapsed : 0;
271
+ }
272
+ /**
273
+ * Export the full history of today's spending entries.
274
+ */
275
+ exportHistory() {
276
+ return [...this.todayEntries];
277
+ }
278
+ // ----------------------------------------------------------
279
+ // Private helpers
280
+ // ----------------------------------------------------------
281
+ /**
282
+ * Sum the cost of an array of entries.
283
+ */
284
+ sumCost(entries) {
285
+ return entries.reduce((acc, e) => acc + e.cost, 0);
286
+ }
287
+ /**
288
+ * Sum costs grouped by model name.
289
+ */
290
+ sumByModel(entries) {
291
+ if (!this.config.trackByModel)
292
+ return {};
293
+ const result = {};
294
+ for (const entry of entries) {
295
+ result[entry.model] = (result[entry.model] ?? 0) + entry.cost;
296
+ }
297
+ return result;
298
+ }
299
+ /**
300
+ * Estimate total daily spend by extrapolating the current session rate
301
+ * over the remaining portion of the day.
302
+ */
303
+ estimateDailySpend() {
304
+ const todayTotal = this.sumCost(this.todayEntries);
305
+ if (this.sessionEntries.length === 0)
306
+ return todayTotal;
307
+ const rate = this.getSpendingRate();
308
+ if (rate <= 0)
309
+ return todayTotal;
310
+ // Calculate remaining hours until midnight
311
+ const now = new Date();
312
+ const midnight = new Date(now);
313
+ midnight.setHours(24, 0, 0, 0);
314
+ const remainingMs = midnight.getTime() - now.getTime();
315
+ const remainingHours = remainingMs / (1000 * 60 * 60);
316
+ const estimatedRemaining = rate * remainingHours;
317
+ return todayTotal + estimatedRemaining;
318
+ }
319
+ /**
320
+ * Check whether a warning should be emitted for the current usage percent.
321
+ * Returns the warning message or undefined.
322
+ */
323
+ checkWarnings(currentPercent, limitType) {
324
+ const sorted = [...this.config.warnAtPercent].sort((a, b) => a - b);
325
+ let matchedThreshold;
326
+ for (const threshold of sorted) {
327
+ if (currentPercent >= threshold && !this.warnedThresholds.has(threshold)) {
328
+ matchedThreshold = threshold;
329
+ // Keep checking higher thresholds; we want the highest one that applies
330
+ }
331
+ }
332
+ // Actually find the highest threshold we've crossed but not yet warned about
333
+ let highestNew;
334
+ for (const threshold of sorted) {
335
+ if (currentPercent >= threshold && !this.warnedThresholds.has(threshold)) {
336
+ highestNew = threshold;
337
+ }
338
+ }
339
+ if (highestNew === undefined)
340
+ return undefined;
341
+ // Mark all crossed thresholds as warned (including ones below the highest)
342
+ for (const threshold of sorted) {
343
+ if (currentPercent >= threshold) {
344
+ this.warnedThresholds.add(threshold);
345
+ }
346
+ }
347
+ const typeLabel = limitType === 'daily' ? 'Daily' : 'Session';
348
+ const bold = '\x1b[1m';
349
+ const reset = '\x1b[0m';
350
+ const yellow = '\x1b[33m';
351
+ const red = '\x1b[31m';
352
+ const color = highestNew >= 90 ? red : yellow;
353
+ const message = `${color}${bold}[SPENDING WARNING]${reset} ${typeLabel} spending has reached ${highestNew}% of the limit.`;
354
+ // Output the warning to terminal
355
+ console.warn(message);
356
+ if (highestNew >= 90) {
357
+ console.warn(`${red}Approaching spending limit. Consider wrapping up or the session will auto-stop.${reset}`);
358
+ }
359
+ return message;
360
+ }
361
+ /**
362
+ * Load today's spending data from disk.
363
+ */
364
+ loadTodayData() {
365
+ if (existsSync(this.dataPath)) {
366
+ try {
367
+ const raw = readFileSync(this.dataPath, 'utf-8');
368
+ const parsed = JSON.parse(raw);
369
+ if (Array.isArray(parsed)) {
370
+ this.todayEntries = parsed;
371
+ }
372
+ else {
373
+ this.todayEntries = [];
374
+ }
375
+ }
376
+ catch {
377
+ this.todayEntries = [];
378
+ }
379
+ }
380
+ else {
381
+ this.todayEntries = [];
382
+ }
383
+ }
384
+ /**
385
+ * Persist today's spending data to disk.
386
+ */
387
+ saveTodayData() {
388
+ try {
389
+ if (!existsSync(SPENDING_DIR)) {
390
+ mkdirSync(SPENDING_DIR, { recursive: true });
391
+ }
392
+ writeFileSync(this.dataPath, JSON.stringify(this.todayEntries, null, 2), 'utf-8');
393
+ }
394
+ catch {
395
+ // Silently fail - spending tracking should not block the CLI
396
+ }
397
+ }
398
+ /**
399
+ * Schedule an automatic reset at the next midnight.
400
+ */
401
+ scheduleDailyReset() {
402
+ const now = new Date();
403
+ const midnight = new Date(now);
404
+ midnight.setHours(24, 0, 0, 0);
405
+ const msUntilMidnight = midnight.getTime() - now.getTime();
406
+ setTimeout(() => {
407
+ this.resetDaily();
408
+ this.cleanupOldFiles();
409
+ // Reschedule for the next day
410
+ this.scheduleDailyReset();
411
+ }, msUntilMidnight);
412
+ }
413
+ /**
414
+ * Remove spending data files older than 30 days to avoid unbounded disk use.
415
+ */
416
+ cleanupOldFiles() {
417
+ try {
418
+ if (!existsSync(SPENDING_DIR))
419
+ return;
420
+ const files = readdirSync(SPENDING_DIR);
421
+ const thirtyDaysAgo = Date.now() - 30 * 24 * 60 * 60 * 1000;
422
+ for (const file of files) {
423
+ if (!file.endsWith('.json'))
424
+ continue;
425
+ const filePath = join(SPENDING_DIR, file);
426
+ try {
427
+ const raw = readFileSync(filePath, 'utf-8');
428
+ const entries = JSON.parse(raw);
429
+ if (Array.isArray(entries) && entries.length > 0) {
430
+ // Check the first entry's timestamp
431
+ if (entries[0].timestamp < thirtyDaysAgo) {
432
+ unlinkSync(filePath);
433
+ }
434
+ }
435
+ else {
436
+ // Empty or malformed file - check filename date
437
+ const dateMatch = file.match(/^(\d{4})-(\d{2})-(\d{2})\.json$/);
438
+ if (dateMatch) {
439
+ const fileDate = new Date(parseInt(dateMatch[1]), parseInt(dateMatch[2]) - 1, parseInt(dateMatch[3]));
440
+ if (fileDate.getTime() < thirtyDaysAgo) {
441
+ unlinkSync(filePath);
442
+ }
443
+ }
444
+ }
445
+ }
446
+ catch {
447
+ // Skip files we can't parse
448
+ }
449
+ }
450
+ }
451
+ catch {
452
+ // Silently fail - cleanup should not block the CLI
453
+ }
454
+ }
455
+ }
456
+ //# sourceMappingURL=spending-warnings.js.map