claude-wake 0.1.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/LICENSE +21 -0
- package/README.md +204 -0
- package/bin/claude-wake.js +290 -0
- package/docs/design.md +181 -0
- package/lib/config.js +174 -0
- package/lib/detect.js +223 -0
- package/lib/doctor.js +135 -0
- package/lib/editors.js +87 -0
- package/lib/install.js +315 -0
- package/lib/log.js +43 -0
- package/lib/parse-reset.js +192 -0
- package/lib/send/darwin.js +233 -0
- package/lib/send/index.js +47 -0
- package/lib/send/linux.js +149 -0
- package/lib/send/win32.js +133 -0
- package/lib/state.js +139 -0
- package/lib/watch.js +276 -0
- package/package.json +38 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kazuki Nakayashiki
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# claude-wake
|
|
2
|
+
|
|
3
|
+
**Auto-resume Claude Code in the VS Code panel when your usage limit resets β while you sleep.**
|
|
4
|
+
|
|
5
|
+
You're deep in a task and Claude Code stops:
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
You've hit your session limit Β· resets 3:30am (America/Los_Angeles)
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
It's midnight. The limit lifts at 3:30am. Nobody's awake to type "continue" β so
|
|
12
|
+
six hours evaporate. **claude-wake** waits for the reset, re-focuses that exact
|
|
13
|
+
panel, and submits a resume message for you. You wake up to finished work.
|
|
14
|
+
|
|
15
|
+
There are two ways to run it: a **menu-bar app** or a **CLI** β same engine,
|
|
16
|
+
same `~/.claude-wake` state, so pick whichever you like.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## π Fastest setup: let Claude Code do it
|
|
21
|
+
|
|
22
|
+
Open a Claude Code session (anywhere) and paste this. It clones, installs, and
|
|
23
|
+
enables claude-wake for you, then walks you through the oneβtime permission:
|
|
24
|
+
|
|
25
|
+
```text
|
|
26
|
+
Set up "claude-wake" for me β it auto-resumes my Claude Code (VS Code panel)
|
|
27
|
+
when my usage limit resets. Steps:
|
|
28
|
+
|
|
29
|
+
1. Clone https://github.com/kazuki-sf/claude-wake into ~/GitHub/claude-wake
|
|
30
|
+
(if it already exists, cd in and `git pull` instead).
|
|
31
|
+
2. It's a zero-dependency Node CLI (needs Node 18+). Install it globally:
|
|
32
|
+
`cd ~/GitHub/claude-wake && npm install -g .`
|
|
33
|
+
If a global npm install is blocked, symlink it instead:
|
|
34
|
+
`mkdir -p ~/.local/bin && ln -sf ~/GitHub/claude-wake/bin/claude-wake.js ~/.local/bin/claude-wake && chmod +x ~/GitHub/claude-wake/bin/claude-wake.js`
|
|
35
|
+
(make sure ~/.local/bin is on PATH).
|
|
36
|
+
3. Enable auto-resume at login: `claude-wake install`.
|
|
37
|
+
4. Run `claude-wake doctor` and follow it to grant macOS Accessibility to the
|
|
38
|
+
KeySender.app it points to (one-time β required so it can paste into the panel).
|
|
39
|
+
5. Confirm end-to-end with `claude-wake test` (pastes a harmless test message
|
|
40
|
+
into my most recent Claude Code panel), then show me `claude-wake status`.
|
|
41
|
+
6. Ask me whether I also want the **menu-bar app** β a GUI dashboard with a live
|
|
42
|
+
countdown to the next resume, pause/resume, and message editing. If yes and
|
|
43
|
+
the Rust toolchain (`cargo`) is available, build it:
|
|
44
|
+
`cd app/src-tauri && cargo install tauri-cli --version '^2' --locked && cargo tauri build`,
|
|
45
|
+
then move the produced `.app` to ~/Applications and open it (it shares the
|
|
46
|
+
same daemon, so nothing double-runs). If Rust isn't installed, point me to
|
|
47
|
+
https://rustup.rs or let me skip it β the CLI already handles auto-resume.
|
|
48
|
+
If I say no, skip this step.
|
|
49
|
+
|
|
50
|
+
Don't commit anything or modify my repos β everything lives under ~/.claude-wake.
|
|
51
|
+
Then tell me it's running and that it'll resume a limited panel shortly after
|
|
52
|
+
the limit resets.
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
> What this changes on your machine: it registers a **login item** (the
|
|
56
|
+
> background watcher) and asks for a one-time macOS **Accessibility** grant to a
|
|
57
|
+
> dedicated `KeySender.app` (so it can paste into the panel). Everything lives
|
|
58
|
+
> under `~/.claude-wake`; remove it any time with `claude-wake uninstall`.
|
|
59
|
+
|
|
60
|
+
> Prefer a GUI? After the CLI is set up, the **menu-bar app** is an optional
|
|
61
|
+
> build β see [The menu-bar app](#the-menu-bar-app).
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Install it yourself
|
|
66
|
+
|
|
67
|
+
### CLI
|
|
68
|
+
|
|
69
|
+
Once published:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
npm install -g claude-wake
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
From source (today):
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
git clone https://github.com/kazuki-sf/claude-wake ~/GitHub/claude-wake
|
|
79
|
+
cd ~/GitHub/claude-wake
|
|
80
|
+
npm install -g . # zero dependencies; needs Node 18+
|
|
81
|
+
claude-wake install # start on login
|
|
82
|
+
claude-wake doctor # verify + grant Accessibility (guided)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### The menu-bar app
|
|
86
|
+
|
|
87
|
+
A small tray/menu-bar dashboard (Tauri: Rust + plain HTML, no npm frontend).
|
|
88
|
+
Build it from source β needs the Rust toolchain:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
cd ~/GitHub/claude-wake/app/src-tauri
|
|
92
|
+
cargo install tauri-cli --version '^2' --locked # once
|
|
93
|
+
cargo tauri build # β target/release/bundle/β¦
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
That produces a `.app` (macOS). It's unsigned (OSS) β first launch: right-click
|
|
97
|
+
β Open. The app bundles the engine and shares `~/.claude-wake` with the CLI, so
|
|
98
|
+
**don't run two watchers** β use the app *or* the CLI daemon, not both. See
|
|
99
|
+
[`app/README.md`](app/README.md).
|
|
100
|
+
|
|
101
|
+
### macOS: grant permission once
|
|
102
|
+
|
|
103
|
+
Pasting into the panel needs the **Accessibility** permission. claude-wake
|
|
104
|
+
compiles a tiny dedicated `KeySender.app` on your machine so you grant it to
|
|
105
|
+
**one app only** β not your terminal, not Node. `claude-wake doctor` opens the
|
|
106
|
+
right settings pane and shows exactly what to add:
|
|
107
|
+
|
|
108
|
+
> System Settings β Privacy & Security β Accessibility β add & enable
|
|
109
|
+
> `~/.claude-wake/KeySender.app`
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## How it works
|
|
114
|
+
|
|
115
|
+
1. **Detect** β Claude Code writes each session's transcript to
|
|
116
|
+
`~/.claude/projects/**/*.jsonl`. A usage limit is a structured entry whose
|
|
117
|
+
text carries the reset time **with timezone**. claude-wake tails these files
|
|
118
|
+
(cheap append-only reads) β no screen scraping.
|
|
119
|
+
2. **Focus** β at reset time it opens
|
|
120
|
+
`vscode://anthropic.claude-code/open?session=<id>`, which brings the exact
|
|
121
|
+
panel to the front, even with many panels open.
|
|
122
|
+
3. **Send** β the panel's input ignores fake keystrokes but accepts a paste, so
|
|
123
|
+
it puts the message on the clipboard and does **paste + Enter** β only after
|
|
124
|
+
confirming your editor is frontmost.
|
|
125
|
+
|
|
126
|
+
It fires **once per limit** (duplicate events are de-duplicated), and if you
|
|
127
|
+
already resumed manually, it notices and skips. The default message is
|
|
128
|
+
deliberately **safe if it fires when it didn't need to**:
|
|
129
|
+
|
|
130
|
+
> _"If you were stopped by a usage or token limit, continue from exactly where
|
|
131
|
+
> you left off. If you were not stopped, or the task is already complete, ignore
|
|
132
|
+
> this message."_
|
|
133
|
+
|
|
134
|
+
## Using it
|
|
135
|
+
|
|
136
|
+
**Menu-bar app** β click the β‘ icon to open the popover:
|
|
137
|
+
|
|
138
|
+
- Live **countdown** to the next resume, current state (Watching / Paused /
|
|
139
|
+
Not enabled), and recent activity.
|
|
140
|
+
- Edit the **resume message** (applies live), **Pause/Resume**, **Send test**,
|
|
141
|
+
**Grant permission**, **Start on login**, open logs/config, GitHub, Quit.
|
|
142
|
+
|
|
143
|
+
**CLI:**
|
|
144
|
+
|
|
145
|
+
| Command | What it does |
|
|
146
|
+
| ----------------------- | ----------------------------------------------------------------- |
|
|
147
|
+
| `claude-wake watch` | Watch and auto-resume in the foreground (`--dry-run` to log only). |
|
|
148
|
+
| `claude-wake install` | Start on login (launchd / systemd / Startup) and run now. |
|
|
149
|
+
| `claude-wake uninstall` | Stop and remove the login service. |
|
|
150
|
+
| `claude-wake status` | Daemon state + scheduled resumes (`--json` for machines). |
|
|
151
|
+
| `claude-wake pause` / `resume` | Suspend / un-suspend auto-resume. |
|
|
152
|
+
| `claude-wake doctor` | Check dependencies + permissions, with fixes. |
|
|
153
|
+
| `claude-wake test` | Fire a harmless test message into your latest session now. |
|
|
154
|
+
| `claude-wake set-message "β¦"` | Set the resume message (blank resets to default). |
|
|
155
|
+
| `claude-wake logs [-f]` | Recent log lines (`-f` to follow). |
|
|
156
|
+
|
|
157
|
+
## Configuration
|
|
158
|
+
|
|
159
|
+
Optional `~/.claude-wake/config.json` (the app edits `message` for you):
|
|
160
|
+
|
|
161
|
+
```jsonc
|
|
162
|
+
{
|
|
163
|
+
"message": "Continue from where you left off.", // resume text (one line)
|
|
164
|
+
"editor": "vscode", // vscode | vscode-insiders | cursor | windsurf
|
|
165
|
+
"marginSeconds": 60, // extra wait after the printed reset time
|
|
166
|
+
"pollSeconds": 20, // transcript scan interval
|
|
167
|
+
"focusDelayMs": 2500, // wait between opening the panel and pasting
|
|
168
|
+
"skipIfResumed": true, // skip if you already continued manually
|
|
169
|
+
"allowUnverifiedPaste": false // Wayland only β see Platform support
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Invalid values fall back to safe defaults. Config changes apply live.
|
|
174
|
+
|
|
175
|
+
## Platform support
|
|
176
|
+
|
|
177
|
+
| Platform | Status |
|
|
178
|
+
| ------------------- | --------------------------------------------------------------- |
|
|
179
|
+
| **macOS** | β
Reference β verified end-to-end. |
|
|
180
|
+
| **Linux (X11)** | π§ͺ Implemented (`xdotool` + `xclip`/`xsel`). Community-verify. |
|
|
181
|
+
| **Linux (Wayland)** | π§ͺ Experimental; blind send, needs `allowUnverifiedPaste`. |
|
|
182
|
+
| **Windows** | π§ͺ Implemented (PowerShell `SendKeys`). Community-verify. |
|
|
183
|
+
|
|
184
|
+
## Security
|
|
185
|
+
|
|
186
|
+
Zero runtime dependencies, no network, no `eval`. **The text it pastes is always
|
|
187
|
+
your static `message`** β never derived from transcript or web content, so
|
|
188
|
+
summarizing a malicious page can't make it paste anything. Keystrokes go only
|
|
189
|
+
after verifying the editor is frontmost. Full trust model in
|
|
190
|
+
[`SECURITY.md`](SECURITY.md); design in [`docs/design.md`](docs/design.md).
|
|
191
|
+
|
|
192
|
+
## Uninstall
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
claude-wake uninstall # stop + remove the login service
|
|
196
|
+
npm rm -g claude-wake # (or: rm ~/.local/bin/claude-wake)
|
|
197
|
+
rm -rf ~/.claude-wake # state, logs, KeySender.app
|
|
198
|
+
# macOS: also remove KeySender from System Settings β Accessibility
|
|
199
|
+
# app: drag claude-wake.app to the Trash
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## License
|
|
203
|
+
|
|
204
|
+
MIT Β© Kazuki Nakayashiki
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import {
|
|
6
|
+
BASE_DIR,
|
|
7
|
+
CONFIG_FILE,
|
|
8
|
+
LOG_FILE,
|
|
9
|
+
DEFAULT_MESSAGE,
|
|
10
|
+
loadConfig,
|
|
11
|
+
projectsDir,
|
|
12
|
+
isPaused,
|
|
13
|
+
setPaused,
|
|
14
|
+
sanitizeMessage,
|
|
15
|
+
readConfigRaw,
|
|
16
|
+
writeConfigRaw,
|
|
17
|
+
} from "../lib/config.js";
|
|
18
|
+
import { createLogger } from "../lib/log.js";
|
|
19
|
+
import { getEditor, isValidSessionId, EDITOR_NAMES } from "../lib/editors.js";
|
|
20
|
+
import { watchLoop } from "../lib/watch.js";
|
|
21
|
+
import { loadState } from "../lib/state.js";
|
|
22
|
+
import { listTranscripts } from "../lib/detect.js";
|
|
23
|
+
import { sendResume } from "../lib/send/index.js";
|
|
24
|
+
import {
|
|
25
|
+
installDaemon,
|
|
26
|
+
uninstallDaemon,
|
|
27
|
+
daemonStatus,
|
|
28
|
+
} from "../lib/install.js";
|
|
29
|
+
import { runDoctor } from "../lib/doctor.js";
|
|
30
|
+
|
|
31
|
+
const HELP = `claude-wake β auto-resume Claude Code (VS Code panels) when your usage limit resets
|
|
32
|
+
|
|
33
|
+
Usage: claude-wake <command> [options]
|
|
34
|
+
|
|
35
|
+
Commands
|
|
36
|
+
watch Watch transcripts and auto-resume limited panels (foreground)
|
|
37
|
+
--dry-run detect + schedule, but only log instead of sending
|
|
38
|
+
--quiet no stdout (log file only; used by the daemon)
|
|
39
|
+
install Register the watcher to start at login (launchd / systemd / Startup)
|
|
40
|
+
uninstall Remove the login registration and stop the daemon
|
|
41
|
+
status Show daemon state, pending resumes, and config summary (--json for machines)
|
|
42
|
+
pause Suspend auto-resume (watcher keeps running)
|
|
43
|
+
resume Un-suspend auto-resume
|
|
44
|
+
set-message <t> Set the resume message (blank resets to default)
|
|
45
|
+
doctor Check dependencies and permissions, with fix instructions
|
|
46
|
+
test Fire one resume into the most recent session right now
|
|
47
|
+
--session <id> target a specific session id
|
|
48
|
+
logs Print the last 40 log lines (-f to follow)
|
|
49
|
+
|
|
50
|
+
Config: ${CONFIG_FILE} (see README for keys)
|
|
51
|
+
Editors: ${EDITOR_NAMES.join(", ")} (config "editor")
|
|
52
|
+
`;
|
|
53
|
+
|
|
54
|
+
const argv = process.argv.slice(2);
|
|
55
|
+
const cmd = argv[0];
|
|
56
|
+
const flags = new Set(argv.slice(1).filter((a) => a.startsWith("-")));
|
|
57
|
+
function flagValue(name) {
|
|
58
|
+
const i = argv.indexOf(name);
|
|
59
|
+
return i !== -1 && argv[i + 1] && !argv[i + 1].startsWith("--")
|
|
60
|
+
? argv[i + 1]
|
|
61
|
+
: null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const selfPath = fileURLToPath(import.meta.url);
|
|
65
|
+
|
|
66
|
+
function pkgVersion() {
|
|
67
|
+
try {
|
|
68
|
+
return JSON.parse(
|
|
69
|
+
fs.readFileSync(
|
|
70
|
+
path.join(path.dirname(selfPath), "..", "package.json"),
|
|
71
|
+
"utf8",
|
|
72
|
+
),
|
|
73
|
+
).version;
|
|
74
|
+
} catch {
|
|
75
|
+
return "0.0.0";
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function main() {
|
|
80
|
+
switch (cmd) {
|
|
81
|
+
case "watch": {
|
|
82
|
+
const log = createLogger({ quiet: flags.has("--quiet") });
|
|
83
|
+
const cfg = loadConfig({ warn: log });
|
|
84
|
+
await watchLoop(cfg, log, { dryRun: flags.has("--dry-run") });
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
case "install": {
|
|
89
|
+
// argv[1] = the bin path the user invoked (npm's stable symlink), NOT
|
|
90
|
+
// import.meta.url (a version-pinned realpath that dies on node upgrades).
|
|
91
|
+
const { dest, warnings } = installDaemon(process.argv[1] || selfPath);
|
|
92
|
+
console.log(`installed: ${dest}`);
|
|
93
|
+
for (const w of warnings) console.log(`warning: ${w}`);
|
|
94
|
+
console.log("the watcher now starts at login and is running.");
|
|
95
|
+
console.log("next: claude-wake doctor (verify permissions)");
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
case "uninstall": {
|
|
100
|
+
uninstallDaemon();
|
|
101
|
+
console.log("daemon stopped and login registration removed.");
|
|
102
|
+
console.log(
|
|
103
|
+
`(files in ${BASE_DIR} were kept β delete manually for a full removal)`,
|
|
104
|
+
);
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
case "status": {
|
|
109
|
+
const cfg = loadConfig({
|
|
110
|
+
warn: flags.has("--json") ? () => {} : (m) => console.log(`warn: ${m}`),
|
|
111
|
+
});
|
|
112
|
+
const d = daemonStatus();
|
|
113
|
+
const st = loadState();
|
|
114
|
+
if (flags.has("--json")) {
|
|
115
|
+
// Stable machine-readable contract for the desktop app.
|
|
116
|
+
process.stdout.write(
|
|
117
|
+
JSON.stringify({
|
|
118
|
+
version: pkgVersion(),
|
|
119
|
+
daemonInstalled: d.installed,
|
|
120
|
+
daemonRunning: d.running,
|
|
121
|
+
paused: isPaused(),
|
|
122
|
+
editor: getEditor(cfg.editor).name,
|
|
123
|
+
message: cfg.message,
|
|
124
|
+
defaultMessage: DEFAULT_MESSAGE,
|
|
125
|
+
messageIsDefault: cfg.message === DEFAULT_MESSAGE,
|
|
126
|
+
watching: projectsDir(cfg),
|
|
127
|
+
pending: st.pending.map((p) => ({
|
|
128
|
+
sessionId: p.sessionId,
|
|
129
|
+
fire: p.fire,
|
|
130
|
+
event: p.event,
|
|
131
|
+
})),
|
|
132
|
+
}) + "\n",
|
|
133
|
+
);
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
console.log(
|
|
137
|
+
`daemon: ${d.installed ? (d.running ? "running" : "installed, NOT running") : "not installed"}`,
|
|
138
|
+
);
|
|
139
|
+
console.log(`state: ${isPaused() ? "PAUSED" : "active"}`);
|
|
140
|
+
console.log(`editor: ${getEditor(cfg.editor).name}`);
|
|
141
|
+
console.log(`watching: ${projectsDir(cfg)}`);
|
|
142
|
+
console.log(`pending: ${st.pending.length}`);
|
|
143
|
+
for (const p of st.pending) {
|
|
144
|
+
console.log(
|
|
145
|
+
` ${p.sessionId} fires ${new Date(p.fire).toLocaleString()}`,
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
case "pause": {
|
|
152
|
+
setPaused(true);
|
|
153
|
+
console.log("paused β auto-resume suspended (the watcher keeps running)");
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
case "resume": {
|
|
158
|
+
setPaused(false);
|
|
159
|
+
console.log("resumed β watching for new limits");
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
case "set-message": {
|
|
164
|
+
// The message is argv[1] (read directly so a leading "-" isn't eaten).
|
|
165
|
+
const clean = sanitizeMessage(argv[1] ?? "");
|
|
166
|
+
const raw = readConfigRaw();
|
|
167
|
+
if (clean) raw.message = clean;
|
|
168
|
+
else delete raw.message; // blank β fall back to the default
|
|
169
|
+
writeConfigRaw(raw);
|
|
170
|
+
console.log(clean ? "message updated" : "message reset to default");
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
case "doctor": {
|
|
175
|
+
const ok = await runDoctor();
|
|
176
|
+
process.exitCode = ok ? 0 : 1;
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
case "test": {
|
|
181
|
+
const cfg = loadConfig({ warn: (m) => console.log(`warn: ${m}`) });
|
|
182
|
+
let sessionId = flagValue("--session");
|
|
183
|
+
if (sessionId && !isValidSessionId(sessionId)) {
|
|
184
|
+
console.error("invalid --session id (expected a UUID)");
|
|
185
|
+
process.exitCode = 1;
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
if (!sessionId) {
|
|
189
|
+
// Most recently modified transcript = most recently active session.
|
|
190
|
+
const files = (listTranscripts(projectsDir(cfg)) || [])
|
|
191
|
+
.map((f) => {
|
|
192
|
+
try {
|
|
193
|
+
return { f, m: fs.statSync(f).mtimeMs };
|
|
194
|
+
} catch {
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
})
|
|
198
|
+
.filter(Boolean)
|
|
199
|
+
.sort((a, b) => b.m - a.m);
|
|
200
|
+
if (!files.length) {
|
|
201
|
+
console.error(
|
|
202
|
+
`no session transcripts found under ${projectsDir(cfg)}`,
|
|
203
|
+
);
|
|
204
|
+
process.exitCode = 1;
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
sessionId = path.basename(files[0].f, ".jsonl");
|
|
208
|
+
if (!isValidSessionId(sessionId)) {
|
|
209
|
+
console.error(
|
|
210
|
+
`most recent transcript has a non-UUID name (${sessionId}); use --session <id>`,
|
|
211
|
+
);
|
|
212
|
+
process.exitCode = 1;
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
console.log(`sending a test resume into session ${sessionId} β¦`);
|
|
217
|
+
const res = await sendResume({
|
|
218
|
+
sessionId,
|
|
219
|
+
editor: getEditor(cfg.editor),
|
|
220
|
+
message:
|
|
221
|
+
"[claude-wake] Test message β setup works. You can ignore this.",
|
|
222
|
+
focusDelayMs: cfg.focusDelayMs,
|
|
223
|
+
allowUnverifiedPaste: cfg.allowUnverifiedPaste,
|
|
224
|
+
dryRun: flags.has("--dry-run"),
|
|
225
|
+
log: (m) => console.log(m),
|
|
226
|
+
});
|
|
227
|
+
console.log(res.ok ? `ok: ${res.detail}` : `FAILED: ${res.detail}`);
|
|
228
|
+
if (!res.ok) process.exitCode = 1;
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
case "logs": {
|
|
233
|
+
if (!fs.existsSync(LOG_FILE)) {
|
|
234
|
+
console.log(`no log yet (${LOG_FILE})`);
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
if (flags.has("-f") || flags.has("--follow")) {
|
|
238
|
+
if (process.platform === "win32") {
|
|
239
|
+
// no `tail` on Windows: naive size-diff follower
|
|
240
|
+
let pos = fs.statSync(LOG_FILE).size;
|
|
241
|
+
process.stdout.write(
|
|
242
|
+
fs
|
|
243
|
+
.readFileSync(LOG_FILE, "utf8")
|
|
244
|
+
.split("\n")
|
|
245
|
+
.slice(-10)
|
|
246
|
+
.join("\n") + "\n",
|
|
247
|
+
);
|
|
248
|
+
setInterval(() => {
|
|
249
|
+
try {
|
|
250
|
+
const size = fs.statSync(LOG_FILE).size;
|
|
251
|
+
if (size > pos) {
|
|
252
|
+
const fd = fs.openSync(LOG_FILE, "r");
|
|
253
|
+
const buf = Buffer.alloc(size - pos);
|
|
254
|
+
fs.readSync(fd, buf, 0, buf.length, pos);
|
|
255
|
+
fs.closeSync(fd);
|
|
256
|
+
process.stdout.write(buf.toString("utf8"));
|
|
257
|
+
pos = size;
|
|
258
|
+
} else if (size < pos) pos = 0;
|
|
259
|
+
} catch {
|
|
260
|
+
/* keep polling */
|
|
261
|
+
}
|
|
262
|
+
}, 1000);
|
|
263
|
+
return; // keep process alive
|
|
264
|
+
}
|
|
265
|
+
const { spawn } = await import("node:child_process");
|
|
266
|
+
spawn("tail", ["-f", LOG_FILE], { stdio: "inherit" });
|
|
267
|
+
} else {
|
|
268
|
+
const lines = fs.readFileSync(LOG_FILE, "utf8").trimEnd().split("\n");
|
|
269
|
+
console.log(lines.slice(-40).join("\n"));
|
|
270
|
+
}
|
|
271
|
+
break;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
case "--version":
|
|
275
|
+
case "-v": {
|
|
276
|
+
console.log(pkgVersion());
|
|
277
|
+
break;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
default:
|
|
281
|
+
console.log(HELP);
|
|
282
|
+
if (cmd && cmd !== "help" && cmd !== "--help" && cmd !== "-h")
|
|
283
|
+
process.exitCode = 1;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
main().catch((err) => {
|
|
288
|
+
console.error(`error: ${err.message}`);
|
|
289
|
+
process.exit(1);
|
|
290
|
+
});
|