ccusage-collector 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.
- package/.env.example +9 -0
- package/.turbo/turbo-lint.log +7 -0
- package/LICENSE +21 -0
- package/README.md +242 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +76 -0
- package/dist/cli.js.map +1 -0
- package/dist/collector.d.ts +9 -0
- package/dist/collector.d.ts.map +1 -0
- package/dist/collector.js +102 -0
- package/dist/collector.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +46 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/eslint.config.js +13 -0
- package/package.json +56 -0
- package/src/cli.ts +88 -0
- package/src/collector.ts +119 -0
- package/src/index.ts +7 -0
- package/src/types.ts +48 -0
- package/tsconfig.json +18 -0
package/.env.example
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Example usage:
|
|
2
|
+
# npx ccusage-collector --api-key=your-key --endpoint=https://example.com/api/usage-sync
|
|
3
|
+
# npx ccusage-collector --api-key=your-key --endpoint=https://example.com/api/usage-sync --schedule="0 */4 * * *"
|
|
4
|
+
|
|
5
|
+
# API endpoint for syncing usage data
|
|
6
|
+
API_ENDPOINT=https://example.com/api/usage-sync
|
|
7
|
+
|
|
8
|
+
# API key for authentication
|
|
9
|
+
API_KEY=your-api-key-here
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Richard Wang
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
# ccusage-collector
|
|
2
|
+
|
|
3
|
+
A command-line tool for collecting Claude Code usage statistics and syncing them to your self-hosted My Claude Code Usage Dashboard.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
This package automatically collects your local Claude Code usage data and syncs it to your dashboard, giving you insights into your AI-assisted development workflow.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install -g ccusage-collector
|
|
13
|
+
npm install -g pm2
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Prerequisites
|
|
17
|
+
|
|
18
|
+
- Node.js ≥20
|
|
19
|
+
- PM2 process manager (for reliable background execution)
|
|
20
|
+
- Claude Code CLI installed and configured
|
|
21
|
+
- Access to a deployed My Claude Code Usage Dashboard
|
|
22
|
+
|
|
23
|
+
## Quick Start
|
|
24
|
+
|
|
25
|
+
### 1. Basic sync (run once)
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
ccusage-collector --api-key=your-api-key --endpoint=https://example.com/api/usage-sync
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### 2. Scheduled sync (recommended - runs in background)
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pm2 start ccusage-collector -- --api-key=your-api-key --endpoint=https://example.com/api/usage-sync --schedule="0 */4 * * *"
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### 3. Test data collection (dry run)
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
ccusage-collector --api-key=test --endpoint=https://example.com/api/usage-sync --dry-run
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## CLI Options
|
|
44
|
+
|
|
45
|
+
| Option | Description | Required | Default |
|
|
46
|
+
|--------|-------------|----------|---------|
|
|
47
|
+
| `-k, --api-key <key>` | API key for authentication | Yes | - |
|
|
48
|
+
| `-e, --endpoint <url>` | API endpoint URL | Yes | - |
|
|
49
|
+
| `-s, --schedule <cron>` | Cron schedule for periodic sync | No | - |
|
|
50
|
+
| `-r, --max-retries <number>` | Maximum retry attempts | No | 3 |
|
|
51
|
+
| `-d, --retry-delay <ms>` | Delay between retries (ms) | No | 1000 |
|
|
52
|
+
| `--dry-run` | Collect data but don't sync | No | false |
|
|
53
|
+
| `-h, --help` | Show help | No | - |
|
|
54
|
+
| `-V, --version` | Show version | No | - |
|
|
55
|
+
|
|
56
|
+
## Usage Examples
|
|
57
|
+
|
|
58
|
+
### One-time sync
|
|
59
|
+
```bash
|
|
60
|
+
ccusage-collector --api-key=sk-1234567890abcdef --endpoint=https://myccusage.example.com/api/usage-sync
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Scheduled sync (every 6 hours) - Background execution
|
|
64
|
+
```bash
|
|
65
|
+
pm2 start ccusage-collector -- \
|
|
66
|
+
--api-key=sk-1234567890abcdef \
|
|
67
|
+
--endpoint=https://myccusage.example.com/api/usage-sync \
|
|
68
|
+
--schedule="0 */6 * * *"
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Test without syncing
|
|
72
|
+
```bash
|
|
73
|
+
ccusage-collector \
|
|
74
|
+
--api-key=test \
|
|
75
|
+
--endpoint=https://myccusage.example.com/api/usage-sync \
|
|
76
|
+
--dry-run
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Cron Schedule Examples
|
|
80
|
+
|
|
81
|
+
| Schedule | Description |
|
|
82
|
+
|----------|-------------|
|
|
83
|
+
| `"0 */4 * * *"` | Every 4 hours |
|
|
84
|
+
| `"*/10 * * * *"` | Every 10 minutes |
|
|
85
|
+
| `"0 0 * * *"` | Daily at midnight |
|
|
86
|
+
| `"0 */1 * * *"` | Every hour |
|
|
87
|
+
| `"0 9,17 * * 1-5"` | 9 AM and 5 PM on weekdays |
|
|
88
|
+
|
|
89
|
+
## Setup Guide
|
|
90
|
+
|
|
91
|
+
### 1. Deploy the Dashboard
|
|
92
|
+
First, deploy your own instance of My Claude Code Usage Dashboard:
|
|
93
|
+
- Clone the repository
|
|
94
|
+
- Set up PostgreSQL database
|
|
95
|
+
- Configure environment variables
|
|
96
|
+
- Deploy to your preferred platform
|
|
97
|
+
|
|
98
|
+
### 2. Get API Key
|
|
99
|
+
Set the `API_KEY` environment variable in your dashboard deployment:
|
|
100
|
+
```env
|
|
101
|
+
API_KEY=your-secret-api-key-here
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### 3. Install and Run Collector
|
|
105
|
+
```bash
|
|
106
|
+
npm install -g ccusage-collector
|
|
107
|
+
npm install -g pm2
|
|
108
|
+
|
|
109
|
+
# Start background sync
|
|
110
|
+
pm2 start ccusage-collector -- --api-key=your-secret-api-key-here --endpoint=https://your-domain.com/api/usage-sync --schedule="0 */4 * * *"
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Data Collection
|
|
114
|
+
|
|
115
|
+
The collector:
|
|
116
|
+
- Uses `npx ccusage daily --json` to gather usage statistics
|
|
117
|
+
- Collects historical data (not just recent usage)
|
|
118
|
+
- Syncs complete usage records to your dashboard
|
|
119
|
+
- Supports upsert operations (updates existing records)
|
|
120
|
+
|
|
121
|
+
### Data Format
|
|
122
|
+
The collector syncs:
|
|
123
|
+
- Daily usage metrics (tokens, costs, models)
|
|
124
|
+
- Token breakdowns (input, output, cache creation/read)
|
|
125
|
+
- Model usage statistics
|
|
126
|
+
- Raw usage data for detailed analysis
|
|
127
|
+
|
|
128
|
+
## Error Handling
|
|
129
|
+
|
|
130
|
+
- **Network failures**: Automatic retry with exponential backoff
|
|
131
|
+
- **Authentication errors**: Immediate failure (no retry on 401/403)
|
|
132
|
+
- **Individual record failures**: Continues processing other records
|
|
133
|
+
- **Detailed logging**: Clear error messages and debugging info
|
|
134
|
+
|
|
135
|
+
## Process Management with PM2
|
|
136
|
+
|
|
137
|
+
For reliable background execution, use PM2 to manage the collector process:
|
|
138
|
+
|
|
139
|
+
### Start Background Sync
|
|
140
|
+
```bash
|
|
141
|
+
pm2 start ccusage-collector -- --api-key=your-key --endpoint=https://your-domain.com/api/usage-sync --schedule="0 */4 * * *"
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Check Status
|
|
145
|
+
```bash
|
|
146
|
+
pm2 list # Show all processes
|
|
147
|
+
pm2 show ccusage-collector # Show detailed info
|
|
148
|
+
pm2 logs ccusage-collector # View logs
|
|
149
|
+
pm2 monit # Real-time monitoring
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Control Process
|
|
153
|
+
```bash
|
|
154
|
+
pm2 stop ccusage-collector # Stop process
|
|
155
|
+
pm2 restart ccusage-collector # Restart process
|
|
156
|
+
pm2 delete ccusage-collector # Remove process
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Auto-start on Boot
|
|
160
|
+
```bash
|
|
161
|
+
pm2 startup # Generate startup script
|
|
162
|
+
pm2 save # Save current process list
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Benefits of PM2
|
|
166
|
+
- **Automatic restart** if process crashes
|
|
167
|
+
- **Background execution** - no need to keep terminal open
|
|
168
|
+
- **Process monitoring** and logging
|
|
169
|
+
- **Prevents duplicate instances** - safe to run multiple times
|
|
170
|
+
- **Cross-platform** - works on Windows, Linux, and macOS
|
|
171
|
+
|
|
172
|
+
## Troubleshooting
|
|
173
|
+
|
|
174
|
+
### Common Issues
|
|
175
|
+
|
|
176
|
+
**"ccusage command not found"**
|
|
177
|
+
- Ensure Claude Code CLI is installed and in PATH
|
|
178
|
+
- Run `npx ccusage --help` to verify installation
|
|
179
|
+
|
|
180
|
+
**"Authentication failed"**
|
|
181
|
+
- Verify API key matches your dashboard configuration
|
|
182
|
+
- Check that API key is correctly set in dashboard environment
|
|
183
|
+
|
|
184
|
+
**"Connection refused"**
|
|
185
|
+
- Verify endpoint URL is correct and accessible
|
|
186
|
+
- Check that your dashboard is running and deployed
|
|
187
|
+
|
|
188
|
+
**"Invalid cron expression"**
|
|
189
|
+
- Use online cron validators to test expressions
|
|
190
|
+
- Ensure cron format is correct (5 fields: minute hour day month weekday)
|
|
191
|
+
|
|
192
|
+
### Debug Mode
|
|
193
|
+
```bash
|
|
194
|
+
# Enable verbose logging
|
|
195
|
+
DEBUG=ccusage-collector ccusage-collector --api-key=... --endpoint=...
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## Contributing
|
|
199
|
+
|
|
200
|
+
1. Fork the repository
|
|
201
|
+
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
|
|
202
|
+
3. Commit your changes (`git commit -m 'Add amazing feature'`)
|
|
203
|
+
4. Push to the branch (`git push origin feature/amazing-feature`)
|
|
204
|
+
5. Open a Pull Request
|
|
205
|
+
|
|
206
|
+
## Development
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
# Clone the main repository
|
|
210
|
+
git clone https://github.com/i-richardwang/MyCCusage.git
|
|
211
|
+
cd MyCCusage/packages/ccusage-collector
|
|
212
|
+
|
|
213
|
+
# Install dependencies
|
|
214
|
+
pnpm install
|
|
215
|
+
|
|
216
|
+
# Build the package
|
|
217
|
+
pnpm build
|
|
218
|
+
|
|
219
|
+
# Test CLI locally
|
|
220
|
+
pnpm cli --help
|
|
221
|
+
pnpm cli --dry-run --api-key=test --endpoint=http://localhost:3000
|
|
222
|
+
|
|
223
|
+
# Run type checking
|
|
224
|
+
pnpm typecheck
|
|
225
|
+
|
|
226
|
+
# Run linting
|
|
227
|
+
pnpm lint
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
## License
|
|
231
|
+
|
|
232
|
+
MIT License - see [LICENSE](LICENSE) file for details.
|
|
233
|
+
|
|
234
|
+
## Related Projects
|
|
235
|
+
|
|
236
|
+
- [My Claude Code Usage Dashboard](https://github.com/i-richardwang/MyCCusage) - The main dashboard application
|
|
237
|
+
- [Claude Code](https://claude.ai/code) - The AI-powered coding assistant
|
|
238
|
+
|
|
239
|
+
## Support
|
|
240
|
+
|
|
241
|
+
- 🐛 [Report bugs](https://github.com/i-richardwang/MyCCusage/issues)
|
|
242
|
+
- 💡 [Request features](https://github.com/i-richardwang/MyCCusage/issues)
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { program } from 'commander';
|
|
3
|
+
import * as cron from 'node-cron';
|
|
4
|
+
import { UsageCollector } from './collector.js';
|
|
5
|
+
program
|
|
6
|
+
.name('ccusage-collector')
|
|
7
|
+
.description('Collect and sync Claude Code usage statistics')
|
|
8
|
+
.version('0.1.0');
|
|
9
|
+
program
|
|
10
|
+
.option('-k, --api-key <key>', 'API key for authentication')
|
|
11
|
+
.option('-e, --endpoint <url>', 'API endpoint URL')
|
|
12
|
+
.option('-s, --schedule <cron>', `Cron schedule for periodic sync
|
|
13
|
+
Examples:
|
|
14
|
+
"0 */4 * * *" - Every 4 hours
|
|
15
|
+
"*/10 * * * *" - Every 10 minutes`)
|
|
16
|
+
.option('-r, --max-retries <number>', 'Maximum number of retry attempts', '3')
|
|
17
|
+
.option('-d, --retry-delay <ms>', 'Delay between retry attempts in milliseconds', '1000')
|
|
18
|
+
.option('--dry-run', 'Collect data but don\'t sync to server')
|
|
19
|
+
.action(async (options) => {
|
|
20
|
+
// Validate required options
|
|
21
|
+
if (!options.apiKey) {
|
|
22
|
+
console.error('Error: API key is required (--api-key)');
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
if (!options.endpoint) {
|
|
26
|
+
console.error('Error: API endpoint is required (--endpoint)');
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
const config = {
|
|
30
|
+
apiKey: options.apiKey,
|
|
31
|
+
endpoint: options.endpoint,
|
|
32
|
+
maxRetries: parseInt(options.maxRetries),
|
|
33
|
+
retryDelay: parseInt(options.retryDelay)
|
|
34
|
+
};
|
|
35
|
+
const collector = new UsageCollector(config);
|
|
36
|
+
if (options.dryRun) {
|
|
37
|
+
console.log('Dry run mode: collecting data only');
|
|
38
|
+
try {
|
|
39
|
+
const data = await collector.collectUsageData();
|
|
40
|
+
console.log('Collected data:', JSON.stringify(data, null, 2));
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
console.error('Failed to collect data:', error instanceof Error ? error.message : error);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (options.schedule) {
|
|
49
|
+
console.log(`Starting scheduled sync with cron: ${options.schedule}`);
|
|
50
|
+
// Validate cron expression
|
|
51
|
+
if (!cron.validate(options.schedule)) {
|
|
52
|
+
console.error('Error: Invalid cron expression');
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
// Run once immediately
|
|
56
|
+
console.log('Running initial sync...');
|
|
57
|
+
await collector.run();
|
|
58
|
+
// Schedule periodic runs
|
|
59
|
+
cron.schedule(options.schedule, async () => {
|
|
60
|
+
console.log(`\n[${new Date().toISOString()}] Running scheduled sync...`);
|
|
61
|
+
await collector.run();
|
|
62
|
+
});
|
|
63
|
+
console.log('Scheduled sync is running. Press Ctrl+C to stop.');
|
|
64
|
+
// Keep the process running
|
|
65
|
+
process.on('SIGINT', () => {
|
|
66
|
+
console.log('\nStopping scheduled sync...');
|
|
67
|
+
process.exit(0);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
// Run once
|
|
72
|
+
await collector.run();
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
program.parse();
|
|
76
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { UsageData, SyncResult, CollectorConfig } from './types.js';
|
|
2
|
+
export declare class UsageCollector {
|
|
3
|
+
private config;
|
|
4
|
+
constructor(config: CollectorConfig);
|
|
5
|
+
collectUsageData(): Promise<UsageData>;
|
|
6
|
+
syncData(data: UsageData): Promise<SyncResult>;
|
|
7
|
+
run(): Promise<void>;
|
|
8
|
+
}
|
|
9
|
+
//# sourceMappingURL=collector.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"collector.d.ts","sourceRoot":"","sources":["../src/collector.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAIxE,qBAAa,cAAc;IACzB,OAAO,CAAC,MAAM,CAAiB;gBAEnB,MAAM,EAAE,eAAe;IAQ7B,gBAAgB,IAAI,OAAO,CAAC,SAAS,CAAC;IAqBtC,QAAQ,CAAC,IAAI,EAAE,SAAS,GAAG,OAAO,CAAC,UAAU,CAAC;IAqE9C,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC;CAU3B"}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { exec } from 'child_process';
|
|
2
|
+
import { promisify } from 'util';
|
|
3
|
+
import axios from 'axios';
|
|
4
|
+
const execAsync = promisify(exec);
|
|
5
|
+
export class UsageCollector {
|
|
6
|
+
config;
|
|
7
|
+
constructor(config) {
|
|
8
|
+
this.config = {
|
|
9
|
+
maxRetries: 3,
|
|
10
|
+
retryDelay: 1000,
|
|
11
|
+
...config
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
async collectUsageData() {
|
|
15
|
+
try {
|
|
16
|
+
console.log('Collecting usage data...');
|
|
17
|
+
const { stdout } = await execAsync('npx ccusage daily --json');
|
|
18
|
+
const data = JSON.parse(stdout);
|
|
19
|
+
if (!data.daily || !Array.isArray(data.daily)) {
|
|
20
|
+
throw new Error('Invalid usage data format');
|
|
21
|
+
}
|
|
22
|
+
console.log(`Collected ${data.daily.length} daily records`);
|
|
23
|
+
return data;
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
if (error instanceof Error) {
|
|
27
|
+
throw new Error(`Failed to collect usage data: ${error.message}`);
|
|
28
|
+
}
|
|
29
|
+
throw new Error('Failed to collect usage data: Unknown error');
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
async syncData(data) {
|
|
33
|
+
const { endpoint, apiKey, maxRetries = 3, retryDelay = 1000 } = this.config;
|
|
34
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
35
|
+
try {
|
|
36
|
+
console.log(`Syncing data (attempt ${attempt}/${maxRetries})...`);
|
|
37
|
+
const response = await axios.post(endpoint, data, {
|
|
38
|
+
headers: {
|
|
39
|
+
'Content-Type': 'application/json',
|
|
40
|
+
'x-api-key': apiKey
|
|
41
|
+
},
|
|
42
|
+
timeout: 30000
|
|
43
|
+
});
|
|
44
|
+
const result = response.data;
|
|
45
|
+
if (result.success) {
|
|
46
|
+
console.log(`Successfully synced ${result.processed} records`);
|
|
47
|
+
// Log any errors for individual records
|
|
48
|
+
const errors = result.results.filter(r => r.status === 'error');
|
|
49
|
+
if (errors.length > 0) {
|
|
50
|
+
console.warn(`${errors.length} records had errors:`);
|
|
51
|
+
errors.forEach(error => {
|
|
52
|
+
console.warn(` - ${error.date}: ${error.message}`);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
return result;
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
throw new Error('Sync failed: ' + JSON.stringify(result));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
const isLastAttempt = attempt === maxRetries;
|
|
63
|
+
if (axios.isAxiosError(error)) {
|
|
64
|
+
const status = error.response?.status;
|
|
65
|
+
const message = error.response?.data?.error || error.message;
|
|
66
|
+
console.error(`Sync failed (attempt ${attempt}/${maxRetries}): ${status} - ${message}`);
|
|
67
|
+
// Don't retry on authentication errors
|
|
68
|
+
if (status === 401 || status === 403) {
|
|
69
|
+
throw new Error(`Authentication failed: ${message}`);
|
|
70
|
+
}
|
|
71
|
+
if (isLastAttempt) {
|
|
72
|
+
throw new Error(`Sync failed after ${maxRetries} attempts: ${message}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
console.error(`Sync failed (attempt ${attempt}/${maxRetries}):`, error);
|
|
77
|
+
if (isLastAttempt) {
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// Wait before retrying
|
|
82
|
+
if (!isLastAttempt) {
|
|
83
|
+
console.log(`Retrying in ${retryDelay}ms...`);
|
|
84
|
+
await new Promise(resolve => setTimeout(resolve, retryDelay));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
throw new Error('Sync failed after all retries');
|
|
89
|
+
}
|
|
90
|
+
async run() {
|
|
91
|
+
try {
|
|
92
|
+
const data = await this.collectUsageData();
|
|
93
|
+
await this.syncData(data);
|
|
94
|
+
console.log('Usage data sync completed successfully');
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
console.error('Usage data sync failed:', error instanceof Error ? error.message : error);
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=collector.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"collector.js","sourceRoot":"","sources":["../src/collector.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AAC/C,YAAY,EACV,gBAAgB,EAChB,SAAS,EACT,UAAU,EACV,eAAe,EAChB,MAAM,YAAY,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export interface DailyUsageRecord {
|
|
2
|
+
date: string;
|
|
3
|
+
inputTokens: number;
|
|
4
|
+
outputTokens: number;
|
|
5
|
+
cacheCreationTokens: number;
|
|
6
|
+
cacheReadTokens: number;
|
|
7
|
+
totalTokens: number;
|
|
8
|
+
totalCost: number;
|
|
9
|
+
modelsUsed: string[];
|
|
10
|
+
modelBreakdowns: Array<{
|
|
11
|
+
modelName: string;
|
|
12
|
+
inputTokens: number;
|
|
13
|
+
outputTokens: number;
|
|
14
|
+
cacheCreationTokens: number;
|
|
15
|
+
cacheReadTokens: number;
|
|
16
|
+
cost: number;
|
|
17
|
+
}>;
|
|
18
|
+
}
|
|
19
|
+
export interface UsageData {
|
|
20
|
+
daily: DailyUsageRecord[];
|
|
21
|
+
totals: {
|
|
22
|
+
inputTokens: number;
|
|
23
|
+
outputTokens: number;
|
|
24
|
+
cacheCreationTokens: number;
|
|
25
|
+
cacheReadTokens: number;
|
|
26
|
+
totalCost: number;
|
|
27
|
+
totalTokens: number;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export interface SyncResult {
|
|
31
|
+
success: boolean;
|
|
32
|
+
processed: number;
|
|
33
|
+
results: Array<{
|
|
34
|
+
date: string;
|
|
35
|
+
status: 'success' | 'error';
|
|
36
|
+
message?: string;
|
|
37
|
+
}>;
|
|
38
|
+
}
|
|
39
|
+
export interface CollectorConfig {
|
|
40
|
+
endpoint: string;
|
|
41
|
+
apiKey: string;
|
|
42
|
+
schedule?: string;
|
|
43
|
+
maxRetries?: number;
|
|
44
|
+
retryDelay?: number;
|
|
45
|
+
}
|
|
46
|
+
//# 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,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,EAAE,MAAM,CAAA;IACnB,YAAY,EAAE,MAAM,CAAA;IACpB,mBAAmB,EAAE,MAAM,CAAA;IAC3B,eAAe,EAAE,MAAM,CAAA;IACvB,WAAW,EAAE,MAAM,CAAA;IACnB,SAAS,EAAE,MAAM,CAAA;IACjB,UAAU,EAAE,MAAM,EAAE,CAAA;IACpB,eAAe,EAAE,KAAK,CAAC;QACrB,SAAS,EAAE,MAAM,CAAA;QACjB,WAAW,EAAE,MAAM,CAAA;QACnB,YAAY,EAAE,MAAM,CAAA;QACpB,mBAAmB,EAAE,MAAM,CAAA;QAC3B,eAAe,EAAE,MAAM,CAAA;QACvB,IAAI,EAAE,MAAM,CAAA;KACb,CAAC,CAAA;CACH;AAED,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,gBAAgB,EAAE,CAAA;IACzB,MAAM,EAAE;QACN,WAAW,EAAE,MAAM,CAAA;QACnB,YAAY,EAAE,MAAM,CAAA;QACpB,mBAAmB,EAAE,MAAM,CAAA;QAC3B,eAAe,EAAE,MAAM,CAAA;QACvB,SAAS,EAAE,MAAM,CAAA;QACjB,WAAW,EAAE,MAAM,CAAA;KACpB,CAAA;CACF;AAED,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,OAAO,CAAA;IAChB,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,KAAK,CAAC;QACb,IAAI,EAAE,MAAM,CAAA;QACZ,MAAM,EAAE,SAAS,GAAG,OAAO,CAAA;QAC3B,OAAO,CAAC,EAAE,MAAM,CAAA;KACjB,CAAC,CAAA;CACH;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
package/eslint.config.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ccusage-collector",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Local collector for Claude Code usage statistics",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"ccusage-collector": "dist/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"keywords": [
|
|
11
|
+
"claude",
|
|
12
|
+
"claude-code",
|
|
13
|
+
"usage",
|
|
14
|
+
"collector",
|
|
15
|
+
"statistics",
|
|
16
|
+
"cli",
|
|
17
|
+
"monitoring",
|
|
18
|
+
"dashboard"
|
|
19
|
+
],
|
|
20
|
+
"author": "Richard Wang",
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "https://github.com/i-richardwang/MyCCusage.git",
|
|
24
|
+
"directory": "packages/ccusage-collector"
|
|
25
|
+
},
|
|
26
|
+
"bugs": {
|
|
27
|
+
"url": "https://github.com/i-richardwang/MyCCusage/issues"
|
|
28
|
+
},
|
|
29
|
+
"homepage": "https://github.com/i-richardwang/MyCCusage#readme",
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@types/node": "^20",
|
|
33
|
+
"@types/node-cron": "^3.0.11",
|
|
34
|
+
"tsx": "^4.20.3",
|
|
35
|
+
"typescript": "^5.8.3",
|
|
36
|
+
"@workspace/typescript-config": "0.0.0",
|
|
37
|
+
"@workspace/eslint-config": "0.0.0"
|
|
38
|
+
},
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"axios": "^1.10.0",
|
|
44
|
+
"commander": "^14.0.0",
|
|
45
|
+
"dotenv": "^17.1.0",
|
|
46
|
+
"node-cron": "^4.2.0"
|
|
47
|
+
},
|
|
48
|
+
"scripts": {
|
|
49
|
+
"build": "tsc",
|
|
50
|
+
"dev": "tsx src/index.ts",
|
|
51
|
+
"start": "node dist/index.js",
|
|
52
|
+
"cli": "tsx src/cli.ts",
|
|
53
|
+
"lint": "eslint . --max-warnings 0",
|
|
54
|
+
"typecheck": "tsc --noEmit"
|
|
55
|
+
}
|
|
56
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { program } from 'commander'
|
|
4
|
+
import * as cron from 'node-cron'
|
|
5
|
+
import { UsageCollector } from './collector.js'
|
|
6
|
+
import type { CollectorConfig } from './types.js'
|
|
7
|
+
|
|
8
|
+
program
|
|
9
|
+
.name('ccusage-collector')
|
|
10
|
+
.description('Collect and sync Claude Code usage statistics')
|
|
11
|
+
.version('0.1.0')
|
|
12
|
+
|
|
13
|
+
program
|
|
14
|
+
.option('-k, --api-key <key>', 'API key for authentication')
|
|
15
|
+
.option('-e, --endpoint <url>', 'API endpoint URL')
|
|
16
|
+
.option('-s, --schedule <cron>', `Cron schedule for periodic sync
|
|
17
|
+
Examples:
|
|
18
|
+
"0 */4 * * *" - Every 4 hours
|
|
19
|
+
"*/10 * * * *" - Every 10 minutes`)
|
|
20
|
+
.option('-r, --max-retries <number>', 'Maximum number of retry attempts', '3')
|
|
21
|
+
.option('-d, --retry-delay <ms>', 'Delay between retry attempts in milliseconds', '1000')
|
|
22
|
+
.option('--dry-run', 'Collect data but don\'t sync to server')
|
|
23
|
+
.action(async (options) => {
|
|
24
|
+
// Validate required options
|
|
25
|
+
if (!options.apiKey) {
|
|
26
|
+
console.error('Error: API key is required (--api-key)')
|
|
27
|
+
process.exit(1)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (!options.endpoint) {
|
|
31
|
+
console.error('Error: API endpoint is required (--endpoint)')
|
|
32
|
+
process.exit(1)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const config: CollectorConfig = {
|
|
36
|
+
apiKey: options.apiKey,
|
|
37
|
+
endpoint: options.endpoint,
|
|
38
|
+
maxRetries: parseInt(options.maxRetries),
|
|
39
|
+
retryDelay: parseInt(options.retryDelay)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const collector = new UsageCollector(config)
|
|
43
|
+
|
|
44
|
+
if (options.dryRun) {
|
|
45
|
+
console.log('Dry run mode: collecting data only')
|
|
46
|
+
try {
|
|
47
|
+
const data = await collector.collectUsageData()
|
|
48
|
+
console.log('Collected data:', JSON.stringify(data, null, 2))
|
|
49
|
+
} catch (error) {
|
|
50
|
+
console.error('Failed to collect data:', error instanceof Error ? error.message : error)
|
|
51
|
+
process.exit(1)
|
|
52
|
+
}
|
|
53
|
+
return
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (options.schedule) {
|
|
57
|
+
console.log(`Starting scheduled sync with cron: ${options.schedule}`)
|
|
58
|
+
|
|
59
|
+
// Validate cron expression
|
|
60
|
+
if (!cron.validate(options.schedule)) {
|
|
61
|
+
console.error('Error: Invalid cron expression')
|
|
62
|
+
process.exit(1)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Run once immediately
|
|
66
|
+
console.log('Running initial sync...')
|
|
67
|
+
await collector.run()
|
|
68
|
+
|
|
69
|
+
// Schedule periodic runs
|
|
70
|
+
cron.schedule(options.schedule, async () => {
|
|
71
|
+
console.log(`\n[${new Date().toISOString()}] Running scheduled sync...`)
|
|
72
|
+
await collector.run()
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
console.log('Scheduled sync is running. Press Ctrl+C to stop.')
|
|
76
|
+
|
|
77
|
+
// Keep the process running
|
|
78
|
+
process.on('SIGINT', () => {
|
|
79
|
+
console.log('\nStopping scheduled sync...')
|
|
80
|
+
process.exit(0)
|
|
81
|
+
})
|
|
82
|
+
} else {
|
|
83
|
+
// Run once
|
|
84
|
+
await collector.run()
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
program.parse()
|
package/src/collector.ts
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { exec } from 'child_process'
|
|
2
|
+
import { promisify } from 'util'
|
|
3
|
+
import axios from 'axios'
|
|
4
|
+
import type { UsageData, SyncResult, CollectorConfig } from './types.js'
|
|
5
|
+
|
|
6
|
+
const execAsync = promisify(exec)
|
|
7
|
+
|
|
8
|
+
export class UsageCollector {
|
|
9
|
+
private config: CollectorConfig
|
|
10
|
+
|
|
11
|
+
constructor(config: CollectorConfig) {
|
|
12
|
+
this.config = {
|
|
13
|
+
maxRetries: 3,
|
|
14
|
+
retryDelay: 1000,
|
|
15
|
+
...config
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async collectUsageData(): Promise<UsageData> {
|
|
20
|
+
try {
|
|
21
|
+
console.log('Collecting usage data...')
|
|
22
|
+
const { stdout } = await execAsync('npx ccusage daily --json')
|
|
23
|
+
|
|
24
|
+
const data: UsageData = JSON.parse(stdout)
|
|
25
|
+
|
|
26
|
+
if (!data.daily || !Array.isArray(data.daily)) {
|
|
27
|
+
throw new Error('Invalid usage data format')
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
console.log(`Collected ${data.daily.length} daily records`)
|
|
31
|
+
return data
|
|
32
|
+
} catch (error) {
|
|
33
|
+
if (error instanceof Error) {
|
|
34
|
+
throw new Error(`Failed to collect usage data: ${error.message}`)
|
|
35
|
+
}
|
|
36
|
+
throw new Error('Failed to collect usage data: Unknown error')
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async syncData(data: UsageData): Promise<SyncResult> {
|
|
41
|
+
const { endpoint, apiKey, maxRetries = 3, retryDelay = 1000 } = this.config
|
|
42
|
+
|
|
43
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
44
|
+
try {
|
|
45
|
+
console.log(`Syncing data (attempt ${attempt}/${maxRetries})...`)
|
|
46
|
+
|
|
47
|
+
const response = await axios.post(endpoint, data, {
|
|
48
|
+
headers: {
|
|
49
|
+
'Content-Type': 'application/json',
|
|
50
|
+
'x-api-key': apiKey
|
|
51
|
+
},
|
|
52
|
+
timeout: 30000
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
const result: SyncResult = response.data
|
|
56
|
+
|
|
57
|
+
if (result.success) {
|
|
58
|
+
console.log(`Successfully synced ${result.processed} records`)
|
|
59
|
+
|
|
60
|
+
// Log any errors for individual records
|
|
61
|
+
const errors = result.results.filter(r => r.status === 'error')
|
|
62
|
+
if (errors.length > 0) {
|
|
63
|
+
console.warn(`${errors.length} records had errors:`)
|
|
64
|
+
errors.forEach(error => {
|
|
65
|
+
console.warn(` - ${error.date}: ${error.message}`)
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return result
|
|
70
|
+
} else {
|
|
71
|
+
throw new Error('Sync failed: ' + JSON.stringify(result))
|
|
72
|
+
}
|
|
73
|
+
} catch (error) {
|
|
74
|
+
const isLastAttempt = attempt === maxRetries
|
|
75
|
+
|
|
76
|
+
if (axios.isAxiosError(error)) {
|
|
77
|
+
const status = error.response?.status
|
|
78
|
+
const message = error.response?.data?.error || error.message
|
|
79
|
+
|
|
80
|
+
console.error(`Sync failed (attempt ${attempt}/${maxRetries}): ${status} - ${message}`)
|
|
81
|
+
|
|
82
|
+
// Don't retry on authentication errors
|
|
83
|
+
if (status === 401 || status === 403) {
|
|
84
|
+
throw new Error(`Authentication failed: ${message}`)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (isLastAttempt) {
|
|
88
|
+
throw new Error(`Sync failed after ${maxRetries} attempts: ${message}`)
|
|
89
|
+
}
|
|
90
|
+
} else {
|
|
91
|
+
console.error(`Sync failed (attempt ${attempt}/${maxRetries}):`, error)
|
|
92
|
+
|
|
93
|
+
if (isLastAttempt) {
|
|
94
|
+
throw error
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Wait before retrying
|
|
99
|
+
if (!isLastAttempt) {
|
|
100
|
+
console.log(`Retrying in ${retryDelay}ms...`)
|
|
101
|
+
await new Promise(resolve => setTimeout(resolve, retryDelay))
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
throw new Error('Sync failed after all retries')
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async run(): Promise<void> {
|
|
110
|
+
try {
|
|
111
|
+
const data = await this.collectUsageData()
|
|
112
|
+
await this.syncData(data)
|
|
113
|
+
console.log('Usage data sync completed successfully')
|
|
114
|
+
} catch (error) {
|
|
115
|
+
console.error('Usage data sync failed:', error instanceof Error ? error.message : error)
|
|
116
|
+
process.exit(1)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
package/src/index.ts
ADDED
package/src/types.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export interface DailyUsageRecord {
|
|
2
|
+
date: string
|
|
3
|
+
inputTokens: number
|
|
4
|
+
outputTokens: number
|
|
5
|
+
cacheCreationTokens: number
|
|
6
|
+
cacheReadTokens: number
|
|
7
|
+
totalTokens: number
|
|
8
|
+
totalCost: number
|
|
9
|
+
modelsUsed: string[]
|
|
10
|
+
modelBreakdowns: Array<{
|
|
11
|
+
modelName: string
|
|
12
|
+
inputTokens: number
|
|
13
|
+
outputTokens: number
|
|
14
|
+
cacheCreationTokens: number
|
|
15
|
+
cacheReadTokens: number
|
|
16
|
+
cost: number
|
|
17
|
+
}>
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface UsageData {
|
|
21
|
+
daily: DailyUsageRecord[]
|
|
22
|
+
totals: {
|
|
23
|
+
inputTokens: number
|
|
24
|
+
outputTokens: number
|
|
25
|
+
cacheCreationTokens: number
|
|
26
|
+
cacheReadTokens: number
|
|
27
|
+
totalCost: number
|
|
28
|
+
totalTokens: number
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface SyncResult {
|
|
33
|
+
success: boolean
|
|
34
|
+
processed: number
|
|
35
|
+
results: Array<{
|
|
36
|
+
date: string
|
|
37
|
+
status: 'success' | 'error'
|
|
38
|
+
message?: string
|
|
39
|
+
}>
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface CollectorConfig {
|
|
43
|
+
endpoint: string
|
|
44
|
+
apiKey: string
|
|
45
|
+
schedule?: string
|
|
46
|
+
maxRetries?: number
|
|
47
|
+
retryDelay?: number
|
|
48
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "@workspace/typescript-config/base.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"outDir": "dist",
|
|
5
|
+
"rootDir": "src",
|
|
6
|
+
"declaration": true,
|
|
7
|
+
"declarationMap": true,
|
|
8
|
+
"sourceMap": true,
|
|
9
|
+
"module": "ESNext",
|
|
10
|
+
"moduleResolution": "node",
|
|
11
|
+
"target": "ES2022",
|
|
12
|
+
"lib": ["ES2022"],
|
|
13
|
+
"allowSyntheticDefaultImports": true,
|
|
14
|
+
"esModuleInterop": true
|
|
15
|
+
},
|
|
16
|
+
"include": ["src/**/*"],
|
|
17
|
+
"exclude": ["node_modules", "dist"]
|
|
18
|
+
}
|