claude-resume-hub 1.2.0 → 1.4.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 +8 -2
- package/bin/cli.js +72 -9
- package/lib/dashboard.js +6 -0
- package/lib/detect.js +1 -2
- package/lib/sessions.js +59 -2
- package/lib/tray.js +106 -0
- package/package.json +8 -3
package/README.md
CHANGED
|
@@ -52,14 +52,17 @@ That's it. No install, no config. It works on **Windows, macOS and Linux**.
|
|
|
52
52
|
```bash
|
|
53
53
|
npx claude-resume-hub # no dashboard, just auto-resume
|
|
54
54
|
npx claude-resume-hub --web # with dashboard + alerts
|
|
55
|
-
npx claude-resume-hub
|
|
55
|
+
npx claude-resume-hub --tray # background system-tray icon (Windows)
|
|
56
|
+
npx claude-resume-hub -t "run all tests and fix failures" # start a NEW session (not resume)
|
|
56
57
|
npx claude-resume-hub --list # list this project's sessions (+ their ids)
|
|
58
|
+
npx claude-resume-hub --web --smart # context-aware resume (picks up your last step)
|
|
57
59
|
npx claude-resume-hub -s <id> # resume a specific session
|
|
58
60
|
npx claude-resume-hub --help # all options
|
|
59
61
|
```
|
|
60
62
|
|
|
61
63
|
Prefer a permanent command? `npm i -g claude-resume-hub` gives you `crh --web`.
|
|
62
64
|
**Always up to date:** `npx claude-resume-hub@latest` pulls the newest version automatically.
|
|
65
|
+
**No Node?** Grab a standalone **.exe** from the [latest release](https://github.com/IbrahimKalemci/claude-resume-hub/releases) (Windows/macOS/Linux). It still needs the `claude` CLI on your PATH.
|
|
63
66
|
|
|
64
67
|
---
|
|
65
68
|
|
|
@@ -94,14 +97,17 @@ Hepsi bu. Kurulum yok, ayar yok. **Windows, macOS ve Linux**'ta çalışır.
|
|
|
94
97
|
```bash
|
|
95
98
|
npx claude-resume-hub # panelsiz, sadece otomatik devam
|
|
96
99
|
npx claude-resume-hub --web # panel + bildirim
|
|
97
|
-
npx claude-resume-hub
|
|
100
|
+
npx claude-resume-hub --tray # arka planda sistem tepsisi ikonu (Windows)
|
|
101
|
+
npx claude-resume-hub -t "tüm testleri çalıştır ve hataları düzelt" # YENİ session başlat (devam DEĞİL)
|
|
98
102
|
npx claude-resume-hub --list # bu projenin session'larını listele (+ id'leri)
|
|
103
|
+
npx claude-resume-hub --web --smart # bağlam-farkında devam (kaldığın adımdan sürer)
|
|
99
104
|
npx claude-resume-hub -s <id> # belirli bir session'ı devam ettir
|
|
100
105
|
npx claude-resume-hub --help # tüm seçenekler
|
|
101
106
|
```
|
|
102
107
|
|
|
103
108
|
Kalıcı komut ister misin? `npm i -g claude-resume-hub` sana `crh --web` verir.
|
|
104
109
|
**Hep güncel:** `npx claude-resume-hub@latest` otomatik en yeni sürümü çeker.
|
|
110
|
+
**Node yok mu?** [Son sürümden](https://github.com/IbrahimKalemci/claude-resume-hub/releases) standalone **.exe** indir (Windows/macOS/Linux). Yine de `claude` CLI'ın PATH'te olması gerekir.
|
|
105
111
|
|
|
106
112
|
---
|
|
107
113
|
|
package/bin/cli.js
CHANGED
|
@@ -23,7 +23,9 @@ function parseArgs(argv) {
|
|
|
23
23
|
maxCycles: 100,
|
|
24
24
|
verbose: false,
|
|
25
25
|
list: false,
|
|
26
|
+
smart: false,
|
|
26
27
|
web: false,
|
|
28
|
+
tray: false,
|
|
27
29
|
port: 4177,
|
|
28
30
|
open: true,
|
|
29
31
|
passthrough: [],
|
|
@@ -44,7 +46,9 @@ function parseArgs(argv) {
|
|
|
44
46
|
case "--poll": opts.poll = parseInt(next(), 10); break;
|
|
45
47
|
case "-m": case "--max-cycles": opts.maxCycles = parseInt(next(), 10); break;
|
|
46
48
|
case "-l": case "--list": opts.list = true; break;
|
|
49
|
+
case "--smart": opts.smart = true; break;
|
|
47
50
|
case "-w": case "--web": opts.web = true; break;
|
|
51
|
+
case "--tray": opts.tray = true; opts.web = true; break;
|
|
48
52
|
case "--port": opts.port = parseInt(next(), 10); break;
|
|
49
53
|
case "--no-open": opts.open = false; break;
|
|
50
54
|
case "--verbose": opts.verbose = true; break;
|
|
@@ -69,8 +73,9 @@ USAGE
|
|
|
69
73
|
|
|
70
74
|
OPTIONS
|
|
71
75
|
-p, --prompt <text> Message sent to continue after a reset (default: "continue")
|
|
72
|
-
-t, --task <text>
|
|
73
|
-
|
|
76
|
+
-t, --task <text> Start a NEW session with this task (NOT for resuming an
|
|
77
|
+
existing one). To continue your stopped session, omit -t
|
|
78
|
+
and use plain run / --smart / --session instead.
|
|
74
79
|
-s, --session <id> Resume a specific session id (instead of the most recent).
|
|
75
80
|
Find ids by running: claude --resume
|
|
76
81
|
-d, --dir <path> Working directory / project (default: current dir)
|
|
@@ -78,7 +83,12 @@ OPTIONS
|
|
|
78
83
|
--poll <minutes> Retry interval if a reset time can't be determined (default: 5)
|
|
79
84
|
-m, --max-cycles <n> Max limit->wait->continue cycles (default: 100)
|
|
80
85
|
-l, --list List Claude Code sessions in this project and exit
|
|
86
|
+
--smart Context-aware resume: reads the session's last step and
|
|
87
|
+
nudges Claude to pick up exactly there (instead of a bare
|
|
88
|
+
"continue"). Reads the transcript locally; no AI/network.
|
|
81
89
|
-w, --web Open a live dashboard (countdown + desktop alerts)
|
|
90
|
+
--tray Run a system-tray icon (Windows; implies --web). On
|
|
91
|
+
macOS/Linux this falls back to the browser dashboard.
|
|
82
92
|
--port <n> Dashboard port (default: 4177)
|
|
83
93
|
--no-open Don't auto-open the browser for the dashboard
|
|
84
94
|
--verbose Print detection diagnostics
|
|
@@ -88,7 +98,9 @@ OPTIONS
|
|
|
88
98
|
EXAMPLES
|
|
89
99
|
claude-resume-hub # keep the latest session going across resets
|
|
90
100
|
claude-resume-hub --web # ...with a live dashboard + desktop alerts
|
|
101
|
+
claude-resume-hub --tray # background system-tray icon (Windows)
|
|
91
102
|
claude-resume-hub --list # see this project's sessions and their ids
|
|
103
|
+
claude-resume-hub --web --smart # context-aware resume (picks up your last step)
|
|
92
104
|
claude-resume-hub -s <id> # resume a specific session id
|
|
93
105
|
claude-resume-hub -t "run all the tests and fix failures"
|
|
94
106
|
claude-resume-hub -- --model opus # forward flags to claude
|
|
@@ -128,7 +140,47 @@ async function main() {
|
|
|
128
140
|
process.exit(0);
|
|
129
141
|
}
|
|
130
142
|
|
|
143
|
+
// --task starts a fresh session; warn so people don't use it expecting a resume.
|
|
144
|
+
if (opts.task) {
|
|
145
|
+
console.log(`${C.yellow}[auto-resume] Note: --task starts a NEW session with this task — it does NOT resume an existing one.${C.reset}`);
|
|
146
|
+
console.log(`${C.dim} To continue your existing (stopped) session instead, run without -t: claude-resume-hub --smart (or pick one with --list / --session)${C.reset}`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Pin the target session up front so every cycle resumes the SAME conversation.
|
|
150
|
+
// Plain "-c" means "most recent by mtime" and can silently switch to the wrong
|
|
151
|
+
// session (the tool's own prior run, or a second terminal). Resolving + pinning
|
|
152
|
+
// a concrete id, and printing it, makes resume predictable.
|
|
153
|
+
if (!opts.session && !opts.task) {
|
|
154
|
+
const { pickActiveSession } = require("../lib/sessions");
|
|
155
|
+
const active = pickActiveSession(opts.dir);
|
|
156
|
+
if (active) {
|
|
157
|
+
opts.session = active.id;
|
|
158
|
+
console.log(`${C.cyan}[auto-resume]${C.reset} Resuming session ${C.green}${active.id}${C.reset}`);
|
|
159
|
+
console.log(`${C.dim} last activity ${active.mtime.toLocaleString()} · ${active.turns} prompts${active.preview ? ` · "${active.preview.slice(0, 48)}…"` : ""}${C.reset}`);
|
|
160
|
+
console.log(`${C.dim} not this one? → claude-resume-hub --list then --session <id>${C.reset}`);
|
|
161
|
+
} else {
|
|
162
|
+
console.log(`${C.dim}[auto-resume] No prior session found here — will use "claude -c" (most recent).${C.reset}`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// --smart: build a context-aware resume prompt from the session's last step.
|
|
167
|
+
if (opts.smart && !opts.task) {
|
|
168
|
+
const { sessionRecap } = require("../lib/sessions");
|
|
169
|
+
const recap = sessionRecap(opts.dir);
|
|
170
|
+
if (recap) {
|
|
171
|
+
opts.prompt =
|
|
172
|
+
`Continue where you left off and finish the task you were working on. ` +
|
|
173
|
+
`If it is already complete, say so instead of inventing new work. ` +
|
|
174
|
+
`For context, your last message was: "${recap.slice(0, 300)}${recap.length > 300 ? "…" : ""}"`;
|
|
175
|
+
console.log(`${C.dim}[auto-resume] smart mode: resuming with context from your last step.${C.reset}`);
|
|
176
|
+
} else {
|
|
177
|
+
console.log(`${C.dim}[auto-resume] smart mode: no prior session found — using plain "continue".${C.reset}`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
131
181
|
const engine = new AutoResumeEngine(opts);
|
|
182
|
+
let trayChild = null;
|
|
183
|
+
const cleanup = () => { if (trayChild) { try { trayChild.kill(); } catch {} trayChild = null; } };
|
|
132
184
|
|
|
133
185
|
// Colorize a few known log lines for the terminal.
|
|
134
186
|
engine.on("log", (line) => {
|
|
@@ -141,17 +193,28 @@ async function main() {
|
|
|
141
193
|
engine.on("output", (chunk) => process.stdout.write(chunk));
|
|
142
194
|
|
|
143
195
|
if (opts.web) {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
196
|
+
// When running the tray, don't also pop a browser tab — the tray is the UI.
|
|
197
|
+
const { url, error } = await startDashboard(engine, { port: opts.port, open: opts.open && !opts.tray });
|
|
198
|
+
if (url) {
|
|
199
|
+
console.log(`${C.green}[auto-resume]${C.reset} Dashboard: ${url} (leave it open for desktop alerts)`);
|
|
200
|
+
if (opts.tray) {
|
|
201
|
+
const { startTray } = require("../lib/tray");
|
|
202
|
+
const t = startTray(url);
|
|
203
|
+
if (t.ok) { trayChild = t.child; console.log(`${C.green}[auto-resume]${C.reset} Tray icon started — check your system tray; right-click for options.`); }
|
|
204
|
+
else console.log(`${C.yellow}[auto-resume]${C.reset} Tray unavailable (${t.reason}); the dashboard is still at ${url}`);
|
|
205
|
+
}
|
|
206
|
+
} else {
|
|
207
|
+
console.log(`${C.red}[auto-resume]${C.reset} Could not start dashboard: ${error && error.message}`);
|
|
208
|
+
}
|
|
147
209
|
}
|
|
148
210
|
|
|
149
|
-
process.on("SIGINT", () => { console.log("\n[auto-resume] Interrupted. Bye."); engine.stop(); process.exit(130); });
|
|
211
|
+
process.on("SIGINT", () => { console.log("\n[auto-resume] Interrupted. Bye."); cleanup(); engine.stop(); process.exit(130); });
|
|
150
212
|
|
|
151
213
|
const result = await engine.run();
|
|
152
|
-
// Keep the process (and dashboard) alive briefly so the browser sees the final
|
|
153
|
-
|
|
154
|
-
|
|
214
|
+
// Keep the process (and dashboard) alive briefly so the browser sees the final
|
|
215
|
+
// state; the tray self-exits once /status stops responding, but kill it too.
|
|
216
|
+
if (opts.web) setTimeout(() => { cleanup(); process.exit(result.code); }, 1500);
|
|
217
|
+
else { cleanup(); process.exit(result.code); }
|
|
155
218
|
}
|
|
156
219
|
|
|
157
220
|
main().catch((err) => { console.error("[auto-resume] Fatal:", err); process.exit(1); });
|
package/lib/dashboard.js
CHANGED
|
@@ -43,6 +43,12 @@ function startDashboard(engine, { port = 4177, open = true } = {}) {
|
|
|
43
43
|
req.on("close", () => clients.delete(res));
|
|
44
44
|
return;
|
|
45
45
|
}
|
|
46
|
+
if (req.url === "/status") {
|
|
47
|
+
// Lightweight JSON snapshot for pollers (e.g. the --tray shim).
|
|
48
|
+
res.writeHead(200, { "Content-Type": "application/json", "Cache-Control": "no-cache" });
|
|
49
|
+
res.end(JSON.stringify(engine.state));
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
46
52
|
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
47
53
|
res.end(PAGE);
|
|
48
54
|
});
|
package/lib/detect.js
CHANGED
|
@@ -36,7 +36,7 @@ function detectLimit(text) {
|
|
|
36
36
|
* "resets Mon 12:00am". Best-effort fallback; the epoch marker is authoritative.
|
|
37
37
|
* Returns a Date (local) or null.
|
|
38
38
|
*/
|
|
39
|
-
function parseClockTime(text) {
|
|
39
|
+
function parseClockTime(text, now = new Date()) {
|
|
40
40
|
const m = text.match(
|
|
41
41
|
/reset(?:s|\s+at)?\s+(?:(Mon|Tue|Wed|Thu|Fri|Sat|Sun)\s+)?(\d{1,2})(?::(\d{2}))?\s*(am|pm)/i
|
|
42
42
|
);
|
|
@@ -49,7 +49,6 @@ function parseClockTime(text) {
|
|
|
49
49
|
if (ap === "pm" && h < 12) h += 12;
|
|
50
50
|
if (ap === "am" && h === 12) h = 0;
|
|
51
51
|
|
|
52
|
-
const now = new Date();
|
|
53
52
|
const target = new Date(now);
|
|
54
53
|
target.setHours(h, min, 0, 0);
|
|
55
54
|
|
package/lib/sessions.js
CHANGED
|
@@ -64,7 +64,12 @@ function listSessions(dir) {
|
|
|
64
64
|
} catch {
|
|
65
65
|
st = { mtime: new Date(0), size: 0 };
|
|
66
66
|
}
|
|
67
|
-
|
|
67
|
+
// Count real human prompts (a "user" line that isn't a tool_result echo),
|
|
68
|
+
// not every message line — otherwise tool traffic inflates the number.
|
|
69
|
+
const turns = lines.reduce(
|
|
70
|
+
(n, l) => n + (/"type":"user"/.test(l) && !/"tool_result"/.test(l) ? 1 : 0),
|
|
71
|
+
0
|
|
72
|
+
);
|
|
68
73
|
return {
|
|
69
74
|
id: f.replace(/\.jsonl$/, ""),
|
|
70
75
|
mtime: st.mtime,
|
|
@@ -78,4 +83,56 @@ function listSessions(dir) {
|
|
|
78
83
|
return out;
|
|
79
84
|
}
|
|
80
85
|
|
|
81
|
-
|
|
86
|
+
/** Last non-empty assistant text in a transcript — used as a resume recap. */
|
|
87
|
+
function lastAssistantText(lines) {
|
|
88
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
89
|
+
try {
|
|
90
|
+
const o = JSON.parse(lines[i]);
|
|
91
|
+
if (o.type === "assistant" && o.message) {
|
|
92
|
+
let c = o.message.content;
|
|
93
|
+
if (Array.isArray(c)) c = c.filter((x) => x && x.type === "text").map((x) => x.text).join(" ");
|
|
94
|
+
if (typeof c === "string" && c.trim()) return c.trim().replace(/\s+/g, " ");
|
|
95
|
+
}
|
|
96
|
+
} catch { /* ignore malformed lines */ }
|
|
97
|
+
}
|
|
98
|
+
return "";
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Recap of the most-recent session in `dir`: the last thing the assistant said.
|
|
103
|
+
* Used by --smart to build a context-aware resume prompt. Returns "" if none.
|
|
104
|
+
*/
|
|
105
|
+
function sessionRecap(dir) {
|
|
106
|
+
const folder = findProjectFolder(dir);
|
|
107
|
+
if (!folder) return "";
|
|
108
|
+
let files = [];
|
|
109
|
+
try { files = fs.readdirSync(folder).filter((f) => f.endsWith(".jsonl")); } catch { return ""; }
|
|
110
|
+
let newest = null, newestM = -1;
|
|
111
|
+
for (const f of files) {
|
|
112
|
+
try {
|
|
113
|
+
const m = fs.statSync(path.join(folder, f)).mtimeMs;
|
|
114
|
+
if (m > newestM) { newestM = m; newest = f; }
|
|
115
|
+
} catch { /* skip */ }
|
|
116
|
+
}
|
|
117
|
+
if (!newest) return "";
|
|
118
|
+
try {
|
|
119
|
+
const lines = fs.readFileSync(path.join(folder, newest), "utf8").split("\n").filter(Boolean);
|
|
120
|
+
return lastAssistantText(lines);
|
|
121
|
+
} catch { return ""; }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* The session to resume by default in `dir`: the most recently active one.
|
|
126
|
+
* Returned object is the same shape as listSessions() entries, or null.
|
|
127
|
+
* The engine PINS this id so later cycles don't race "most recent" (which can
|
|
128
|
+
* change if the tool's own run, or a second terminal, touches another session).
|
|
129
|
+
*/
|
|
130
|
+
function pickActiveSession(dir) {
|
|
131
|
+
const all = listSessions(dir);
|
|
132
|
+
return all.length ? all[0] : null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
module.exports = {
|
|
136
|
+
projectsRoot, encodeDir, findProjectFolder, listSessions,
|
|
137
|
+
lastAssistantText, sessionRecap, pickActiveSession,
|
|
138
|
+
};
|
package/lib/tray.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { spawn } = require("child_process");
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
const os = require("os");
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A genuine Windows system-tray icon with ZERO bundled dependencies: a tiny
|
|
10
|
+
* PowerShell + System.Windows.Forms.NotifyIcon shim that rides the .NET already
|
|
11
|
+
* present on every Windows 10/11 machine. It polls the dashboard's /status
|
|
12
|
+
* endpoint, colours the tray icon by phase, and pops a balloon on phase changes.
|
|
13
|
+
* On macOS/Linux there is no zero-dep native tray in pure Node — callers should
|
|
14
|
+
* fall back to the browser dashboard.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// NOTE: written as a plain string (no backticks / no ${...}) so it survives the
|
|
18
|
+
// JS template and PowerShell parsing cleanly.
|
|
19
|
+
const PS_SCRIPT = [
|
|
20
|
+
'param([string]$Url)',
|
|
21
|
+
'Add-Type -AssemblyName System.Windows.Forms',
|
|
22
|
+
'Add-Type -AssemblyName System.Drawing',
|
|
23
|
+
'',
|
|
24
|
+
'function New-DotIcon([System.Drawing.Color]$color) {',
|
|
25
|
+
' $bmp = New-Object System.Drawing.Bitmap 16,16',
|
|
26
|
+
' $g = [System.Drawing.Graphics]::FromImage($bmp)',
|
|
27
|
+
" $g.SmoothingMode = 'AntiAlias'",
|
|
28
|
+
' $g.Clear([System.Drawing.Color]::Transparent)',
|
|
29
|
+
' $brush = New-Object System.Drawing.SolidBrush $color',
|
|
30
|
+
' $g.FillEllipse($brush, 2, 2, 12, 12)',
|
|
31
|
+
' $g.Dispose()',
|
|
32
|
+
' return [System.Drawing.Icon]::FromHandle($bmp.GetHicon())',
|
|
33
|
+
'}',
|
|
34
|
+
'',
|
|
35
|
+
'$colors = @{',
|
|
36
|
+
' starting = [System.Drawing.Color]::FromArgb(201,100,66)',
|
|
37
|
+
' running = [System.Drawing.Color]::FromArgb(201,100,66)',
|
|
38
|
+
' waiting = [System.Drawing.Color]::FromArgb(210,153,34)',
|
|
39
|
+
' done = [System.Drawing.Color]::FromArgb(63,185,80)',
|
|
40
|
+
' error = [System.Drawing.Color]::FromArgb(248,81,73)',
|
|
41
|
+
'}',
|
|
42
|
+
'',
|
|
43
|
+
'$ni = New-Object System.Windows.Forms.NotifyIcon',
|
|
44
|
+
'$ni.Icon = New-DotIcon $colors.starting',
|
|
45
|
+
'$ni.Text = "claude-resume-hub"',
|
|
46
|
+
'$ni.Visible = $true',
|
|
47
|
+
'',
|
|
48
|
+
'$menu = New-Object System.Windows.Forms.ContextMenuStrip',
|
|
49
|
+
'$open = $menu.Items.Add("Open dashboard")',
|
|
50
|
+
'$open.add_Click({ Start-Process $Url })',
|
|
51
|
+
'$quit = $menu.Items.Add("Quit tray")',
|
|
52
|
+
'$quit.add_Click({ $ni.Visible = $false; $ni.Dispose(); [System.Windows.Forms.Application]::Exit() })',
|
|
53
|
+
'$ni.ContextMenuStrip = $menu',
|
|
54
|
+
'$ni.add_MouseClick({ param($s,$e) if ($e.Button -eq [System.Windows.Forms.MouseButtons]::Left) { Start-Process $Url } })',
|
|
55
|
+
'',
|
|
56
|
+
'$script:last = ""',
|
|
57
|
+
'$script:fails = 0',
|
|
58
|
+
'$timer = New-Object System.Windows.Forms.Timer',
|
|
59
|
+
'$timer.Interval = 2000',
|
|
60
|
+
'$timer.add_Tick({',
|
|
61
|
+
' try {',
|
|
62
|
+
' $s = Invoke-RestMethod -Uri ($Url + "/status") -TimeoutSec 3',
|
|
63
|
+
' $script:fails = 0',
|
|
64
|
+
' $phase = [string]$s.phase',
|
|
65
|
+
' $msg = [string]$s.message',
|
|
66
|
+
' if ($phase -ne $script:last) {',
|
|
67
|
+
' if ($colors.ContainsKey($phase)) { $ni.Icon = New-DotIcon $colors[$phase] }',
|
|
68
|
+
' $t = "claude-resume-hub - " + $phase',
|
|
69
|
+
' if ($t.Length -gt 63) { $t = $t.Substring(0,63) }',
|
|
70
|
+
' $ni.Text = $t',
|
|
71
|
+
' if ($script:last -ne "") { $ni.ShowBalloonTip(4000, "claude-resume-hub", $msg, [System.Windows.Forms.ToolTipIcon]::Info) }',
|
|
72
|
+
' $script:last = $phase',
|
|
73
|
+
' }',
|
|
74
|
+
' } catch {',
|
|
75
|
+
' $script:fails = $script:fails + 1',
|
|
76
|
+
' if ($script:fails -ge 6) { $ni.Visible = $false; $ni.Dispose(); [System.Windows.Forms.Application]::Exit() }',
|
|
77
|
+
' }',
|
|
78
|
+
'})',
|
|
79
|
+
'$timer.Start()',
|
|
80
|
+
'',
|
|
81
|
+
'[System.Windows.Forms.Application]::Run((New-Object System.Windows.Forms.ApplicationContext))',
|
|
82
|
+
'',
|
|
83
|
+
].join("\n");
|
|
84
|
+
|
|
85
|
+
function startTray(url) {
|
|
86
|
+
if (process.platform !== "win32") {
|
|
87
|
+
return { ok: false, reason: "native tray is Windows-only — use the --web dashboard on macOS/Linux" };
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
const file = path.join(os.tmpdir(), `crh-tray-${process.pid}.ps1`);
|
|
91
|
+
fs.writeFileSync(file, PS_SCRIPT, "utf8");
|
|
92
|
+
// -STA is REQUIRED: WinForms' message loop / Timer won't pump in MTA.
|
|
93
|
+
// Do NOT use detached:true — a detached process's message pump doesn't run,
|
|
94
|
+
// so the tray never updates. windowsHide hides the console window instead.
|
|
95
|
+
const child = spawn(
|
|
96
|
+
"powershell",
|
|
97
|
+
["-NoProfile", "-STA", "-WindowStyle", "Hidden", "-ExecutionPolicy", "Bypass", "-File", file, url],
|
|
98
|
+
{ stdio: "ignore", windowsHide: true }
|
|
99
|
+
);
|
|
100
|
+
return { ok: true, file, pid: child.pid, child };
|
|
101
|
+
} catch (e) {
|
|
102
|
+
return { ok: false, reason: e.message };
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
module.exports = { startTray, PS_SCRIPT };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-resume-hub",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.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",
|
|
@@ -18,7 +18,9 @@
|
|
|
18
18
|
],
|
|
19
19
|
"scripts": {
|
|
20
20
|
"start": "node bin/cli.js",
|
|
21
|
-
"test": "node --test"
|
|
21
|
+
"test": "node --test",
|
|
22
|
+
"build:bundle": "esbuild bin/cli.js --bundle --platform=node --target=node20 --format=cjs --outfile=dist/bundle.cjs",
|
|
23
|
+
"build:exe": "node scripts/build-exe.js"
|
|
22
24
|
},
|
|
23
25
|
"keywords": [
|
|
24
26
|
"claude",
|
|
@@ -41,5 +43,8 @@
|
|
|
41
43
|
"bugs": {
|
|
42
44
|
"url": "https://github.com/IbrahimKalemci/claude-resume-hub/issues"
|
|
43
45
|
},
|
|
44
|
-
"homepage": "https://github.com/IbrahimKalemci/claude-resume-hub#readme"
|
|
46
|
+
"homepage": "https://github.com/IbrahimKalemci/claude-resume-hub#readme",
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"esbuild": "^0.28.1"
|
|
49
|
+
}
|
|
45
50
|
}
|