ccwarm-temp 1.0.0 → 1.0.1
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/README.md +8 -4
- package/dist/index.js +8 -9
- package/dist/src/analyzer.js +20 -32
- package/dist/src/daemon/manager.js +10 -0
- package/dist/src/types.d.ts +6 -3
- package/dist/src/utils.d.ts +11 -0
- package/dist/src/utils.js +67 -0
- package/dist/src/worker.js +13 -13
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,15 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
Optimize your Claude Code rate limit window by pre-warming sessions based on your usage patterns.
|
|
4
4
|
|
|
5
|
+
> **Note:** This tool is for **Claude Pro and Max** subscribers, not for API-based usage.
|
|
6
|
+
|
|
5
7
|
## The Problem
|
|
6
8
|
|
|
7
|
-
Claude Code's rate limits reset 5 hours after your **first message**.
|
|
9
|
+
Claude Code's rate limits reset 5 hours after your **first message**.
|
|
8
10
|
|
|
9
11
|
## The Solution
|
|
10
12
|
|
|
11
|
-
ccwarm analyzes your usage history to find when you typically work. It then sends a
|
|
13
|
+
ccwarm analyzes your usage history to find when you typically work. It then sends a short message to Claude Code at the optimal time, so your rate limit resets when you actually need it.
|
|
14
|
+
|
|
15
|
+
### Example
|
|
12
16
|
|
|
13
|
-
|
|
17
|
+
You typically work 9:00-13:30 and get blocked in the middle → ccwarm sends "hi" at 7:15 → limit resets at 12:15 instead of 14:00
|
|
14
18
|
|
|
15
19
|
## Usage
|
|
16
20
|
|
|
@@ -27,7 +31,7 @@ npm install -g ccwarm
|
|
|
27
31
|
## Usage
|
|
28
32
|
|
|
29
33
|
```bash
|
|
30
|
-
ccwarm analyze [days] # Analyze usage patterns (default:
|
|
34
|
+
ccwarm analyze [days] # Analyze usage patterns (default: 30 days)
|
|
31
35
|
ccwarm warmup # Run warmup check now
|
|
32
36
|
ccwarm start # Start background daemon
|
|
33
37
|
ccwarm stop # Stop daemon
|
package/dist/index.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { spawn } from 'node:child_process';
|
|
3
2
|
import { readFile } from 'node:fs/promises';
|
|
4
3
|
import { consola } from 'consola';
|
|
5
4
|
import { createWarmupPlan } from './src/analyzer.js';
|
|
6
5
|
import { paths } from './src/config.js';
|
|
7
|
-
import { getDaemonStatus, startDaemon, stopDaemon } from './src/daemon/manager.js';
|
|
6
|
+
import { getDaemonLogs, getDaemonStatus, startDaemon, stopDaemon } from './src/daemon/manager.js';
|
|
7
|
+
import { formatDuration, formatTime } from './src/utils.js';
|
|
8
8
|
import { runWarmupWorker } from './src/worker.js';
|
|
9
9
|
const cmd = process.argv[2] ?? '';
|
|
10
10
|
const commands = {
|
|
11
11
|
async analyze() {
|
|
12
|
-
const days = parseInt(process.argv[3] ?? '
|
|
12
|
+
const days = parseInt(process.argv[3] ?? '30', 10);
|
|
13
13
|
await createWarmupPlan(days);
|
|
14
14
|
},
|
|
15
15
|
async warmup() {
|
|
@@ -41,9 +41,9 @@ const commands = {
|
|
|
41
41
|
plan
|
|
42
42
|
? [
|
|
43
43
|
'Warmup Plan:',
|
|
44
|
-
` Target
|
|
45
|
-
` Median Start: ${plan.
|
|
46
|
-
` Median Duration: ${plan.
|
|
44
|
+
` Target Time: ${formatTime(plan.targetMinute)}`,
|
|
45
|
+
` Median Start: ${formatTime(plan.medianStartMinute)}`,
|
|
46
|
+
` Median Duration: ${formatDuration(plan.medianDurationMinutes)}`,
|
|
47
47
|
` Calculated: ${plan.calculatedAt}`,
|
|
48
48
|
].join('\n')
|
|
49
49
|
: "No warmup plan. Run 'ccwarm analyze' first.",
|
|
@@ -51,8 +51,7 @@ const commands = {
|
|
|
51
51
|
});
|
|
52
52
|
},
|
|
53
53
|
async logs() {
|
|
54
|
-
|
|
55
|
-
tail.on('error', () => consola.error(`Cannot read: ${paths.log}`));
|
|
54
|
+
await getDaemonLogs();
|
|
56
55
|
},
|
|
57
56
|
};
|
|
58
57
|
const run = commands[cmd];
|
|
@@ -66,7 +65,7 @@ else {
|
|
|
66
65
|
'Usage: ccwarm <command>',
|
|
67
66
|
'',
|
|
68
67
|
'Commands:',
|
|
69
|
-
' analyze [days] Analyze usage patterns (default:
|
|
68
|
+
' analyze [days] Analyze usage patterns (default: 30 days)',
|
|
70
69
|
' warmup Run warmup check now',
|
|
71
70
|
' start Start background daemon',
|
|
72
71
|
' stop Stop background daemon',
|
package/dist/src/analyzer.js
CHANGED
|
@@ -2,23 +2,8 @@ import { writeFile } from 'node:fs/promises';
|
|
|
2
2
|
import { loadSessionBlockData } from 'ccusage/data-loader';
|
|
3
3
|
import { consola } from 'consola';
|
|
4
4
|
import { ensureConfigDir, paths } from './config.js';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
return 0;
|
|
8
|
-
const sorted = arr.toSorted((a, b) => a - b);
|
|
9
|
-
const mid = Math.floor(sorted.length / 2);
|
|
10
|
-
if (sorted.length % 2) {
|
|
11
|
-
return sorted[mid] ?? 0;
|
|
12
|
-
}
|
|
13
|
-
return ((sorted[mid - 1] ?? 0) + (sorted[mid] ?? 0)) / 2;
|
|
14
|
-
};
|
|
15
|
-
const daysAgo = (n) => {
|
|
16
|
-
const d = new Date();
|
|
17
|
-
d.setDate(d.getDate() - n);
|
|
18
|
-
const [date] = d.toISOString().split('T');
|
|
19
|
-
return date?.replace(/-/g, '') ?? '';
|
|
20
|
-
};
|
|
21
|
-
export async function createWarmupPlan(days = 21) {
|
|
5
|
+
import { daysAgo, formatTime, median } from './utils.js';
|
|
6
|
+
export async function createWarmupPlan(days = 30) {
|
|
22
7
|
consola.info(`Analyzing Claude usage patterns (last ${days} days)...`);
|
|
23
8
|
try {
|
|
24
9
|
await ensureConfigDir();
|
|
@@ -31,33 +16,36 @@ export async function createWarmupPlan(days = 21) {
|
|
|
31
16
|
const MIN_SESSION_MINUTES = 1;
|
|
32
17
|
const sessions = blocks
|
|
33
18
|
.filter((b) => !b.isGap && !!b.actualEndTime)
|
|
34
|
-
.map((b) =>
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
19
|
+
.map((b) => {
|
|
20
|
+
const start = new Date(b.startTime);
|
|
21
|
+
return {
|
|
22
|
+
startMinute: start.getHours() * 60 + start.getMinutes(),
|
|
23
|
+
durationMs: new Date(b.actualEndTime).getTime() - start.getTime(),
|
|
24
|
+
};
|
|
25
|
+
})
|
|
38
26
|
.filter((s) => s.durationMs >= MIN_SESSION_MINUTES * 60_000)
|
|
39
27
|
.map((s) => ({
|
|
40
|
-
|
|
41
|
-
|
|
28
|
+
startMinute: s.startMinute,
|
|
29
|
+
durationMinutes: s.durationMs / 60_000,
|
|
42
30
|
}));
|
|
43
31
|
const MIN_SESSIONS = 3;
|
|
44
32
|
if (sessions.length < MIN_SESSIONS) {
|
|
45
33
|
consola.warn(`Not enough data (need at least ${MIN_SESSIONS} sessions)`);
|
|
46
34
|
return null;
|
|
47
35
|
}
|
|
48
|
-
const
|
|
49
|
-
const
|
|
50
|
-
let
|
|
51
|
-
if (
|
|
52
|
-
|
|
36
|
+
const medianStartMinute = median(sessions.map((s) => s.startMinute));
|
|
37
|
+
const medianDurationMinutes = median(sessions.map((s) => s.durationMinutes));
|
|
38
|
+
let targetMinute = Math.round(medianStartMinute - medianDurationMinutes / 2);
|
|
39
|
+
if (targetMinute < 0)
|
|
40
|
+
targetMinute += 24 * 60;
|
|
53
41
|
const plan = {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
42
|
+
targetMinute,
|
|
43
|
+
medianStartMinute,
|
|
44
|
+
medianDurationMinutes,
|
|
57
45
|
calculatedAt: new Date().toISOString(),
|
|
58
46
|
};
|
|
59
47
|
await writeFile(paths.plan, JSON.stringify(plan, null, 2));
|
|
60
|
-
consola.success(`Plan updated: Target Warmup at ${
|
|
48
|
+
consola.success(`Plan updated: Target Warmup at ${formatTime(targetMinute)}`);
|
|
61
49
|
return plan;
|
|
62
50
|
}
|
|
63
51
|
catch (error) {
|
|
@@ -2,6 +2,7 @@ import { spawn, spawnSync } from 'node:child_process';
|
|
|
2
2
|
import { dirname, join } from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import { consola } from 'consola';
|
|
5
|
+
import { getNextWarmupDate, loadPlan } from '../utils.js';
|
|
5
6
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
6
7
|
const APP_NAME = 'ccwarm';
|
|
7
8
|
const pm2 = (args) => new Promise((resolve) => {
|
|
@@ -46,6 +47,15 @@ export async function startDaemon() {
|
|
|
46
47
|
if (code === 0) {
|
|
47
48
|
const newStatus = await getDaemonStatus();
|
|
48
49
|
consola.success(`Daemon started (PID: ${newStatus.pid})`);
|
|
50
|
+
let plan = await loadPlan();
|
|
51
|
+
if (!plan) {
|
|
52
|
+
consola.info('No warmup plan found, running analyze...');
|
|
53
|
+
const { createWarmupPlan } = await import('../analyzer.js');
|
|
54
|
+
plan = await createWarmupPlan();
|
|
55
|
+
}
|
|
56
|
+
if (plan) {
|
|
57
|
+
consola.info(`Next warmup: ${getNextWarmupDate(plan.targetMinute).toLocaleString()}`);
|
|
58
|
+
}
|
|
49
59
|
return true;
|
|
50
60
|
}
|
|
51
61
|
consola.error('Failed to start daemon');
|
package/dist/src/types.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
export interface WarmupPlan {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
/** Target warmup time in minutes from midnight (0-1439) */
|
|
3
|
+
targetMinute: number;
|
|
4
|
+
/** Median session start time in minutes from midnight */
|
|
5
|
+
medianStartMinute: number;
|
|
6
|
+
/** Median session duration in minutes */
|
|
7
|
+
medianDurationMinutes: number;
|
|
5
8
|
calculatedAt: string;
|
|
6
9
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { WarmupPlan } from './types.js';
|
|
2
|
+
export declare const median: (arr: number[]) => number;
|
|
3
|
+
export declare const daysAgo: (n: number) => string;
|
|
4
|
+
export declare const loadPlan: () => Promise<WarmupPlan | null>;
|
|
5
|
+
export declare const getNextWarmupDate: (targetMinute: number) => Date;
|
|
6
|
+
/** Parse time string like "11:30" or "11" to minutes from midnight */
|
|
7
|
+
export declare const parseTime: (input: string) => number | null;
|
|
8
|
+
/** Format minutes from midnight to HH:MM string */
|
|
9
|
+
export declare const formatTime: (minutes: number) => string;
|
|
10
|
+
/** Format duration in minutes to human readable string */
|
|
11
|
+
export declare const formatDuration: (minutes: number) => string;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { paths } from './config.js';
|
|
3
|
+
export const median = (arr) => {
|
|
4
|
+
if (!arr.length)
|
|
5
|
+
return 0;
|
|
6
|
+
const sorted = arr.toSorted((a, b) => a - b);
|
|
7
|
+
const mid = Math.floor(sorted.length / 2);
|
|
8
|
+
return sorted.length % 2 ? (sorted[mid] ?? 0) : ((sorted[mid - 1] ?? 0) + (sorted[mid] ?? 0)) / 2;
|
|
9
|
+
};
|
|
10
|
+
export const daysAgo = (n) => {
|
|
11
|
+
const d = new Date();
|
|
12
|
+
d.setDate(d.getDate() - n);
|
|
13
|
+
return d.toISOString().split('T')[0]?.replace(/-/g, '') ?? '';
|
|
14
|
+
};
|
|
15
|
+
export const loadPlan = async () => {
|
|
16
|
+
try {
|
|
17
|
+
return JSON.parse(await readFile(paths.plan, 'utf-8'));
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
export const getNextWarmupDate = (targetMinute) => {
|
|
24
|
+
const date = new Date();
|
|
25
|
+
const hours = Math.floor(targetMinute / 60);
|
|
26
|
+
const minutes = targetMinute % 60;
|
|
27
|
+
date.setHours(hours, minutes, 0, 0);
|
|
28
|
+
if (date <= new Date())
|
|
29
|
+
date.setDate(date.getDate() + 1);
|
|
30
|
+
return date;
|
|
31
|
+
};
|
|
32
|
+
/** Parse time string like "11:30" or "11" to minutes from midnight */
|
|
33
|
+
export const parseTime = (input) => {
|
|
34
|
+
const trimmed = input.trim();
|
|
35
|
+
// Format: HH:MM or H:MM
|
|
36
|
+
if (trimmed.includes(':')) {
|
|
37
|
+
const [hourStr, minStr] = trimmed.split(':');
|
|
38
|
+
const hour = parseInt(hourStr ?? '', 10);
|
|
39
|
+
const min = parseInt(minStr ?? '', 10);
|
|
40
|
+
if (Number.isNaN(hour) || Number.isNaN(min) || hour < 0 || hour > 23 || min < 0 || min > 59) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
return hour * 60 + min;
|
|
44
|
+
}
|
|
45
|
+
// Format: HH or H (just hours)
|
|
46
|
+
const hour = parseInt(trimmed, 10);
|
|
47
|
+
if (Number.isNaN(hour) || hour < 0 || hour > 23) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
return hour * 60;
|
|
51
|
+
};
|
|
52
|
+
/** Format minutes from midnight to HH:MM string */
|
|
53
|
+
export const formatTime = (minutes) => {
|
|
54
|
+
const h = Math.floor(minutes / 60);
|
|
55
|
+
const m = minutes % 60;
|
|
56
|
+
return `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}`;
|
|
57
|
+
};
|
|
58
|
+
/** Format duration in minutes to human readable string */
|
|
59
|
+
export const formatDuration = (minutes) => {
|
|
60
|
+
const h = Math.floor(minutes / 60);
|
|
61
|
+
const m = Math.round(minutes % 60);
|
|
62
|
+
if (h === 0)
|
|
63
|
+
return `${m}m`;
|
|
64
|
+
if (m === 0)
|
|
65
|
+
return `${h}h`;
|
|
66
|
+
return `${h}h ${m}m`;
|
|
67
|
+
};
|
package/dist/src/worker.js
CHANGED
|
@@ -1,17 +1,22 @@
|
|
|
1
|
-
import { readFile } from 'node:fs/promises';
|
|
2
1
|
import { loadSessionBlockData } from 'ccusage/data-loader';
|
|
3
2
|
import { consola } from 'consola';
|
|
4
3
|
import { execa } from 'execa';
|
|
5
|
-
import {
|
|
4
|
+
import { formatTime, loadPlan } from './utils.js';
|
|
6
5
|
export async function runWarmupWorker() {
|
|
7
6
|
try {
|
|
8
|
-
const plan =
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
const plan = await loadPlan();
|
|
8
|
+
if (!plan) {
|
|
9
|
+
consola.warn("No warmup plan found. Run 'ccwarm analyze' first.");
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
const now = new Date();
|
|
13
|
+
const currentMinute = now.getHours() * 60 + now.getMinutes();
|
|
14
|
+
const effectiveMinute = plan.targetMinute;
|
|
15
|
+
if (currentMinute !== effectiveMinute) {
|
|
16
|
+
consola.debug(`Not warmup time. Current: ${formatTime(currentMinute)}, Target: ${formatTime(effectiveMinute)}`);
|
|
12
17
|
return false;
|
|
13
18
|
}
|
|
14
|
-
consola.info('Target
|
|
19
|
+
consola.info('Target time matched. Checking for active sessions...');
|
|
15
20
|
const blocks = await loadSessionBlockData();
|
|
16
21
|
if (blocks?.some((b) => b.isActive)) {
|
|
17
22
|
consola.info('Session already active. Skipping warmup.');
|
|
@@ -23,12 +28,7 @@ export async function runWarmupWorker() {
|
|
|
23
28
|
return true;
|
|
24
29
|
}
|
|
25
30
|
catch (err) {
|
|
26
|
-
|
|
27
|
-
consola.warn("No warmup plan found. Run 'ccwarm analyze' first.");
|
|
28
|
-
}
|
|
29
|
-
else {
|
|
30
|
-
consola.error('Warmup failed:', err);
|
|
31
|
-
}
|
|
31
|
+
consola.error('Warmup failed:', err);
|
|
32
32
|
return false;
|
|
33
33
|
}
|
|
34
34
|
}
|