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 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**. If you start working at 9:00 and work for 4 hours, your limit resets at 14:00 - right when you might need it.
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 "hi" message before you start, so your rate limit resets when you actually need it.
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
- **Example:** You work 9:00-13:30 → ccwarm sends "hi" at 7:15 → limit resets at 12:15 instead of 14:00
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: 21 days)
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] ?? '21', 10);
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 Hour: ${plan.targetHour}:00`,
45
- ` Median Start: ${plan.medianStartHour}:00`,
46
- ` Median Duration: ${plan.medianDurationHours.toFixed(2)}h`,
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
- const tail = spawn('tail', ['-f', paths.log], { stdio: 'inherit' });
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: 21 days)',
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',
@@ -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
- 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) {
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
- startHour: new Date(b.startTime).getHours(),
36
- durationMs: new Date(b.actualEndTime).getTime() - new Date(b.startTime).getTime(),
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
- startHour: s.startHour,
41
- duration: s.durationMs / 3600000,
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 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;
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
- targetHour,
55
- medianStartHour: medianStart,
56
- medianDurationHours: medianDuration,
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 ${targetHour}:00`);
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');
@@ -1,6 +1,9 @@
1
1
  export interface WarmupPlan {
2
- targetHour: number;
3
- medianStartHour: number;
4
- medianDurationHours: number;
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
+ };
@@ -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 { paths } from './config.js';
4
+ import { formatTime, loadPlan } from './utils.js';
6
5
  export async function runWarmupWorker() {
7
6
  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`);
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 hour matched. Checking for active sessions...');
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
- 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
- }
31
+ consola.error('Warmup failed:', err);
32
32
  return false;
33
33
  }
34
34
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccwarm-temp",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Claude Auto-Warmer - Automatically warm up Claude based on your usage patterns",
5
5
  "type": "module",
6
6
  "bin": {