claude-resume-hub 1.0.0 → 1.2.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 +20 -1
- package/bin/cli.js +37 -0
- package/lib/engine.js +20 -5
- package/lib/sessions.js +81 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,13 +7,14 @@
|
|
|
7
7
|
|
|
8
8
|
<p align="center">
|
|
9
9
|
<a href="https://www.npmjs.com/package/claude-resume-hub"><img src="https://img.shields.io/npm/v/claude-resume-hub?color=c96442" alt="npm"></a>
|
|
10
|
+
<a href="https://github.com/IbrahimKalemci/claude-resume-hub/actions/workflows/ci.yml"><img src="https://github.com/IbrahimKalemci/claude-resume-hub/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
|
|
10
11
|
<img src="https://img.shields.io/badge/node-%3E%3D16-3fb950" alt="node >= 16">
|
|
11
12
|
<img src="https://img.shields.io/badge/Windows%20%7C%20macOS%20%7C%20Linux-8b93a7" alt="platforms">
|
|
12
13
|
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue" alt="MIT"></a>
|
|
13
14
|
</p>
|
|
14
15
|
|
|
15
16
|
<p align="center">
|
|
16
|
-
<img src="https://raw.githubusercontent.com/IbrahimKalemci/claude-resume-hub/main/docs/
|
|
17
|
+
<img src="https://raw.githubusercontent.com/IbrahimKalemci/claude-resume-hub/main/docs/demo.svg" alt="countdown reaching zero, then sending continue to Claude to resume the task" width="700">
|
|
17
18
|
</p>
|
|
18
19
|
|
|
19
20
|
<p align="center"><b>🇬🇧 <a href="#-english">English</a> · 🇹🇷 <a href="#-türkçe">Türkçe</a></b></p>
|
|
@@ -52,10 +53,13 @@ That's it. No install, no config. It works on **Windows, macOS and Linux**.
|
|
|
52
53
|
npx claude-resume-hub # no dashboard, just auto-resume
|
|
53
54
|
npx claude-resume-hub --web # with dashboard + alerts
|
|
54
55
|
npx claude-resume-hub -t "run all tests and fix failures" # start a task
|
|
56
|
+
npx claude-resume-hub --list # list this project's sessions (+ their ids)
|
|
57
|
+
npx claude-resume-hub -s <id> # resume a specific session
|
|
55
58
|
npx claude-resume-hub --help # all options
|
|
56
59
|
```
|
|
57
60
|
|
|
58
61
|
Prefer a permanent command? `npm i -g claude-resume-hub` gives you `crh --web`.
|
|
62
|
+
**Always up to date:** `npx claude-resume-hub@latest` pulls the newest version automatically.
|
|
59
63
|
|
|
60
64
|
---
|
|
61
65
|
|
|
@@ -91,10 +95,25 @@ Hepsi bu. Kurulum yok, ayar yok. **Windows, macOS ve Linux**'ta çalışır.
|
|
|
91
95
|
npx claude-resume-hub # panelsiz, sadece otomatik devam
|
|
92
96
|
npx claude-resume-hub --web # panel + bildirim
|
|
93
97
|
npx claude-resume-hub -t "tüm testleri çalıştır ve hataları düzelt" # görev başlat
|
|
98
|
+
npx claude-resume-hub --list # bu projenin session'larını listele (+ id'leri)
|
|
99
|
+
npx claude-resume-hub -s <id> # belirli bir session'ı devam ettir
|
|
94
100
|
npx claude-resume-hub --help # tüm seçenekler
|
|
95
101
|
```
|
|
96
102
|
|
|
97
103
|
Kalıcı komut ister misin? `npm i -g claude-resume-hub` sana `crh --web` verir.
|
|
104
|
+
**Hep güncel:** `npx claude-resume-hub@latest` otomatik en yeni sürümü çeker.
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## 🛠️ From source · Kaynaktan
|
|
109
|
+
|
|
110
|
+
Prefer to run it straight from the repo (no npm)? / npm'siz, doğrudan repodan mı çalıştırmak istiyorsun?
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
git clone https://github.com/IbrahimKalemci/claude-resume-hub.git
|
|
114
|
+
cd claude-resume-hub
|
|
115
|
+
node bin/cli.js --web
|
|
116
|
+
```
|
|
98
117
|
|
|
99
118
|
---
|
|
100
119
|
|
package/bin/cli.js
CHANGED
|
@@ -16,11 +16,13 @@ function parseArgs(argv) {
|
|
|
16
16
|
const opts = {
|
|
17
17
|
prompt: "continue",
|
|
18
18
|
task: null,
|
|
19
|
+
session: null,
|
|
19
20
|
dir: process.cwd(),
|
|
20
21
|
buffer: 30,
|
|
21
22
|
poll: 5,
|
|
22
23
|
maxCycles: 100,
|
|
23
24
|
verbose: false,
|
|
25
|
+
list: false,
|
|
24
26
|
web: false,
|
|
25
27
|
port: 4177,
|
|
26
28
|
open: true,
|
|
@@ -36,10 +38,12 @@ function parseArgs(argv) {
|
|
|
36
38
|
switch (a) {
|
|
37
39
|
case "-p": case "--prompt": opts.prompt = next(); break;
|
|
38
40
|
case "-t": case "--task": opts.task = next(); break;
|
|
41
|
+
case "-s": case "--session": opts.session = next(); break;
|
|
39
42
|
case "-d": case "--dir": opts.dir = next(); break;
|
|
40
43
|
case "-b": case "--buffer": opts.buffer = parseInt(next(), 10); break;
|
|
41
44
|
case "--poll": opts.poll = parseInt(next(), 10); break;
|
|
42
45
|
case "-m": case "--max-cycles": opts.maxCycles = parseInt(next(), 10); break;
|
|
46
|
+
case "-l": case "--list": opts.list = true; break;
|
|
43
47
|
case "-w": case "--web": opts.web = true; break;
|
|
44
48
|
case "--port": opts.port = parseInt(next(), 10); break;
|
|
45
49
|
case "--no-open": opts.open = false; break;
|
|
@@ -67,10 +71,13 @@ OPTIONS
|
|
|
67
71
|
-p, --prompt <text> Message sent to continue after a reset (default: "continue")
|
|
68
72
|
-t, --task <text> Initial task; starts a fresh session on the first run,
|
|
69
73
|
then continues it. Omit to attach to the most recent session.
|
|
74
|
+
-s, --session <id> Resume a specific session id (instead of the most recent).
|
|
75
|
+
Find ids by running: claude --resume
|
|
70
76
|
-d, --dir <path> Working directory / project (default: current dir)
|
|
71
77
|
-b, --buffer <seconds> Safety margin added after the reset time (default: 30)
|
|
72
78
|
--poll <minutes> Retry interval if a reset time can't be determined (default: 5)
|
|
73
79
|
-m, --max-cycles <n> Max limit->wait->continue cycles (default: 100)
|
|
80
|
+
-l, --list List Claude Code sessions in this project and exit
|
|
74
81
|
-w, --web Open a live dashboard (countdown + desktop alerts)
|
|
75
82
|
--port <n> Dashboard port (default: 4177)
|
|
76
83
|
--no-open Don't auto-open the browser for the dashboard
|
|
@@ -81,6 +88,8 @@ OPTIONS
|
|
|
81
88
|
EXAMPLES
|
|
82
89
|
claude-resume-hub # keep the latest session going across resets
|
|
83
90
|
claude-resume-hub --web # ...with a live dashboard + desktop alerts
|
|
91
|
+
claude-resume-hub --list # see this project's sessions and their ids
|
|
92
|
+
claude-resume-hub -s <id> # resume a specific session id
|
|
84
93
|
claude-resume-hub -t "run all the tests and fix failures"
|
|
85
94
|
claude-resume-hub -- --model opus # forward flags to claude
|
|
86
95
|
`);
|
|
@@ -89,8 +98,36 @@ EXAMPLES
|
|
|
89
98
|
const C = { cyan: "\x1b[36m", dim: "\x1b[2m", green: "\x1b[32m", yellow: "\x1b[33m", red: "\x1b[31m", reset: "\x1b[0m" };
|
|
90
99
|
const stamp = () => new Date().toLocaleString();
|
|
91
100
|
|
|
101
|
+
function printSessions(dir) {
|
|
102
|
+
const { listSessions, findProjectFolder } = require("../lib/sessions");
|
|
103
|
+
if (!findProjectFolder(dir)) {
|
|
104
|
+
console.log(`No Claude Code sessions found for:\n ${dir}`);
|
|
105
|
+
console.log(`${C.dim}(Looked in ~/.claude/projects. Run this in your project folder, or pass -d <path>.)${C.reset}`);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const sessions = listSessions(dir);
|
|
109
|
+
if (!sessions.length) { console.log("No sessions in this project yet."); return; }
|
|
110
|
+
|
|
111
|
+
console.log(`\nClaude Code sessions for ${C.cyan}${dir}${C.reset} ${C.dim}(newest first)${C.reset}\n`);
|
|
112
|
+
sessions.forEach((s, i) => {
|
|
113
|
+
const dot = i === 0 ? `${C.green}●${C.reset}` : " ";
|
|
114
|
+
console.log(`${dot} ${s.id}`);
|
|
115
|
+
console.log(` ${C.dim}${s.mtime.toLocaleString()} · ${s.turns} turns · ${s.sizeKB} KB${C.reset}`);
|
|
116
|
+
if (s.preview) console.log(` ${C.dim}"${s.preview}${s.preview.length >= 80 ? "…" : ""}"${C.reset}`);
|
|
117
|
+
console.log("");
|
|
118
|
+
});
|
|
119
|
+
console.log(`${C.green}●${C.reset} ${C.dim}= what plain "continue" (-c) resumes (the most recent).${C.reset}`);
|
|
120
|
+
console.log(`${C.dim}Resume a specific one: ${C.reset}crh --session <id>`);
|
|
121
|
+
}
|
|
122
|
+
|
|
92
123
|
async function main() {
|
|
93
124
|
const opts = parseArgs(process.argv.slice(2));
|
|
125
|
+
|
|
126
|
+
if (opts.list) {
|
|
127
|
+
printSessions(opts.dir);
|
|
128
|
+
process.exit(0);
|
|
129
|
+
}
|
|
130
|
+
|
|
94
131
|
const engine = new AutoResumeEngine(opts);
|
|
95
132
|
|
|
96
133
|
// Colorize a few known log lines for the terminal.
|
package/lib/engine.js
CHANGED
|
@@ -4,6 +4,23 @@ const { spawn } = require("child_process");
|
|
|
4
4
|
const { EventEmitter } = require("events");
|
|
5
5
|
const { detectLimit, fmtDuration } = require("./detect");
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Build the argument list passed to `claude` for a given cycle.
|
|
9
|
+
* - First cycle with an initial task -> start a fresh session with that task
|
|
10
|
+
* - A specific session id (--session) -> resume exactly that session
|
|
11
|
+
* - Otherwise -> continue the most recent session (-c)
|
|
12
|
+
*/
|
|
13
|
+
function buildClaudeArgs(opts, cycle) {
|
|
14
|
+
const extra = opts.passthrough || [];
|
|
15
|
+
if (cycle === 1 && opts.task) {
|
|
16
|
+
return ["-p", opts.task, ...extra];
|
|
17
|
+
}
|
|
18
|
+
if (opts.session) {
|
|
19
|
+
return ["--resume", opts.session, "-p", opts.prompt, ...extra];
|
|
20
|
+
}
|
|
21
|
+
return ["-c", "-p", opts.prompt, ...extra];
|
|
22
|
+
}
|
|
23
|
+
|
|
7
24
|
/**
|
|
8
25
|
* AutoResumeEngine runs Claude Code in a loop, waiting out usage limits.
|
|
9
26
|
*
|
|
@@ -88,12 +105,10 @@ class AutoResumeEngine extends EventEmitter {
|
|
|
88
105
|
const o = this.opts;
|
|
89
106
|
this.log(`Starting. dir="${o.dir}" prompt="${o.prompt}"`);
|
|
90
107
|
if (o.task) this.log(`Initial task: "${o.task}"`);
|
|
108
|
+
if (o.session) this.log(`Resuming session: ${o.session}`);
|
|
91
109
|
|
|
92
110
|
for (let cycle = 1; cycle <= o.maxCycles && !this._stopped; cycle++) {
|
|
93
|
-
const claudeArgs =
|
|
94
|
-
cycle === 1 && o.task
|
|
95
|
-
? ["-p", o.task, ...o.passthrough]
|
|
96
|
-
: ["-c", "-p", o.prompt, ...o.passthrough];
|
|
111
|
+
const claudeArgs = buildClaudeArgs(o, cycle);
|
|
97
112
|
|
|
98
113
|
this._setState({ phase: "running", cycle, resetAt: null, wakeAt: null, message: "Running Claude…" });
|
|
99
114
|
this.log(`Cycle ${cycle}/${o.maxCycles} — running claude…`);
|
|
@@ -143,4 +158,4 @@ class AutoResumeEngine extends EventEmitter {
|
|
|
143
158
|
}
|
|
144
159
|
}
|
|
145
160
|
|
|
146
|
-
module.exports = { AutoResumeEngine };
|
|
161
|
+
module.exports = { AutoResumeEngine, buildClaudeArgs };
|
package/lib/sessions.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const os = require("os");
|
|
6
|
+
|
|
7
|
+
/** Root where Claude Code stores per-project session transcripts. */
|
|
8
|
+
function projectsRoot() {
|
|
9
|
+
return path.join(os.homedir(), ".claude", "projects");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Encode an absolute directory the way Claude Code names its project folders:
|
|
14
|
+
* the path with `:`, `/` and `\` all replaced by `-`.
|
|
15
|
+
* e.g. C:\Users\me\proj -> C--Users-me-proj
|
|
16
|
+
*/
|
|
17
|
+
function encodeDir(dir) {
|
|
18
|
+
return path.resolve(dir).replace(/[/\\:]/g, "-");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Locate the project folder for `dir` (exact, then case-insensitive). */
|
|
22
|
+
function findProjectFolder(dir) {
|
|
23
|
+
const root = projectsRoot();
|
|
24
|
+
if (!fs.existsSync(root)) return null;
|
|
25
|
+
const want = encodeDir(dir);
|
|
26
|
+
let entries = [];
|
|
27
|
+
try { entries = fs.readdirSync(root); } catch { return null; }
|
|
28
|
+
if (entries.includes(want)) return path.join(root, want);
|
|
29
|
+
const ci = entries.find((e) => e.toLowerCase() === want.toLowerCase());
|
|
30
|
+
return ci ? path.join(root, ci) : null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** First non-empty user message in a transcript — used as a preview. */
|
|
34
|
+
function firstUserMessage(lines) {
|
|
35
|
+
for (const l of lines) {
|
|
36
|
+
try {
|
|
37
|
+
const o = JSON.parse(l);
|
|
38
|
+
if (o.type === "user" && o.message) {
|
|
39
|
+
let c = o.message.content;
|
|
40
|
+
if (Array.isArray(c)) c = c.map((x) => (x && x.text) || "").join(" ");
|
|
41
|
+
if (typeof c === "string" && c.trim()) return c.trim().replace(/\s+/g, " ");
|
|
42
|
+
}
|
|
43
|
+
} catch { /* ignore malformed lines */ }
|
|
44
|
+
}
|
|
45
|
+
return "";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* List Claude Code sessions for a project directory, newest first.
|
|
50
|
+
* Returns [{ id, mtime, sizeKB, turns, preview }].
|
|
51
|
+
*/
|
|
52
|
+
function listSessions(dir) {
|
|
53
|
+
const folder = findProjectFolder(dir);
|
|
54
|
+
if (!folder) return [];
|
|
55
|
+
let files = [];
|
|
56
|
+
try { files = fs.readdirSync(folder).filter((f) => f.endsWith(".jsonl")); } catch { return []; }
|
|
57
|
+
|
|
58
|
+
const out = files.map((f) => {
|
|
59
|
+
const full = path.join(folder, f);
|
|
60
|
+
let st, lines = [];
|
|
61
|
+
try {
|
|
62
|
+
st = fs.statSync(full);
|
|
63
|
+
lines = fs.readFileSync(full, "utf8").split("\n").filter(Boolean);
|
|
64
|
+
} catch {
|
|
65
|
+
st = { mtime: new Date(0), size: 0 };
|
|
66
|
+
}
|
|
67
|
+
const turns = lines.reduce((n, l) => n + (/"type":"(user|assistant)"/.test(l) ? 1 : 0), 0);
|
|
68
|
+
return {
|
|
69
|
+
id: f.replace(/\.jsonl$/, ""),
|
|
70
|
+
mtime: st.mtime,
|
|
71
|
+
sizeKB: Math.round((st.size || 0) / 1024),
|
|
72
|
+
turns,
|
|
73
|
+
preview: firstUserMessage(lines).slice(0, 80),
|
|
74
|
+
};
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
out.sort((a, b) => b.mtime - a.mtime); // newest first
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
module.exports = { projectsRoot, encodeDir, findProjectFolder, listSessions };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-resume-hub",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Run Claude Code and auto-continue the moment a usage/session limit resets — with a live dashboard and desktop alerts. Cross-platform, zero-dependency.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"claude-resume-hub": "bin/cli.js",
|