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.
- package/LICENSE +21 -0
- package/README.md +510 -0
- package/dist/advisor/advisor.d.ts +50 -0
- package/dist/advisor/advisor.js +178 -0
- package/dist/agents/base.d.ts +62 -0
- package/dist/agents/base.js +215 -0
- package/dist/agents/orchestrator.d.ts +46 -0
- package/dist/agents/orchestrator.js +192 -0
- package/dist/agents/team.d.ts +51 -0
- package/dist/agents/team.js +210 -0
- package/dist/api/models.d.ts +23 -0
- package/dist/api/models.js +514 -0
- package/dist/api/ollama.d.ts +153 -0
- package/dist/api/ollama.js +751 -0
- package/dist/api/openrouter.d.ts +55 -0
- package/dist/api/openrouter.js +223 -0
- package/dist/commands/commands.d.ts +43 -0
- package/dist/commands/commands.js +308 -0
- package/dist/config/config.d.ts +8 -0
- package/dist/config/config.js +311 -0
- package/dist/context/compaction.d.ts +54 -0
- package/dist/context/compaction.js +251 -0
- package/dist/context/custom-agents.d.ts +107 -0
- package/dist/context/custom-agents.js +397 -0
- package/dist/context/custom-tools.d.ts +120 -0
- package/dist/context/custom-tools.js +564 -0
- package/dist/context/git-checkpoint.d.ts +52 -0
- package/dist/context/git-checkpoint.js +240 -0
- package/dist/context/neuro-md.d.ts +48 -0
- package/dist/context/neuro-md.js +202 -0
- package/dist/context/neuroignore.d.ts +102 -0
- package/dist/context/neuroignore.js +441 -0
- package/dist/context/repo-map.d.ts +38 -0
- package/dist/context/repo-map.js +220 -0
- package/dist/context/skill-standard.d.ts +262 -0
- package/dist/context/skill-standard.js +1156 -0
- package/dist/context/skill-system.d.ts +75 -0
- package/dist/context/skill-system.js +578 -0
- package/dist/context/tree-sitter.d.ts +99 -0
- package/dist/context/tree-sitter.js +1956 -0
- package/dist/core/acp.d.ts +325 -0
- package/dist/core/acp.js +1498 -0
- package/dist/core/api-server.d.ts +143 -0
- package/dist/core/api-server.js +550 -0
- package/dist/core/approval.d.ts +81 -0
- package/dist/core/approval.js +432 -0
- package/dist/core/auto-compact.d.ts +127 -0
- package/dist/core/auto-compact.js +436 -0
- package/dist/core/auto-mode.d.ts +232 -0
- package/dist/core/auto-mode.js +831 -0
- package/dist/core/background-session.d.ts +166 -0
- package/dist/core/background-session.js +696 -0
- package/dist/core/cicd.d.ts +257 -0
- package/dist/core/cicd.js +1443 -0
- package/dist/core/cloud-sync.d.ts +156 -0
- package/dist/core/cloud-sync.js +582 -0
- package/dist/core/code-review.d.ts +132 -0
- package/dist/core/code-review.js +1191 -0
- package/dist/core/completion.d.ts +49 -0
- package/dist/core/completion.js +384 -0
- package/dist/core/context.d.ts +38 -0
- package/dist/core/context.js +144 -0
- package/dist/core/diff-preview.d.ts +35 -0
- package/dist/core/diff-preview.js +173 -0
- package/dist/core/doom-loop.d.ts +51 -0
- package/dist/core/doom-loop.js +179 -0
- package/dist/core/engine.d.ts +183 -0
- package/dist/core/engine.js +942 -0
- package/dist/core/extended-thinking.d.ts +103 -0
- package/dist/core/extended-thinking.js +269 -0
- package/dist/core/fallback.d.ts +54 -0
- package/dist/core/fallback.js +104 -0
- package/dist/core/git-worktree.d.ts +139 -0
- package/dist/core/git-worktree.js +614 -0
- package/dist/core/headless.d.ts +30 -0
- package/dist/core/headless.js +101 -0
- package/dist/core/i18n.d.ts +83 -0
- package/dist/core/i18n.js +739 -0
- package/dist/core/linting.d.ts +129 -0
- package/dist/core/linting.js +699 -0
- package/dist/core/model-router.d.ts +109 -0
- package/dist/core/model-router.js +581 -0
- package/dist/core/multi-model.d.ts +243 -0
- package/dist/core/multi-model.js +1099 -0
- package/dist/core/multi-session.d.ts +144 -0
- package/dist/core/multi-session.js +442 -0
- package/dist/core/multimodal.d.ts +125 -0
- package/dist/core/multimodal.js +286 -0
- package/dist/core/observability.d.ts +93 -0
- package/dist/core/observability.js +737 -0
- package/dist/core/os-sandbox.d.ts +122 -0
- package/dist/core/os-sandbox.js +1193 -0
- package/dist/core/outcome-grading.d.ts +228 -0
- package/dist/core/outcome-grading.js +1123 -0
- package/dist/core/output-styles.d.ts +57 -0
- package/dist/core/output-styles.js +382 -0
- package/dist/core/parallel-agents.d.ts +183 -0
- package/dist/core/parallel-agents.js +563 -0
- package/dist/core/plugin-bundle.d.ts +236 -0
- package/dist/core/plugin-bundle.js +887 -0
- package/dist/core/plugin-sdk.d.ts +139 -0
- package/dist/core/plugin-sdk.js +273 -0
- package/dist/core/prompt-cache.d.ts +163 -0
- package/dist/core/prompt-cache.js +599 -0
- package/dist/core/sandbox.d.ts +127 -0
- package/dist/core/sandbox.js +369 -0
- package/dist/core/scheduled-tasks.d.ts +151 -0
- package/dist/core/scheduled-tasks.js +502 -0
- package/dist/core/security-scanner.d.ts +160 -0
- package/dist/core/security-scanner.js +1494 -0
- package/dist/core/session.d.ts +83 -0
- package/dist/core/session.js +269 -0
- package/dist/core/shell-completion.d.ts +51 -0
- package/dist/core/shell-completion.js +674 -0
- package/dist/core/smart-monitor.d.ts +146 -0
- package/dist/core/smart-monitor.js +1199 -0
- package/dist/core/spec-driven.d.ts +233 -0
- package/dist/core/spec-driven.js +1485 -0
- package/dist/core/spending-warnings.d.ts +123 -0
- package/dist/core/spending-warnings.js +456 -0
- package/dist/core/sub-agent.d.ts +298 -0
- package/dist/core/sub-agent.js +1023 -0
- package/dist/core/telemetry.d.ts +157 -0
- package/dist/core/telemetry.js +412 -0
- package/dist/core/terminal-ux.d.ts +134 -0
- package/dist/core/terminal-ux.js +649 -0
- package/dist/core/testing.d.ts +146 -0
- package/dist/core/testing.js +1200 -0
- package/dist/core/types.d.ts +439 -0
- package/dist/core/types.js +6 -0
- package/dist/core/undo-redo.d.ts +112 -0
- package/dist/core/undo-redo.js +290 -0
- package/dist/core/updater.d.ts +159 -0
- package/dist/core/updater.js +608 -0
- package/dist/core/vim-mode.d.ts +151 -0
- package/dist/core/vim-mode.js +771 -0
- package/dist/core/voice.d.ts +137 -0
- package/dist/core/voice.js +538 -0
- package/dist/core/web-dashboard.d.ts +109 -0
- package/dist/core/web-dashboard.js +484 -0
- package/dist/hooks/hooks.d.ts +74 -0
- package/dist/hooks/hooks.js +160 -0
- package/dist/hooks/llm-evaluator.d.ts +165 -0
- package/dist/hooks/llm-evaluator.js +560 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1186 -0
- package/dist/lsp/lsp-manager.d.ts +63 -0
- package/dist/lsp/lsp-manager.js +351 -0
- package/dist/mcp/client.d.ts +133 -0
- package/dist/mcp/client.js +684 -0
- package/dist/mcp/mcp-apps.d.ts +70 -0
- package/dist/mcp/mcp-apps.js +1007 -0
- package/dist/tools/bash.d.ts +5 -0
- package/dist/tools/bash.js +195 -0
- package/dist/tools/browser.d.ts +92 -0
- package/dist/tools/browser.js +1570 -0
- package/dist/tools/extended.d.ts +6 -0
- package/dist/tools/extended.js +191 -0
- package/dist/tools/file.d.ts +10 -0
- package/dist/tools/file.js +382 -0
- package/dist/tools/github.d.ts +389 -0
- package/dist/tools/github.js +759 -0
- package/dist/tools/index.d.ts +9 -0
- package/dist/tools/index.js +40 -0
- package/dist/tools/memory.d.ts +6 -0
- package/dist/tools/memory.js +197 -0
- package/dist/tools/registry.d.ts +29 -0
- package/dist/tools/registry.js +64 -0
- package/dist/tools/web.d.ts +6 -0
- package/dist/tools/web.js +150 -0
- package/dist/ui/renderer.d.ts +97 -0
- package/dist/ui/renderer.js +279 -0
- package/dist/ui/theme.d.ts +27 -0
- package/dist/ui/theme.js +106 -0
- 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
|