ccwarm-temp 1.0.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/README.md ADDED
@@ -0,0 +1,37 @@
1
+ # ccwarm
2
+
3
+ Optimize your Claude Code rate limit window by pre-warming sessions based on your usage patterns.
4
+
5
+ ## The Problem
6
+
7
+ Claude Code's rate limits reset 5 hours after your **first message**. If you start working at 9:00 and work for 4 hours, your limit resets at 14:00 - right when you might need it.
8
+
9
+ ## The Solution
10
+
11
+ ccwarm analyzes your usage history to find when you typically work. It then sends a "hi" message before you start, so your rate limit resets when you actually need it.
12
+
13
+ **Example:** You work 9:00-13:30 → ccwarm sends "hi" at 7:15 → limit resets at 12:15 instead of 14:00
14
+
15
+ ## Usage
16
+
17
+ ```bash
18
+ npx ccwarm
19
+ ```
20
+
21
+ Or install globally:
22
+
23
+ ```bash
24
+ npm install -g ccwarm
25
+ ```
26
+
27
+ ## Usage
28
+
29
+ ```bash
30
+ ccwarm analyze [days] # Analyze usage patterns (default: 21 days)
31
+ ccwarm warmup # Run warmup check now
32
+ ccwarm start # Start background daemon
33
+ ccwarm stop # Stop daemon
34
+ ccwarm restart # Restart daemon
35
+ ccwarm status # Show status and plan
36
+ ccwarm logs # Follow daemon logs
37
+ ```
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from 'node:child_process';
3
+ import { readFile } from 'node:fs/promises';
4
+ import { consola } from 'consola';
5
+ import { createWarmupPlan } from './src/analyzer.js';
6
+ import { paths } from './src/config.js';
7
+ import { getDaemonStatus, startDaemon, stopDaemon } from './src/daemon/manager.js';
8
+ import { runWarmupWorker } from './src/worker.js';
9
+ const cmd = process.argv[2] ?? '';
10
+ const commands = {
11
+ async analyze() {
12
+ const days = parseInt(process.argv[3] ?? '21', 10);
13
+ await createWarmupPlan(days);
14
+ },
15
+ async warmup() {
16
+ await runWarmupWorker();
17
+ },
18
+ async start() {
19
+ await startDaemon();
20
+ },
21
+ async stop() {
22
+ await stopDaemon();
23
+ },
24
+ async restart() {
25
+ await stopDaemon();
26
+ await new Promise((r) => setTimeout(r, 500));
27
+ await startDaemon();
28
+ },
29
+ async status() {
30
+ const daemon = await getDaemonStatus();
31
+ let plan = null;
32
+ try {
33
+ plan = JSON.parse(await readFile(paths.plan, 'utf-8'));
34
+ }
35
+ catch { }
36
+ consola.box({
37
+ title: 'ccwarm Status',
38
+ message: [
39
+ `Daemon: ${daemon.running ? `Running (PID: ${daemon.pid})` : 'Stopped'}`,
40
+ '',
41
+ plan
42
+ ? [
43
+ 'Warmup Plan:',
44
+ ` Target Hour: ${plan.targetHour}:00`,
45
+ ` Median Start: ${plan.medianStartHour}:00`,
46
+ ` Median Duration: ${plan.medianDurationHours.toFixed(2)}h`,
47
+ ` Calculated: ${plan.calculatedAt}`,
48
+ ].join('\n')
49
+ : "No warmup plan. Run 'ccwarm analyze' first.",
50
+ ].join('\n'),
51
+ });
52
+ },
53
+ async logs() {
54
+ const tail = spawn('tail', ['-f', paths.log], { stdio: 'inherit' });
55
+ tail.on('error', () => consola.error(`Cannot read: ${paths.log}`));
56
+ },
57
+ };
58
+ const run = commands[cmd];
59
+ if (run) {
60
+ await run();
61
+ }
62
+ else {
63
+ consola.box({
64
+ title: 'ccwarm - Claude Auto-Warmer',
65
+ message: [
66
+ 'Usage: ccwarm <command>',
67
+ '',
68
+ 'Commands:',
69
+ ' analyze [days] Analyze usage patterns (default: 21 days)',
70
+ ' warmup Run warmup check now',
71
+ ' start Start background daemon',
72
+ ' stop Stop background daemon',
73
+ ' restart Restart background daemon',
74
+ ' status Show daemon and plan status',
75
+ ' logs Follow daemon log output',
76
+ ].join('\n'),
77
+ });
78
+ }
@@ -0,0 +1,2 @@
1
+ import type { WarmupPlan } from './types.js';
2
+ export declare function createWarmupPlan(days?: number): Promise<WarmupPlan | null>;
@@ -0,0 +1,67 @@
1
+ import { writeFile } from 'node:fs/promises';
2
+ import { loadSessionBlockData } from 'ccusage/data-loader';
3
+ import { consola } from 'consola';
4
+ import { ensureConfigDir, paths } from './config.js';
5
+ const median = (arr) => {
6
+ if (!arr.length)
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) {
22
+ consola.info(`Analyzing Claude usage patterns (last ${days} days)...`);
23
+ try {
24
+ await ensureConfigDir();
25
+ const blocks = await loadSessionBlockData({ since: daysAgo(days), offline: true });
26
+ if (!blocks?.length) {
27
+ consola.warn('No usage blocks found');
28
+ return null;
29
+ }
30
+ // Filter out very short sessions (e.g., warmup pings) to avoid skewing analysis
31
+ const MIN_SESSION_MINUTES = 1;
32
+ const sessions = blocks
33
+ .filter((b) => !b.isGap && !!b.actualEndTime)
34
+ .map((b) => ({
35
+ startHour: new Date(b.startTime).getHours(),
36
+ durationMs: new Date(b.actualEndTime).getTime() - new Date(b.startTime).getTime(),
37
+ }))
38
+ .filter((s) => s.durationMs >= MIN_SESSION_MINUTES * 60_000)
39
+ .map((s) => ({
40
+ startHour: s.startHour,
41
+ duration: s.durationMs / 3600000,
42
+ }));
43
+ const MIN_SESSIONS = 3;
44
+ if (sessions.length < MIN_SESSIONS) {
45
+ consola.warn(`Not enough data (need at least ${MIN_SESSIONS} sessions)`);
46
+ return null;
47
+ }
48
+ const medianStart = median(sessions.map((s) => s.startHour));
49
+ const medianDuration = median(sessions.map((s) => s.duration));
50
+ let targetHour = Math.floor(medianStart - medianDuration / 2);
51
+ if (targetHour < 0)
52
+ targetHour += 24;
53
+ const plan = {
54
+ targetHour,
55
+ medianStartHour: medianStart,
56
+ medianDurationHours: medianDuration,
57
+ calculatedAt: new Date().toISOString(),
58
+ };
59
+ await writeFile(paths.plan, JSON.stringify(plan, null, 2));
60
+ consola.success(`Plan updated: Target Warmup at ${targetHour}:00`);
61
+ return plan;
62
+ }
63
+ catch (error) {
64
+ consola.error('Analysis failed:', error);
65
+ return null;
66
+ }
67
+ }
@@ -0,0 +1,11 @@
1
+ export declare const paths: {
2
+ readonly config: string;
3
+ readonly plan: string;
4
+ readonly pid: string;
5
+ readonly log: string;
6
+ };
7
+ export declare const timing: {
8
+ readonly hour: number;
9
+ readonly day: number;
10
+ };
11
+ export declare const ensureConfigDir: () => Promise<string | undefined>;
@@ -0,0 +1,15 @@
1
+ import { mkdir } from 'node:fs/promises';
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ const home = homedir();
5
+ export const paths = {
6
+ config: join(home, '.ccwarm'),
7
+ plan: join(home, '.ccwarm', 'warmup_plan.json'),
8
+ pid: join(home, '.ccwarm', 'daemon.pid'),
9
+ log: join(home, '.ccwarm', 'daemon.log'),
10
+ };
11
+ export const timing = {
12
+ hour: 60 * 60 * 1000,
13
+ day: 24 * 60 * 60 * 1000,
14
+ };
15
+ export const ensureConfigDir = () => mkdir(paths.config, { recursive: true });
@@ -0,0 +1,8 @@
1
+ export interface DaemonStatus {
2
+ running: boolean;
3
+ pid?: number;
4
+ }
5
+ export declare function getDaemonStatus(): Promise<DaemonStatus>;
6
+ export declare function startDaemon(): Promise<boolean>;
7
+ export declare function stopDaemon(): Promise<boolean>;
8
+ export declare function getDaemonLogs(): Promise<void>;
@@ -0,0 +1,70 @@
1
+ import { spawn, spawnSync } from 'node:child_process';
2
+ import { dirname, join } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { consola } from 'consola';
5
+ const __dirname = dirname(fileURLToPath(import.meta.url));
6
+ const APP_NAME = 'ccwarm';
7
+ const pm2 = (args) => new Promise((resolve) => {
8
+ const p = spawn('npx', ['pm2', ...args], { stdio: ['ignore', 'pipe', 'pipe'] });
9
+ let stdout = '';
10
+ p.stdout?.on('data', (d) => {
11
+ stdout += d;
12
+ });
13
+ p.stderr?.on('data', (d) => {
14
+ stdout += d;
15
+ });
16
+ p.on('close', (code) => resolve({ code: code ?? 1, stdout }));
17
+ });
18
+ export async function getDaemonStatus() {
19
+ const { stdout } = await pm2(['jlist']);
20
+ try {
21
+ const list = JSON.parse(stdout);
22
+ const app = list.find((p) => p.name === APP_NAME);
23
+ if (app && app.pm2_env?.status === 'online') {
24
+ return { running: true, pid: app.pid };
25
+ }
26
+ }
27
+ catch { }
28
+ return { running: false };
29
+ }
30
+ export async function startDaemon() {
31
+ const status = await getDaemonStatus();
32
+ if (status.running) {
33
+ consola.warn(`Daemon already running (PID: ${status.pid})`);
34
+ return false;
35
+ }
36
+ const runnerPath = join(__dirname, 'runner.js');
37
+ const { code } = await pm2([
38
+ 'start',
39
+ runnerPath,
40
+ '--name',
41
+ APP_NAME,
42
+ '--interpreter',
43
+ 'node',
44
+ '--no-autorestart',
45
+ ]);
46
+ if (code === 0) {
47
+ const newStatus = await getDaemonStatus();
48
+ consola.success(`Daemon started (PID: ${newStatus.pid})`);
49
+ return true;
50
+ }
51
+ consola.error('Failed to start daemon');
52
+ return false;
53
+ }
54
+ export async function stopDaemon() {
55
+ const status = await getDaemonStatus();
56
+ if (!status.running) {
57
+ consola.warn('Daemon is not running');
58
+ return false;
59
+ }
60
+ const { code } = await pm2(['delete', APP_NAME]);
61
+ if (code === 0) {
62
+ consola.success(`Daemon stopped (PID: ${status.pid})`);
63
+ return true;
64
+ }
65
+ consola.error('Failed to stop daemon');
66
+ return false;
67
+ }
68
+ export async function getDaemonLogs() {
69
+ spawnSync('npx', ['pm2', 'logs', APP_NAME, '--lines', '50'], { stdio: 'inherit' });
70
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,18 @@
1
+ import { createWarmupPlan } from '../analyzer.js';
2
+ import { timing } from '../config.js';
3
+ import { runWarmupWorker } from '../worker.js';
4
+ const log = (msg) => console.log(`[${new Date().toISOString()}] ${msg}`);
5
+ log(`Daemon started (PID: ${process.pid})`);
6
+ // Initial run
7
+ await createWarmupPlan();
8
+ await runWarmupWorker();
9
+ // Schedule recurring tasks
10
+ setInterval(async () => {
11
+ log('Running daily analysis...');
12
+ await createWarmupPlan();
13
+ }, timing.day);
14
+ setInterval(async () => {
15
+ log('Running hourly warmup check...');
16
+ await runWarmupWorker();
17
+ }, timing.hour);
18
+ log('Daemon running. Analysis: every 24h, Warmup check: every 1h');
@@ -0,0 +1,6 @@
1
+ export interface WarmupPlan {
2
+ targetHour: number;
3
+ medianStartHour: number;
4
+ medianDurationHours: number;
5
+ calculatedAt: string;
6
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export declare function runWarmupWorker(): Promise<boolean>;
@@ -0,0 +1,34 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { loadSessionBlockData } from 'ccusage/data-loader';
3
+ import { consola } from 'consola';
4
+ import { execa } from 'execa';
5
+ import { paths } from './config.js';
6
+ export async function runWarmupWorker() {
7
+ try {
8
+ const plan = JSON.parse(await readFile(paths.plan, 'utf-8'));
9
+ const currentHour = new Date().getHours();
10
+ if (currentHour !== plan.targetHour) {
11
+ consola.debug(`Not warmup time. Current: ${currentHour}:00, Target: ${plan.targetHour}:00`);
12
+ return false;
13
+ }
14
+ consola.info('Target hour matched. Checking for active sessions...');
15
+ const blocks = await loadSessionBlockData();
16
+ if (blocks?.some((b) => b.isActive)) {
17
+ consola.info('Session already active. Skipping warmup.');
18
+ return false;
19
+ }
20
+ consola.start('Warming up Claude...');
21
+ await execa('claude', ['-c', 'say hi'], { stdio: 'inherit', timeout: 60_000 });
22
+ consola.success('Warmup complete!');
23
+ return true;
24
+ }
25
+ catch (err) {
26
+ if (err.code === 'ENOENT') {
27
+ consola.warn("No warmup plan found. Run 'ccwarm analyze' first.");
28
+ }
29
+ else {
30
+ consola.error('Warmup failed:', err);
31
+ }
32
+ return false;
33
+ }
34
+ }
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "ccwarm-temp",
3
+ "version": "1.0.0",
4
+ "description": "Claude Auto-Warmer - Automatically warm up Claude based on your usage patterns",
5
+ "type": "module",
6
+ "bin": {
7
+ "ccwarm": "./dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "scripts": {
13
+ "build": "tsc",
14
+ "prepublishOnly": "npm run build",
15
+ "format": "biome format --write .",
16
+ "format:check": "biome check .",
17
+ "lint": "biome lint .",
18
+ "lint:fix": "biome lint --write ."
19
+ },
20
+ "keywords": [
21
+ "claude",
22
+ "anthropic",
23
+ "warmup",
24
+ "cli"
25
+ ],
26
+ "author": "",
27
+ "engines": {
28
+ "node": ">=18"
29
+ },
30
+ "devDependencies": {
31
+ "@biomejs/biome": "^2.3.11",
32
+ "@types/node": "^25.0.3",
33
+ "typescript": "^5.9.3"
34
+ },
35
+ "dependencies": {
36
+ "ccusage": "^17.2.1",
37
+ "consola": "^3.4.2",
38
+ "execa": "^9.6.1",
39
+ "pm2": "^6.0.14"
40
+ }
41
+ }