token-guard 0.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.
@@ -0,0 +1,160 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TokenMonitor = void 0;
4
+ const node_child_process_1 = require("node:child_process");
5
+ const node_fs_1 = require("node:fs");
6
+ const tokenizer_js_1 = require("./tokenizer.js");
7
+ const pricing_js_1 = require("./pricing.js");
8
+ const display_js_1 = require("./display.js");
9
+ class TokenMonitor {
10
+ stats;
11
+ options;
12
+ process = null;
13
+ updateInterval = null;
14
+ lastStatusLength = 0;
15
+ constructor(command, options) {
16
+ const model = options.model || (0, pricing_js_1.detectModel)(command);
17
+ this.options = options;
18
+ this.stats = {
19
+ inputTokens: 0,
20
+ outputTokens: 0,
21
+ totalTokens: 0,
22
+ estimatedCost: 0,
23
+ model,
24
+ startTime: new Date(),
25
+ command,
26
+ budgetExceeded: false,
27
+ costExceeded: false,
28
+ };
29
+ }
30
+ async run(args) {
31
+ return new Promise((resolve) => {
32
+ const [cmd, ...cmdArgs] = args;
33
+ this.process = (0, node_child_process_1.spawn)(cmd, cmdArgs, {
34
+ stdio: ['inherit', 'pipe', 'pipe'],
35
+ shell: true,
36
+ });
37
+ // Track input (what we send to the process)
38
+ // For now, we count the command itself as input
39
+ this.addInputTokens(args.join(' '));
40
+ // Track stdout
41
+ this.process.stdout?.on('data', (data) => {
42
+ const text = data.toString();
43
+ this.addOutputTokens(text);
44
+ process.stdout.write(data);
45
+ this.checkLimits();
46
+ });
47
+ // Track stderr
48
+ this.process.stderr?.on('data', (data) => {
49
+ const text = data.toString();
50
+ this.addOutputTokens(text);
51
+ process.stderr.write(data);
52
+ this.checkLimits();
53
+ });
54
+ // Periodic status update
55
+ if (!this.options.quiet) {
56
+ this.updateInterval = setInterval(() => {
57
+ this.showStatus();
58
+ }, 1000);
59
+ }
60
+ this.process.on('close', (code) => {
61
+ this.cleanup();
62
+ this.stats.endTime = new Date();
63
+ if (!this.options.quiet) {
64
+ console.error((0, display_js_1.renderFinalReport)(this.stats, this.options));
65
+ }
66
+ if (this.options.output) {
67
+ this.saveReport();
68
+ }
69
+ resolve(code ?? 0);
70
+ });
71
+ this.process.on('error', (err) => {
72
+ this.cleanup();
73
+ (0, display_js_1.error)(`Failed to start command: ${err.message}`);
74
+ resolve(1);
75
+ });
76
+ });
77
+ }
78
+ addInputTokens(text) {
79
+ const tokens = (0, tokenizer_js_1.countTokens)(text);
80
+ this.stats.inputTokens += tokens;
81
+ this.updateTotals();
82
+ }
83
+ addOutputTokens(text) {
84
+ const tokens = (0, tokenizer_js_1.countTokens)(text);
85
+ this.stats.outputTokens += tokens;
86
+ this.updateTotals();
87
+ }
88
+ updateTotals() {
89
+ this.stats.totalTokens = this.stats.inputTokens + this.stats.outputTokens;
90
+ this.stats.estimatedCost = (0, pricing_js_1.calculateCost)(this.stats.inputTokens, this.stats.outputTokens, this.stats.model);
91
+ }
92
+ checkLimits() {
93
+ const { budget, costLimit, warnPercent } = this.options;
94
+ // Check token budget
95
+ if (budget) {
96
+ const percent = (this.stats.totalTokens / budget) * 100;
97
+ if (percent >= 100) {
98
+ this.stats.budgetExceeded = true;
99
+ this.terminate('Token budget exceeded');
100
+ return;
101
+ }
102
+ if (percent >= warnPercent && percent < warnPercent + 5) {
103
+ (0, display_js_1.warn)(`${percent.toFixed(0)}% of token budget used`);
104
+ }
105
+ }
106
+ // Check cost limit
107
+ if (costLimit) {
108
+ const percent = (this.stats.estimatedCost / costLimit) * 100;
109
+ if (percent >= 100) {
110
+ this.stats.costExceeded = true;
111
+ this.terminate('Cost limit exceeded');
112
+ return;
113
+ }
114
+ if (percent >= warnPercent && percent < warnPercent + 5) {
115
+ (0, display_js_1.warn)(`${percent.toFixed(0)}% of cost limit used`);
116
+ }
117
+ }
118
+ }
119
+ terminate(reason) {
120
+ (0, display_js_1.error)(reason);
121
+ if (this.process) {
122
+ this.process.kill('SIGTERM');
123
+ }
124
+ }
125
+ showStatus() {
126
+ if (this.options.quiet)
127
+ return;
128
+ const status = (0, display_js_1.renderStatus)(this.stats, this.options, true);
129
+ (0, display_js_1.clearLine)();
130
+ process.stderr.write(status);
131
+ }
132
+ cleanup() {
133
+ if (this.updateInterval) {
134
+ clearInterval(this.updateInterval);
135
+ this.updateInterval = null;
136
+ }
137
+ (0, display_js_1.clearLine)();
138
+ }
139
+ saveReport() {
140
+ if (!this.options.output)
141
+ return;
142
+ const report = {
143
+ ...this.stats,
144
+ startTime: this.stats.startTime.toISOString(),
145
+ endTime: this.stats.endTime?.toISOString(),
146
+ options: this.options,
147
+ };
148
+ try {
149
+ (0, node_fs_1.writeFileSync)(this.options.output, JSON.stringify(report, null, 2));
150
+ }
151
+ catch (err) {
152
+ (0, display_js_1.error)(`Failed to save report: ${err}`);
153
+ }
154
+ }
155
+ getStats() {
156
+ return { ...this.stats };
157
+ }
158
+ }
159
+ exports.TokenMonitor = TokenMonitor;
160
+ //# sourceMappingURL=monitor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"monitor.js","sourceRoot":"","sources":["../src/monitor.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ import type { ModelPricing } from './types.js';
2
+ export declare const MODEL_PRICING: Record<string, ModelPricing>;
3
+ export declare function getPricing(model: string): ModelPricing;
4
+ export declare function calculateCost(inputTokens: number, outputTokens: number, model: string): number;
5
+ export declare function detectModel(command: string): string;
6
+ //# sourceMappingURL=pricing.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pricing.d.ts","sourceRoot":"","sources":["../src/pricing.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAG/C,eAAO,MAAM,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAmBtD,CAAC;AAEF,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,YAAY,CAUtD;AAED,wBAAgB,aAAa,CAC3B,WAAW,EAAE,MAAM,EACnB,YAAY,EAAE,MAAM,EACpB,KAAK,EAAE,MAAM,GACZ,MAAM,CAKR;AAED,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAkBnD"}
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MODEL_PRICING = void 0;
4
+ exports.getPricing = getPricing;
5
+ exports.calculateCost = calculateCost;
6
+ exports.detectModel = detectModel;
7
+ // Pricing per 1M tokens (as of Jan 2026)
8
+ exports.MODEL_PRICING = {
9
+ // Claude models
10
+ 'claude-opus-4-5': { input: 15.00, output: 75.00 },
11
+ 'claude-opus-4': { input: 15.00, output: 75.00 },
12
+ 'claude-sonnet-4': { input: 3.00, output: 15.00 },
13
+ 'claude-3-opus': { input: 15.00, output: 75.00 },
14
+ 'claude-3-sonnet': { input: 3.00, output: 15.00 },
15
+ 'claude-3-haiku': { input: 0.25, output: 1.25 },
16
+ // OpenAI models
17
+ 'gpt-4o': { input: 2.50, output: 10.00 },
18
+ 'gpt-4-turbo': { input: 10.00, output: 30.00 },
19
+ 'gpt-4': { input: 30.00, output: 60.00 },
20
+ 'gpt-3.5-turbo': { input: 0.50, output: 1.50 },
21
+ 'o1': { input: 15.00, output: 60.00 },
22
+ 'o1-mini': { input: 3.00, output: 12.00 },
23
+ // Default fallback (sonnet-level pricing)
24
+ 'default': { input: 3.00, output: 15.00 },
25
+ };
26
+ function getPricing(model) {
27
+ const normalizedModel = model.toLowerCase().replace(/[_-]/g, '-');
28
+ for (const [key, pricing] of Object.entries(exports.MODEL_PRICING)) {
29
+ if (normalizedModel.includes(key.replace(/[_-]/g, '-'))) {
30
+ return pricing;
31
+ }
32
+ }
33
+ return exports.MODEL_PRICING['default'];
34
+ }
35
+ function calculateCost(inputTokens, outputTokens, model) {
36
+ const pricing = getPricing(model);
37
+ const inputCost = (inputTokens / 1_000_000) * pricing.input;
38
+ const outputCost = (outputTokens / 1_000_000) * pricing.output;
39
+ return inputCost + outputCost;
40
+ }
41
+ function detectModel(command) {
42
+ const lowerCommand = command.toLowerCase();
43
+ if (lowerCommand.includes('claude')) {
44
+ if (lowerCommand.includes('opus'))
45
+ return 'claude-opus-4';
46
+ if (lowerCommand.includes('haiku'))
47
+ return 'claude-3-haiku';
48
+ return 'claude-sonnet-4'; // default for claude commands
49
+ }
50
+ if (lowerCommand.includes('aider')) {
51
+ // Aider defaults vary, assume sonnet
52
+ return 'claude-sonnet-4';
53
+ }
54
+ if (lowerCommand.includes('gpt-4'))
55
+ return 'gpt-4o';
56
+ if (lowerCommand.includes('openai'))
57
+ return 'gpt-4o';
58
+ return 'default';
59
+ }
60
+ //# sourceMappingURL=pricing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pricing.js","sourceRoot":"","sources":["../src/pricing.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ export declare function countTokens(text: string): number;
2
+ export declare function formatTokens(count: number): string;
3
+ //# sourceMappingURL=tokenizer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tokenizer.d.ts","sourceRoot":"","sources":["../src/tokenizer.ts"],"names":[],"mappings":"AAKA,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAyChD;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAQlD"}
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ // Simple token estimation without external dependencies
3
+ // Uses GPT-4 style tokenization rules (~4 chars per token)
4
+ Object.defineProperty(exports, "__esModule", { value: true });
5
+ exports.countTokens = countTokens;
6
+ exports.formatTokens = formatTokens;
7
+ const CHARS_PER_TOKEN = 4;
8
+ function countTokens(text) {
9
+ if (!text)
10
+ return 0;
11
+ // More accurate estimation:
12
+ // - Split on whitespace and punctuation
13
+ // - Count special tokens for code
14
+ // Count words
15
+ const words = text.split(/\s+/).filter(w => w.length > 0);
16
+ // Estimate tokens (rough approximation)
17
+ // - Short words (1-4 chars) = 1 token
18
+ // - Medium words (5-8 chars) = 1-2 tokens
19
+ // - Long words (9+ chars) = 2+ tokens
20
+ // - Code symbols often get their own tokens
21
+ let tokens = 0;
22
+ for (const word of words) {
23
+ if (word.length <= 4) {
24
+ tokens += 1;
25
+ }
26
+ else if (word.length <= 8) {
27
+ tokens += Math.ceil(word.length / 4);
28
+ }
29
+ else {
30
+ tokens += Math.ceil(word.length / 3);
31
+ }
32
+ }
33
+ // Add tokens for punctuation and special characters
34
+ const specialChars = text.match(/[{}()\[\]<>:;,."'`~!@#$%^&*+=|\\/?-]/g);
35
+ if (specialChars) {
36
+ tokens += Math.ceil(specialChars.length * 0.5);
37
+ }
38
+ // Add tokens for newlines (often separate tokens)
39
+ const newlines = text.match(/\n/g);
40
+ if (newlines) {
41
+ tokens += newlines.length;
42
+ }
43
+ return Math.max(1, Math.round(tokens));
44
+ }
45
+ function formatTokens(count) {
46
+ if (count >= 1_000_000) {
47
+ return `${(count / 1_000_000).toFixed(2)}M`;
48
+ }
49
+ if (count >= 1_000) {
50
+ return `${(count / 1_000).toFixed(1)}k`;
51
+ }
52
+ return count.toString();
53
+ }
54
+ //# sourceMappingURL=tokenizer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tokenizer.js","sourceRoot":"","sources":["../src/tokenizer.ts"],"names":[],"mappings":";AAAA,wDAAwD;AACxD,2DAA2D;;AAI3D,kCAyCC;AAED,oCAQC;AArDD,MAAM,eAAe,GAAG,CAAC,CAAC;AAE1B,SAAgB,WAAW,CAAC,IAAY;IACtC,IAAI,CAAC,IAAI;QAAE,OAAO,CAAC,CAAC;IAEpB,4BAA4B;IAC5B,wCAAwC;IACxC,kCAAkC;IAElC,cAAc;IACd,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IAE1D,wCAAwC;IACxC,sCAAsC;IACtC,0CAA0C;IAC1C,sCAAsC;IACtC,4CAA4C;IAE5C,IAAI,MAAM,GAAG,CAAC,CAAC;IAEf,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,IAAI,CAAC,MAAM,IAAI,CAAC,EAAE,CAAC;YACrB,MAAM,IAAI,CAAC,CAAC;QACd,CAAC;aAAM,IAAI,IAAI,CAAC,MAAM,IAAI,CAAC,EAAE,CAAC;YAC5B,MAAM,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;QACvC,CAAC;aAAM,CAAC;YACN,MAAM,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;QACvC,CAAC;IACH,CAAC;IAED,oDAAoD;IACpD,MAAM,YAAY,GAAG,IAAI,CAAC,KAAK,CAAC,uCAAuC,CAAC,CAAC;IACzE,IAAI,YAAY,EAAE,CAAC;QACjB,MAAM,IAAI,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC,MAAM,GAAG,GAAG,CAAC,CAAC;IACjD,CAAC;IAED,kDAAkD;IAClD,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IACnC,IAAI,QAAQ,EAAE,CAAC;QACb,MAAM,IAAI,QAAQ,CAAC,MAAM,CAAC;IAC5B,CAAC;IAED,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC;AACzC,CAAC;AAED,SAAgB,YAAY,CAAC,KAAa;IACxC,IAAI,KAAK,IAAI,SAAS,EAAE,CAAC;QACvB,OAAO,GAAG,CAAC,KAAK,GAAG,SAAS,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC;IAC9C,CAAC;IACD,IAAI,KAAK,IAAI,KAAK,EAAE,CAAC;QACnB,OAAO,GAAG,CAAC,KAAK,GAAG,KAAK,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC;IAC1C,CAAC;IACD,OAAO,KAAK,CAAC,QAAQ,EAAE,CAAC;AAC1B,CAAC"}
@@ -0,0 +1,31 @@
1
+ export interface TokenGuardOptions {
2
+ budget?: number;
3
+ costLimit?: number;
4
+ model?: string;
5
+ warnPercent: number;
6
+ quiet: boolean;
7
+ output?: string;
8
+ }
9
+ export interface ModelPricing {
10
+ input: number;
11
+ output: number;
12
+ }
13
+ export interface UsageStats {
14
+ inputTokens: number;
15
+ outputTokens: number;
16
+ totalTokens: number;
17
+ estimatedCost: number;
18
+ model: string;
19
+ startTime: Date;
20
+ endTime?: Date;
21
+ command: string;
22
+ budgetExceeded: boolean;
23
+ costExceeded: boolean;
24
+ }
25
+ export interface Config {
26
+ defaultBudget?: number;
27
+ defaultCostLimit?: number;
28
+ warnPercent: number;
29
+ models: Record<string, ModelPricing>;
30
+ }
31
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,iBAAiB;IAChC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,UAAU;IACzB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,IAAI,CAAC;IAChB,OAAO,CAAC,EAAE,IAAI,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,EAAE,OAAO,CAAC;IACxB,YAAY,EAAE,OAAO,CAAC;CACvB;AAED,MAAM,WAAW,MAAM;IACrB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;CACtC"}
package/dist/types.js ADDED
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "token-guard",
3
+ "version": "0.1.0",
4
+ "description": "CLI tool to monitor and limit token usage for AI coding assistants",
5
+ "main": "dist/index.js",
6
+ "bin": {
7
+ "token-guard": "dist/cli.js",
8
+ "tg": "dist/cli.js"
9
+ },
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "dev": "tsc --watch",
13
+ "start": "node dist/cli.js",
14
+ "test": "node --test",
15
+ "prepublishOnly": "npm run build"
16
+ },
17
+ "keywords": [
18
+ "ai",
19
+ "llm",
20
+ "tokens",
21
+ "claude",
22
+ "openai",
23
+ "budget",
24
+ "cost",
25
+ "cli"
26
+ ],
27
+ "author": "Josh Duffy",
28
+ "license": "MIT",
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "https://github.com/joshduffy/token-guard.git"
32
+ },
33
+ "devDependencies": {
34
+ "@types/node": "^20.0.0",
35
+ "typescript": "^5.3.0"
36
+ },
37
+ "dependencies": {
38
+ "tiktoken": "^1.0.0"
39
+ },
40
+ "engines": {
41
+ "node": ">=18.0.0"
42
+ }
43
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,166 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { loadConfig } from './config.js';
4
+ import { TokenMonitor } from './monitor.js';
5
+ import type { TokenGuardOptions } from './types.js';
6
+
7
+ const VERSION = '0.1.0';
8
+
9
+ const HELP = `
10
+ token-guard v${VERSION}
11
+ Monitor and limit token usage for AI coding assistants
12
+
13
+ USAGE:
14
+ token-guard [options] -- <command>
15
+
16
+ OPTIONS:
17
+ -b, --budget <tokens> Maximum tokens allowed (default: unlimited)
18
+ -c, --cost-limit <usd> Maximum cost in USD (default: unlimited)
19
+ -m, --model <name> Model for pricing (default: auto-detect)
20
+ -w, --warn <percent> Warn at percentage of budget (default: 80)
21
+ -q, --quiet Only show warnings and errors
22
+ -o, --output <file> Save usage report to JSON file
23
+ -h, --help Show this help message
24
+ -v, --version Show version
25
+
26
+ EXAMPLES:
27
+ token-guard -- claude "refactor the auth module"
28
+ token-guard -b 50000 -- aider --message "add tests"
29
+ token-guard -c 0.50 -- claude "explain this codebase"
30
+ token-guard -b 100000 -w 50 -o report.json -- claude "review PR"
31
+
32
+ CONFIGURATION:
33
+ Create ~/.token-guard.json for persistent settings:
34
+ {
35
+ "defaultBudget": 50000,
36
+ "warnPercent": 80,
37
+ "models": {
38
+ "custom-model": { "input": 5.00, "output": 15.00 }
39
+ }
40
+ }
41
+
42
+ `;
43
+
44
+ function parseArgs(args: string[]): { options: TokenGuardOptions; command: string[] } {
45
+ const config = loadConfig();
46
+
47
+ const options: TokenGuardOptions = {
48
+ budget: config.defaultBudget,
49
+ costLimit: config.defaultCostLimit,
50
+ warnPercent: config.warnPercent,
51
+ quiet: false,
52
+ };
53
+
54
+ const command: string[] = [];
55
+ let i = 0;
56
+ let foundSeparator = false;
57
+
58
+ while (i < args.length) {
59
+ const arg = args[i];
60
+
61
+ if (foundSeparator) {
62
+ command.push(arg);
63
+ i++;
64
+ continue;
65
+ }
66
+
67
+ if (arg === '--') {
68
+ foundSeparator = true;
69
+ i++;
70
+ continue;
71
+ }
72
+
73
+ switch (arg) {
74
+ case '-h':
75
+ case '--help':
76
+ console.log(HELP);
77
+ process.exit(0);
78
+ break;
79
+
80
+ case '-v':
81
+ case '--version':
82
+ console.log(`token-guard v${VERSION}`);
83
+ process.exit(0);
84
+ break;
85
+
86
+ case '-b':
87
+ case '--budget':
88
+ options.budget = parseInt(args[++i], 10);
89
+ if (isNaN(options.budget)) {
90
+ console.error('Error: --budget requires a number');
91
+ process.exit(1);
92
+ }
93
+ break;
94
+
95
+ case '-c':
96
+ case '--cost-limit':
97
+ options.costLimit = parseFloat(args[++i]);
98
+ if (isNaN(options.costLimit)) {
99
+ console.error('Error: --cost-limit requires a number');
100
+ process.exit(1);
101
+ }
102
+ break;
103
+
104
+ case '-m':
105
+ case '--model':
106
+ options.model = args[++i];
107
+ break;
108
+
109
+ case '-w':
110
+ case '--warn':
111
+ options.warnPercent = parseInt(args[++i], 10);
112
+ if (isNaN(options.warnPercent)) {
113
+ console.error('Error: --warn requires a number');
114
+ process.exit(1);
115
+ }
116
+ break;
117
+
118
+ case '-q':
119
+ case '--quiet':
120
+ options.quiet = true;
121
+ break;
122
+
123
+ case '-o':
124
+ case '--output':
125
+ options.output = args[++i];
126
+ break;
127
+
128
+ default:
129
+ // If we hit an unknown arg before --, treat rest as command
130
+ command.push(arg);
131
+ foundSeparator = true;
132
+ break;
133
+ }
134
+
135
+ i++;
136
+ }
137
+
138
+ return { options, command };
139
+ }
140
+
141
+ async function main(): Promise<void> {
142
+ const args = process.argv.slice(2);
143
+
144
+ if (args.length === 0) {
145
+ console.log(HELP);
146
+ process.exit(0);
147
+ }
148
+
149
+ const { options, command } = parseArgs(args);
150
+
151
+ if (command.length === 0) {
152
+ console.error('Error: No command specified');
153
+ console.error('Usage: token-guard [options] -- <command>');
154
+ process.exit(1);
155
+ }
156
+
157
+ const monitor = new TokenMonitor(command.join(' '), options);
158
+ const exitCode = await monitor.run(command);
159
+
160
+ process.exit(exitCode);
161
+ }
162
+
163
+ main().catch((err) => {
164
+ console.error('Fatal error:', err);
165
+ process.exit(1);
166
+ });
package/src/config.ts ADDED
@@ -0,0 +1,44 @@
1
+ import { readFileSync, existsSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import type { Config } from './types.js';
5
+ import { MODEL_PRICING } from './pricing.js';
6
+
7
+ const CONFIG_FILENAME = '.token-guard.json';
8
+
9
+ const DEFAULT_CONFIG: Config = {
10
+ warnPercent: 80,
11
+ models: MODEL_PRICING,
12
+ };
13
+
14
+ export function loadConfig(): Config {
15
+ const configPaths = [
16
+ join(process.cwd(), CONFIG_FILENAME),
17
+ join(homedir(), CONFIG_FILENAME),
18
+ ];
19
+
20
+ for (const configPath of configPaths) {
21
+ if (existsSync(configPath)) {
22
+ try {
23
+ const content = readFileSync(configPath, 'utf-8');
24
+ const userConfig = JSON.parse(content);
25
+ return {
26
+ ...DEFAULT_CONFIG,
27
+ ...userConfig,
28
+ models: {
29
+ ...DEFAULT_CONFIG.models,
30
+ ...userConfig.models,
31
+ },
32
+ };
33
+ } catch {
34
+ // Ignore invalid config files
35
+ }
36
+ }
37
+ }
38
+
39
+ return DEFAULT_CONFIG;
40
+ }
41
+
42
+ export function getConfigPath(): string {
43
+ return join(homedir(), CONFIG_FILENAME);
44
+ }