@gurugokul/doomscroller 0.0.0-stage → 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.
Files changed (57) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/LICENSE +21 -0
  3. package/README.md +300 -2
  4. package/dist/adapters/claude-code-hooks.js +171 -0
  5. package/dist/adapters/claude-code-hooks.js.map +1 -0
  6. package/dist/adapters/cli-wrapper.js +230 -0
  7. package/dist/adapters/cli-wrapper.js.map +1 -0
  8. package/dist/adapters/file-activity.js +78 -0
  9. package/dist/adapters/file-activity.js.map +1 -0
  10. package/dist/adapters/hook.js +17 -0
  11. package/dist/adapters/hook.js.map +1 -0
  12. package/dist/adapters/hotkey.js +50 -0
  13. package/dist/adapters/hotkey.js.map +1 -0
  14. package/dist/adapters/ide-activity.js +245 -0
  15. package/dist/adapters/ide-activity.js.map +1 -0
  16. package/dist/adapters/ports.js +3 -0
  17. package/dist/adapters/ports.js.map +1 -0
  18. package/dist/adapters/wrapper-tracker.js +77 -0
  19. package/dist/adapters/wrapper-tracker.js.map +1 -0
  20. package/dist/cli.js +315 -0
  21. package/dist/cli.js.map +1 -0
  22. package/dist/core/config.js +192 -0
  23. package/dist/core/config.js.map +1 -0
  24. package/dist/core/daemon.js +123 -0
  25. package/dist/core/daemon.js.map +1 -0
  26. package/dist/core/ipc.js +214 -0
  27. package/dist/core/ipc.js.map +1 -0
  28. package/dist/core/logger.js +53 -0
  29. package/dist/core/logger.js.map +1 -0
  30. package/dist/core/sound.js +28 -0
  31. package/dist/core/sound.js.map +1 -0
  32. package/dist/core/state-machine.js +268 -0
  33. package/dist/core/state-machine.js.map +1 -0
  34. package/dist/core/stats.js +109 -0
  35. package/dist/core/stats.js.map +1 -0
  36. package/dist/core/targets.js +23 -0
  37. package/dist/core/targets.js.map +1 -0
  38. package/dist/core/types.js +6 -0
  39. package/dist/core/types.js.map +1 -0
  40. package/dist/index.js +15 -0
  41. package/dist/index.js.map +1 -0
  42. package/dist/util/errors.js +16 -0
  43. package/dist/util/errors.js.map +1 -0
  44. package/dist/util/paths.js +70 -0
  45. package/dist/util/paths.js.map +1 -0
  46. package/dist/windows/cpu-probe.js +108 -0
  47. package/dist/windows/cpu-probe.js.map +1 -0
  48. package/dist/windows/key-listener.js +198 -0
  49. package/dist/windows/key-listener.js.map +1 -0
  50. package/dist/windows/win32.js +155 -0
  51. package/dist/windows/win32.js.map +1 -0
  52. package/dist/windows/window-controller.js +3 -0
  53. package/dist/windows/window-controller.js.map +1 -0
  54. package/package.json +57 -4
  55. package/scripts/cpu-sampler.ps1 +83 -0
  56. package/scripts/key-watcher.ps1 +132 -0
  57. package/scripts/win32-window.ps1 +344 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,35 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. This project follows
4
+ [Semantic Versioning](https://semver.org/).
5
+
6
+ ## [0.1.0] - 2026-10-22
7
+
8
+ ### Added
9
+
10
+ - Windows CLI with an `IDLE -> WORKING -> DONE` state machine driven by
11
+ `agent:started` and `agent:finished`.
12
+ - Configurable delay, target rotation, maximum doomscroll time cap, and a
13
+ `Ctrl+Alt+D` force-return hotkey.
14
+ - Win32 window control through PowerShell and `user32.dll`, including the
15
+ Alt-key workaround for Windows foreground locking.
16
+ - Hotkey adapter.
17
+ - Claude Code hook adapter plus `doomscroller init claude-code`.
18
+ - PTY-based CLI wrapper (`doomscroller run -- <command>`).
19
+ - Experimental IDE-activity adapter using registered window matches, Enter
20
+ detection, workspace mtime checks, and process-tree CPU sampling.
21
+ - `~/.doomscroller.json` config with validation and defaults.
22
+ - Session JSONL logging and `doomscroller stats`.
23
+ - `doomscroller config`, `status`, and `stop` commands.
24
+ - Unit tests for the state machine, adapters, config, hooks, stats, and IDE
25
+ activity.
26
+ - README, CONTRIBUTING, issue templates, PR template, and Windows CI.
27
+
28
+ ### Known limitations
29
+
30
+ - Windows 10/11 only. A macOS/Linux `WindowController` is planned.
31
+ - IDE activity detection is heuristic: a long-running agent with no file
32
+ writes and low CPU can be classified as finished.
33
+ - The hotkey watcher observes keys but does not swallow them.
34
+ - Target rotation is held in memory and resets when the daemon restarts.
35
+ - The demo GIF is not recorded yet; see the placeholder in README.md.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Gurugokul
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 CHANGED
@@ -1,3 +1,301 @@
1
- # Temporary Holding Version
1
+ # doomscroller
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Doomscroll while your AI coding agent works, then get pulled back when the agent finishes.
4
+
5
+ [![npm version](https://img.shields.io/npm/v/@gurugokul/doomscroller.svg?label=npm)](https://www.npmjs.com/package/@gurugokul/doomscroller)
6
+ [![license](https://img.shields.io/github/license/Gurugokul05/doomscroller.svg)](https://github.com/Gurugokul05/doomscroller/blob/main/LICENSE)
7
+ [![platform](https://img.shields.io/badge/platform-Windows-0078D6)](#quick-start)
8
+
9
+ ![demo](./docs/demo.gif)
10
+
11
+ > **Demo GIF TODO:** Record an 8–12 second loop and save it as `docs/demo.gif`:
12
+ > submit a prompt → the agent window minimizes → Instagram/X opens → the agent
13
+ > finishes → the agent window returns. Keep it under ~5 MB. The
14
+ > `Ctrl+Alt+D` force-return should be visible in the clip.
15
+
16
+ ## Why
17
+
18
+ AI agents spend a lot of time thinking. doomscroller turns that dead time into a
19
+ deliberate break: it moves focus to the next site in your rotation, then pulls
20
+ the agent window back when the agent is done. It is a small Windows utility, not
21
+ a productivity system.
22
+
23
+ ## Quick start
24
+
25
+ 1. **Install it globally:**
26
+
27
+ ```powershell
28
+ npm i -g @gurugokul/doomscroller
29
+ ```
30
+
31
+ 2. **Install the Claude Code hooks** (this also creates `~/.doomscroller.json`):
32
+
33
+ ```powershell
34
+ doomscroller init claude-code
35
+ ```
36
+
37
+ 3. **Run your agent and submit a prompt:**
38
+
39
+ ```powershell
40
+ claude
41
+ ```
42
+
43
+ After `delaySeconds` (10 by default), the agent window is minimized and the
44
+ first target opens in a new browser window. When Claude Code stops or emits a
45
+ notification, the browser is minimized and the agent window returns. Press
46
+ `Ctrl+Alt+D` at any time to force the return.
47
+
48
+ Requires Windows 10/11, Node.js 20+, and Windows PowerShell 5.1. If you use a
49
+ different agent, see the table below. For a terminal agent without hooks, use
50
+ `doomscroller run -- <command>`. To configure GUI windows, see
51
+ [Config](#config).
52
+
53
+ ## Supported agents
54
+
55
+ "Working" means the adapter path is implemented and covered by tests.
56
+ "Experimental" means the adapter is implemented but has not been validated
57
+ against that agent's current release. "Planned" means it is not implemented.
58
+
59
+ | Agent | Adapter | Status | Notes |
60
+ | --- | --- | --- | --- |
61
+ | Claude Code | hook | Working | `doomscroller init claude-code` merges `UserPromptSubmit` → start and `Stop`/`Notification` → done into `~/.claude/settings.json`. Verify the hooks fire in your Claude Code version. |
62
+ | Command Code | cli-wrapper | Experimental | `doomscroller run -- cmdc`. The PTY wrapper is tested, but this specific binary is not. |
63
+ | GitHub Copilot CLI | cli-wrapper | Experimental | `doomscroller run -- copilot`. Behavior depends on the CLI's terminal output. |
64
+ | Antigravity | ide-activity | Experimental | Match the window by process and/or title, set `workspace`, and press Enter in the window. |
65
+ | VS Code + Copilot | ide-activity | Experimental | Same heuristic. Use a generous `quietSeconds` and an absolute `workspace`. |
66
+ | Other GUI IDEs (Cursor, etc.) | ide-activity | Experimental | Add a `registeredAgents` entry with the app's process/title and a workspace. |
67
+ | Any foreground app | hotkey | Working | `Ctrl+Alt+D` starts a manual session when idle and force-returns when working. The global key press needs a real key to verify; see [Troubleshooting](#troubleshooting). |
68
+
69
+ macOS and Linux support is **planned**. The state machine and tests are
70
+ platform-neutral, but window control is Win32-only. See
71
+ [CONTRIBUTING.md](https://github.com/Gurugokul05/doomscroller/blob/main/CONTRIBUTING.md#adding-os-support)
72
+ for the `WindowController` interface.
73
+
74
+ ## Config
75
+
76
+ `doomscroller init` (or `doomscroller init claude-code`) creates
77
+ `~/.doomscroller.json`. Every field is optional. Invalid values are ignored or
78
+ fall back to the defaults, and the tool keeps running.
79
+
80
+ ```json
81
+ {
82
+ "targets": [
83
+ "https://www.instagram.com/",
84
+ "https://x.com/",
85
+ "https://www.youtube.com/shorts"
86
+ ],
87
+ "delaySeconds": 10,
88
+ "quietSeconds": 15,
89
+ "maxDoomscrollSeconds": 600,
90
+ "hotkey": "Ctrl+Alt+D",
91
+ "sound": true,
92
+ "browser": "auto",
93
+ "port": 45871,
94
+ "idleCpuPercent": 5,
95
+ "registeredAgents": [
96
+ {
97
+ "name": "Antigravity",
98
+ "adapter": "ide-activity",
99
+ "match": { "process": "Antigravity", "title": "Antigravity" },
100
+ "workspace": "C:\\code\\my-project",
101
+ "quietSeconds": 20
102
+ }
103
+ ],
104
+ "logPath": "~/.doomscroller/doomscroller.log",
105
+ "sessionLogPath": "~/.doomscroller/sessions.jsonl"
106
+ }
107
+ ```
108
+
109
+ | Field | Default | Meaning |
110
+ | --- | --- | --- |
111
+ | `targets` | Instagram, X, YouTube Shorts | URLs rotated one per doomscroll session. An empty or invalid list falls back to the defaults. |
112
+ | `delaySeconds` | `10` | Grace period after `agent:started`. A `finished` event during the delay cancels the switch and no windows move. `0` switches immediately. |
113
+ | `quietSeconds` | `15` | CLI-wrapper silence before `agent:finished`, and the default inactivity window for IDE agents. Minimum `1`. |
114
+ | `maxDoomscrollSeconds` | `600` | Hard cap. On expiry the browser is minimized, the agent returns, and the session is logged with `end: "cap"`. `0` disables the cap. |
115
+ | `hotkey` | `"Ctrl+Alt+D"` | Global force-return / manual-start hotkey. Set to `null` to disable it. The shared Enter watcher still runs when IDE agents are configured. The watcher observes the key but does not swallow it from the foreground app. |
116
+ | `sound` | `true` | Play a short two-tone chime when a doomscroll session ends. |
117
+ | `browser` | `"auto"` | `auto`, `edge`, `chrome`, `brave`, `firefox`, or `default`. `auto` reads the Windows default browser for HTTPS and falls back to Edge/Chrome. `default` launches through the shell and detects a new window from a known browser process. |
118
+ | `port` | `45871` | Local daemon IPC port, bound to `127.0.0.1`. |
119
+ | `idleCpuPercent` | `5` | IDE finish threshold as a percentage of one CPU core. Activity at or below this is considered idle. |
120
+ | `registeredAgents` | one Antigravity entry | GUI windows for the IDE adapter. `[]` disables IDE activity. |
121
+ | `logPath` | `~/.doomscroller/doomscroller.log` | Daemon log. Absolute paths and `~/...` are supported. |
122
+ | `sessionLogPath` | `~/.doomscroller/sessions.jsonl` | Session records read by `doomscroller stats`. Absolute paths and `~/...` are supported. |
123
+
124
+ Each `registeredAgents` entry:
125
+
126
+ | Field | Required | Meaning |
127
+ | --- | --- | --- |
128
+ | `name` | Yes | Display name used in logs. |
129
+ | `adapter` | Yes | Use `ide-activity` for GUI windows. The other adapter names are accepted by config validation but only `ide-activity` is consumed by the IDE adapter. |
130
+ | `match.process` | No | Process name, case-insensitive, `.exe` ignored. |
131
+ | `match.title` | No | Case-insensitive substring of the window title. |
132
+ | `workspace` | No | Absolute path or `~/...` path to scan for modified files. Relative paths are resolved against the daemon's working directory, so prefer an absolute path. Without it, only CPU is used. |
133
+ | `quietSeconds` | No | Per-agent override for the global `quietSeconds`. |
134
+
135
+ At least one of `match.process` and `match.title` is required. If both are
136
+ provided, both must match.
137
+
138
+ Environment overrides, useful for tests and alternate installs:
139
+
140
+ - `DOOMSCROLLER_HOME` — base directory for config, logs, and sessions.
141
+ - `DOOMSCROLLER_CONFIG` — explicit config file path.
142
+ - `CLAUDE_CONFIG_DIR` — Claude Code config directory used by `init claude-code`.
143
+
144
+ Use `doomscroller config` to print the config path and the effective merged
145
+ config. Use `doomscroller config --path` for only the path or
146
+ `doomscroller config --json` for only the JSON document.
147
+
148
+ ## Safety
149
+
150
+ - **Opt-in.** doomscroller does not move windows until an adapter emits
151
+ `agent:started` (a hook, Enter in a registered IDE window, `doomscroller
152
+ start`, or the manual hotkey). There is no background observation of your
153
+ screen.
154
+ - **Force return.** `Ctrl+Alt+D` force-returns to the saved agent window while
155
+ working, and starts a session while idle. It is observed globally but not
156
+ swallowed, so the foreground application also receives it. Change it in the
157
+ config or set `hotkey: null` to disable it.
158
+ - **Hard cap.** `maxDoomscrollSeconds` (default 10 minutes) ends a session even
159
+ if the agent never reports finished. Set it to `0` to disable the cap.
160
+ - **Delay before switching.** `delaySeconds` (default 10 seconds) gives short
161
+ tasks a chance to finish before anything is minimized.
162
+ - **Failure isolation.** Adapters and hooks swallow errors, and the state
163
+ machine serializes events so overlapping callbacks cannot interleave window
164
+ operations. A failed browser launch or denied focus change does not crash the
165
+ agent.
166
+ - **Non-destructive window handling.** The browser is minimized, not closed.
167
+ The agent window handle is saved before minimizing and restored on finish,
168
+ force-return, cap, or `doomscroller stop`.
169
+ - **Local only.** IPC binds to `127.0.0.1`. doomscroller has no telemetry and
170
+ makes no external network requests beyond the browser you ask it to open.
171
+ - **Same elevation.** Run doomscroller at the same elevation as the agent.
172
+ Windows can block focus changes between different integrity levels.
173
+
174
+ ## Commands
175
+
176
+ | Command | What it does |
177
+ | --- | --- |
178
+ | `doomscroller init` | Create `~/.doomscroller.json` if it does not exist. |
179
+ | `doomscroller init claude-code` | Create the config and merge the Claude Code hooks into `~/.claude/settings.json`. Existing hooks are preserved, and `settings.json.doomscroller.bak` is created when the file already exists. |
180
+ | `doomscroller start` | Emit `agent:started`. Hook-safe: exits quickly, never blocks the agent, and auto-starts the daemon if needed. |
181
+ | `doomscroller done` | Emit `agent:finished`. Same hook-safe behavior. |
182
+ | `doomscroller run -- <command>` | Run a terminal agent in a PTY. Enter starts, output resets the quiet timer, `quietSeconds` of silence or process exit finishes. |
183
+ | `doomscroller stats [--days N\|--all]` | Show doomscroll wait time per day from `sessions.jsonl`. Defaults to the last 14 days. |
184
+ | `doomscroller config [--path\|--json]` | Print the config path, whether it exists, and the effective merged config. |
185
+ | `doomscroller daemon` | Run the daemon in the foreground. Hooks and the wrapper normally auto-start it. |
186
+ | `doomscroller hotkey` | Alias for `doomscroller daemon`. |
187
+ | `doomscroller status` | Show `state=idle\|working\|done` and the current target. |
188
+ | `doomscroller stop` | Stop the daemon. If a session is active, restore the agent window first. |
189
+
190
+ `doomscroller --help` lists the same commands. `doomscroller --version` prints
191
+ the package version. `stats` shows time actually spent doomscrolling; sessions
192
+ that finish during the delay are counted with zero waiting time.
193
+
194
+ ## How it works
195
+
196
+ The core is a small state machine in `src/core/state-machine.ts`:
197
+
198
+ ```text
199
+ IDLE --agent:started--> WORKING --agent:finished--> DONE
200
+ | |
201
+ | wait delaySeconds | minimize browser
202
+ | save + minimize agent | restore + focus agent
203
+ | open next target | chime + log session
204
+ | |
205
+ +---- hotkey / max cap ----------+
206
+ ```
207
+
208
+ Adapters only emit the two events; they never move windows themselves. The
209
+ daemon hosts the adapters and exposes a localhost IPC endpoint:
210
+
211
+ - **hotkey** watches for the configured key.
212
+ - **hook** is the `doomscroller start` / `doomscroller done` pair, plus the
213
+ Claude Code settings merger.
214
+ - **cli-wrapper** runs a command in a PTY and derives start/finish from Enter,
215
+ output, silence, and process exit.
216
+ - **ide-activity** watches Enter in registered windows, then uses workspace
217
+ mtimes and process-tree CPU to decide when the agent is idle.
218
+
219
+ Window operations go through the `WindowController` interface
220
+ (`src/windows/window-controller.ts`). The Win32 implementation shells out to
221
+ `scripts/win32-window.ps1`, which uses `GetForegroundWindow`, `ShowWindow`, and
222
+ `SetForegroundWindow`. Before focusing, it attaches to the foreground thread and
223
+ taps Alt to release Windows' foreground lock.
224
+
225
+ Target rotation is held in memory and starts at the first target again when the
226
+ daemon restarts. Sessions are appended to `sessions.jsonl`, and `stats` groups
227
+ them by local finish date.
228
+
229
+ ## Troubleshooting
230
+
231
+ ### Windows focus is not returned to the agent
232
+
233
+ Windows refuses `SetForegroundWindow` from processes that do not own the
234
+ foreground lock. doomscroller applies the standard workaround: `AttachThreadInput`
235
+ plus a synthetic Alt tap before focusing, with one retry. Some elevated, UWP, or
236
+ custom-chrome windows still refuse. Run the daemon at the same elevation as the
237
+ agent; if the focus attempt is not confirmed, check
238
+ `~/.doomscroller/doomscroller.log` and use `Ctrl+Alt+D` to retry. If focus moves
239
+ when you did not expect it, raise `delaySeconds`, lower
240
+ `maxDoomscrollSeconds`, or set `hotkey: null`.
241
+
242
+ ### Claude Code hooks are not firing
243
+
244
+ Confirm that `doomscroller --version` works in the same shell Claude Code uses.
245
+ Re-run `doomscroller init claude-code`; it prints the settings path and the
246
+ events it added or found. Check `~/.claude/settings.json` (or the directory in
247
+ `CLAUDE_CONFIG_DIR`) for the three commands and make sure they point at this
248
+ installation's `dist/index.js`. Invalid settings JSON is never overwritten; fix
249
+ the JSON or restore `settings.json.doomscroller.bak`. Then check
250
+ `doomscroller status` and the daemon log. If the daemon cannot start, the hook
251
+ still exits cleanly, but no session begins.
252
+
253
+ ### `ide-activity` says "finished" too early
254
+
255
+ The IDE adapter is a heuristic. It finishes after `quietSeconds` with no
256
+ workspace mtime changes and CPU at or below `idleCpuPercent`. An agent that is
257
+ thinking with no file writes and little CPU looks idle. Increase the global or
258
+ per-agent `quietSeconds`, make sure `workspace` points at the real project, and
259
+ lower `idleCpuPercent` if small CPU bursts should count as activity. If the
260
+ agent offers hooks or runs in a terminal, the hook or cli-wrapper adapters are
261
+ more deterministic. Press Enter in the registered window to start a session.
262
+
263
+ ### The browser window was not detected
264
+
265
+ doomscroller only recognizes new windows from Edge, Chrome, Brave, Firefox,
266
+ Opera, Vivaldi, and Arc. Set `browser` to `edge` or `chrome`, or use `default`
267
+ if your default browser is in that list. A browser that reuses an existing
268
+ window instead of creating a new one may not produce a detectable handle.
269
+
270
+ ### The hotkey does nothing
271
+
272
+ The watcher polls `GetAsyncKeyState` every 15 ms. Test it with a real key press
273
+ while another application is focused; synthetic `SendKeys` input does not
274
+ reliably update the async key state in non-interactive sessions. Check the log,
275
+ make sure the daemon is running, and avoid remote-desktop or VM layers that
276
+ capture the keyboard.
277
+
278
+ ### `node-pty` fails to load
279
+
280
+ `doomscroller run` needs the native `node-pty` module. Run `npm rebuild
281
+ node-pty`. If that fails, install Visual Studio Build Tools with the C++ desktop
282
+ workload and rebuild. The other adapters do not need node-pty.
283
+
284
+ ### Stats are empty
285
+
286
+ Sessions are written by the daemon to `~/.doomscroller/sessions.jsonl` when a
287
+ session ends. Ensure at least one `start`/`finished` pair reached the daemon,
288
+ then run `doomscroller stats`. `doomscroller status` shows whether the daemon is
289
+ running.
290
+
291
+ ## Contributing
292
+
293
+ Contributions are welcome. See
294
+ [CONTRIBUTING.md](https://github.com/Gurugokul05/doomscroller/blob/main/CONTRIBUTING.md)
295
+ for the dev setup, adapter example, OS-support notes, and PR checklist. Bugs and
296
+ agent requests have issue templates in `.github/ISSUE_TEMPLATE/`.
297
+
298
+ ## License
299
+
300
+ MIT © 2026 Gurugokul. See
301
+ [LICENSE](https://github.com/Gurugokul05/doomscroller/blob/main/LICENSE).
@@ -0,0 +1,171 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.HOOK_EVENTS = void 0;
7
+ exports.hookCommand = hookCommand;
8
+ exports.hookCommands = hookCommands;
9
+ exports.mergeClaudeHooks = mergeClaudeHooks;
10
+ exports.installClaudeCodeHooks = installClaudeCodeHooks;
11
+ const node_fs_1 = __importDefault(require("node:fs"));
12
+ const node_path_1 = __importDefault(require("node:path"));
13
+ const errors_1 = require("../util/errors");
14
+ const paths_1 = require("../util/paths");
15
+ exports.HOOK_EVENTS = ['UserPromptSubmit', 'Stop', 'Notification'];
16
+ function hookCommand(action) {
17
+ return `"${process.execPath}" "${(0, paths_1.cliEntryPath)()}" ${action}`;
18
+ }
19
+ function hookCommands() {
20
+ const start = hookCommand('start');
21
+ const done = hookCommand('done');
22
+ return {
23
+ UserPromptSubmit: start,
24
+ Stop: done,
25
+ Notification: done,
26
+ };
27
+ }
28
+ function asRecord(value) {
29
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
30
+ ? value
31
+ : null;
32
+ }
33
+ function commandTargetsAction(command, action) {
34
+ const trimmed = command.trim();
35
+ const ending = new RegExp(`(?:^|[\\s"'=])${action}\\s*$`, 'i');
36
+ if (!ending.test(trimmed))
37
+ return false;
38
+ const lower = trimmed.toLowerCase();
39
+ return lower.includes('doomscroller') || lower.includes('index.js') || lower.includes('index.ts');
40
+ }
41
+ function groupsContainCommand(groups, command, action) {
42
+ for (const group of groups) {
43
+ const record = asRecord(group);
44
+ if (!record || !Array.isArray(record.hooks))
45
+ continue;
46
+ for (const hook of record.hooks) {
47
+ const hookRecord = asRecord(hook);
48
+ if (!hookRecord || typeof hookRecord.command !== 'string')
49
+ continue;
50
+ if (hookRecord.command === command || commandTargetsAction(hookRecord.command, action)) {
51
+ return true;
52
+ }
53
+ }
54
+ }
55
+ return false;
56
+ }
57
+ /**
58
+ * Adds doomscroller hooks without replacing any existing hook groups. Running
59
+ * this repeatedly is a no-op because an existing doomscroller command for the
60
+ * same event is detected first.
61
+ */
62
+ function mergeClaudeHooks(settings, commands) {
63
+ const root = asRecord(settings) ?? {};
64
+ const hooksRaw = root.hooks;
65
+ if (hooksRaw !== undefined && !asRecord(hooksRaw)) {
66
+ return { settings: root, added: [], existing: [], error: '"hooks" in settings.json is not an object' };
67
+ }
68
+ const hooks = { ...(asRecord(hooksRaw) ?? {}) };
69
+ const added = [];
70
+ const existing = [];
71
+ for (const [eventName, command] of Object.entries(commands)) {
72
+ const current = hooks[eventName];
73
+ if (current !== undefined && !Array.isArray(current)) {
74
+ return {
75
+ settings: root,
76
+ added,
77
+ existing,
78
+ error: `hooks.${eventName} exists but is not an array; refusing to overwrite it`,
79
+ };
80
+ }
81
+ const groups = current === undefined ? [] : [...current];
82
+ const action = eventName === 'UserPromptSubmit' ? 'start' : 'done';
83
+ if (groupsContainCommand(groups, command, action)) {
84
+ existing.push(eventName);
85
+ continue;
86
+ }
87
+ groups.push({ hooks: [{ type: 'command', command }] });
88
+ hooks[eventName] = groups;
89
+ added.push(eventName);
90
+ }
91
+ return {
92
+ settings: { ...root, hooks },
93
+ added,
94
+ existing,
95
+ };
96
+ }
97
+ async function installClaudeCodeHooks(settingsPath = (0, paths_1.claudeSettingsPath)()) {
98
+ let raw = {};
99
+ try {
100
+ const text = node_fs_1.default.readFileSync(settingsPath, 'utf8').replace(/^\uFEFF/, '');
101
+ if (text.trim() !== '')
102
+ raw = JSON.parse(text);
103
+ }
104
+ catch (error) {
105
+ const code = error.code;
106
+ if (code !== 'ENOENT') {
107
+ return {
108
+ settingsPath,
109
+ added: [],
110
+ existing: [],
111
+ changed: false,
112
+ error: `could not read ${settingsPath}: ${(0, errors_1.errorMessage)(error)}`,
113
+ };
114
+ }
115
+ }
116
+ const merged = mergeClaudeHooks(raw, hookCommands());
117
+ if (merged.error) {
118
+ return {
119
+ settingsPath,
120
+ added: merged.added,
121
+ existing: merged.existing,
122
+ changed: false,
123
+ error: merged.error,
124
+ };
125
+ }
126
+ if (merged.added.length === 0) {
127
+ return {
128
+ settingsPath,
129
+ added: [],
130
+ existing: merged.existing,
131
+ changed: false,
132
+ };
133
+ }
134
+ try {
135
+ node_fs_1.default.mkdirSync(node_path_1.default.dirname(settingsPath), { recursive: true });
136
+ if (node_fs_1.default.existsSync(settingsPath)) {
137
+ node_fs_1.default.copyFileSync(settingsPath, `${settingsPath}.doomscroller.bak`);
138
+ }
139
+ const json = `${JSON.stringify(merged.settings, null, 2)}\n`;
140
+ const temporary = `${settingsPath}.doomscroller-${process.pid}.tmp`;
141
+ node_fs_1.default.writeFileSync(temporary, json, 'utf8');
142
+ try {
143
+ node_fs_1.default.renameSync(temporary, settingsPath);
144
+ }
145
+ catch {
146
+ node_fs_1.default.writeFileSync(settingsPath, json, 'utf8');
147
+ try {
148
+ node_fs_1.default.unlinkSync(temporary);
149
+ }
150
+ catch {
151
+ // ignore
152
+ }
153
+ }
154
+ return {
155
+ settingsPath,
156
+ added: merged.added,
157
+ existing: merged.existing,
158
+ changed: true,
159
+ };
160
+ }
161
+ catch (error) {
162
+ return {
163
+ settingsPath,
164
+ added: merged.added,
165
+ existing: merged.existing,
166
+ changed: false,
167
+ error: `could not write ${settingsPath}: ${(0, errors_1.errorMessage)(error)}`,
168
+ };
169
+ }
170
+ }
171
+ //# sourceMappingURL=claude-code-hooks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"claude-code-hooks.js","sourceRoot":"","sources":["../../src/adapters/claude-code-hooks.ts"],"names":[],"mappings":"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"}