@gurugokul/doomscroller 0.0.0-stage → 0.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.
Files changed (61) hide show
  1. package/CHANGELOG.md +57 -0
  2. package/LICENSE +21 -0
  3. package/README.md +323 -2
  4. package/dist/adapters/claude-code-hooks.js +27 -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/command-code-hooks.js +32 -0
  9. package/dist/adapters/command-code-hooks.js.map +1 -0
  10. package/dist/adapters/file-activity.js +78 -0
  11. package/dist/adapters/file-activity.js.map +1 -0
  12. package/dist/adapters/hook-settings.js +164 -0
  13. package/dist/adapters/hook-settings.js.map +1 -0
  14. package/dist/adapters/hook.js +17 -0
  15. package/dist/adapters/hook.js.map +1 -0
  16. package/dist/adapters/hotkey.js +50 -0
  17. package/dist/adapters/hotkey.js.map +1 -0
  18. package/dist/adapters/ide-activity.js +245 -0
  19. package/dist/adapters/ide-activity.js.map +1 -0
  20. package/dist/adapters/ports.js +3 -0
  21. package/dist/adapters/ports.js.map +1 -0
  22. package/dist/adapters/wrapper-tracker.js +77 -0
  23. package/dist/adapters/wrapper-tracker.js.map +1 -0
  24. package/dist/cli.js +343 -0
  25. package/dist/cli.js.map +1 -0
  26. package/dist/core/config.js +192 -0
  27. package/dist/core/config.js.map +1 -0
  28. package/dist/core/daemon.js +156 -0
  29. package/dist/core/daemon.js.map +1 -0
  30. package/dist/core/ipc.js +215 -0
  31. package/dist/core/ipc.js.map +1 -0
  32. package/dist/core/logger.js +53 -0
  33. package/dist/core/logger.js.map +1 -0
  34. package/dist/core/sound.js +38 -0
  35. package/dist/core/sound.js.map +1 -0
  36. package/dist/core/state-machine.js +272 -0
  37. package/dist/core/state-machine.js.map +1 -0
  38. package/dist/core/stats.js +109 -0
  39. package/dist/core/stats.js.map +1 -0
  40. package/dist/core/targets.js +27 -0
  41. package/dist/core/targets.js.map +1 -0
  42. package/dist/core/types.js +6 -0
  43. package/dist/core/types.js.map +1 -0
  44. package/dist/index.js +15 -0
  45. package/dist/index.js.map +1 -0
  46. package/dist/util/errors.js +16 -0
  47. package/dist/util/errors.js.map +1 -0
  48. package/dist/util/paths.js +77 -0
  49. package/dist/util/paths.js.map +1 -0
  50. package/dist/windows/cpu-probe.js +108 -0
  51. package/dist/windows/cpu-probe.js.map +1 -0
  52. package/dist/windows/key-listener.js +198 -0
  53. package/dist/windows/key-listener.js.map +1 -0
  54. package/dist/windows/win32.js +158 -0
  55. package/dist/windows/win32.js.map +1 -0
  56. package/dist/windows/window-controller.js +3 -0
  57. package/dist/windows/window-controller.js.map +1 -0
  58. package/package.json +57 -4
  59. package/scripts/cpu-sampler.ps1 +83 -0
  60. package/scripts/key-watcher.ps1 +132 -0
  61. package/scripts/win32-window.ps1 +344 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,57 @@
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.2.0] - 2026-10-23
7
+
8
+ ### Added
9
+
10
+ - Command Code hook adapter plus `doomscroller init command-code`. It merges a
11
+ `PreToolUse` → start and a `Stop` → done hook into `~/.commandcode/settings.json`
12
+ (user scope), mirroring `init claude-code`. Command Code has no per-prompt
13
+ hook, so start is the first tool call of a turn. Existing hooks are preserved
14
+ and a `settings.json.doomscroller.bak` backup is created.
15
+
16
+ ### Fixed
17
+
18
+ - The end-of-session chime now plays. The player spawned PowerShell detached,
19
+ and a detached, console-less `powershell.exe` exits without running its
20
+ `-Command`, so `[console]::beep` was a silent no-op.
21
+ - Config is re-read at the start of each session, so `delaySeconds`,
22
+ `maxDoomscrollSeconds`, `targets`, `browser`, and `sound` take effect without
23
+ restarting the daemon. A missing or invalid config keeps the previous values;
24
+ `port` and `hotkey` still require a restart.
25
+ - Removed a redundant daemon spawn on the hook fast-path that could race the
26
+ dispatch helper and produce `EADDRINUSE` / duplicate daemons.
27
+
28
+ ## [0.1.0] - 2026-10-22
29
+
30
+ ### Added
31
+
32
+ - Windows CLI with an `IDLE -> WORKING -> DONE` state machine driven by
33
+ `agent:started` and `agent:finished`.
34
+ - Configurable delay, target rotation, maximum doomscroll time cap, and a
35
+ `Ctrl+Alt+D` force-return hotkey.
36
+ - Win32 window control through PowerShell and `user32.dll`, including the
37
+ Alt-key workaround for Windows foreground locking.
38
+ - Hotkey adapter.
39
+ - Claude Code hook adapter plus `doomscroller init claude-code`.
40
+ - PTY-based CLI wrapper (`doomscroller run -- <command>`).
41
+ - Experimental IDE-activity adapter using registered window matches, Enter
42
+ detection, workspace mtime checks, and process-tree CPU sampling.
43
+ - `~/.doomscroller.json` config with validation and defaults.
44
+ - Session JSONL logging and `doomscroller stats`.
45
+ - `doomscroller config`, `status`, and `stop` commands.
46
+ - Unit tests for the state machine, adapters, config, hooks, stats, and IDE
47
+ activity.
48
+ - README, CONTRIBUTING, issue templates, PR template, and Windows CI.
49
+
50
+ ### Known limitations
51
+
52
+ - Windows 10/11 only. A macOS/Linux `WindowController` is planned.
53
+ - IDE activity detection is heuristic: a long-running agent with no file
54
+ writes and low CPU can be classified as finished.
55
+ - The hotkey watcher observes keys but does not swallow them.
56
+ - Target rotation is held in memory and resets when the daemon restarts.
57
+ - 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,324 @@
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 your agent's hooks** (this also creates `~/.doomscroller.json`):
32
+
33
+ ```powershell
34
+ doomscroller init claude-code # Claude Code
35
+ doomscroller init command-code # Command Code
36
+ ```
37
+
38
+ `init command-code` merges the hooks into `~/.commandcode/settings.json` the
39
+ same way `init claude-code` does for Claude Code.
40
+
41
+ 3. **Run your agent and submit a prompt:**
42
+
43
+ ```powershell
44
+ claude
45
+ ```
46
+
47
+ After `delaySeconds` (10 by default), the agent window is minimized and the
48
+ first target opens in a new browser window. When Claude Code stops or emits a
49
+ notification, the browser is minimized and the agent window returns. Press
50
+ `Ctrl+Alt+D` at any time to force the return.
51
+
52
+ Requires Windows 10/11, Node.js 20+, and Windows PowerShell 5.1. If you use a
53
+ different agent, see the table below. For a terminal agent without hooks, use
54
+ `doomscroller run -- <command>`. To configure GUI windows, see
55
+ [Config](#config).
56
+
57
+ ## Supported agents
58
+
59
+ "Working" means the adapter path is implemented and covered by tests.
60
+ "Experimental" means the adapter is implemented but has not been validated
61
+ against that agent's current release. "Planned" means it is not implemented.
62
+
63
+ | Agent | Adapter | Status | Notes |
64
+ | --- | --- | --- | --- |
65
+ | 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. |
66
+ | Command Code | hook | Working | `doomscroller init command-code` merges `PreToolUse` → start and `Stop` → done into `~/.commandcode/settings.json`. Command Code has no per-prompt hook, so a session starts on a turn's first tool call; hooks are skipped in plan mode. Verify the hooks fire in your Command Code version. |
67
+ | GitHub Copilot CLI | cli-wrapper | Experimental | `doomscroller run -- copilot`. Behavior depends on the CLI's terminal output. |
68
+ | Antigravity | ide-activity | Experimental | Match the window by process and/or title, set `workspace`, and press Enter in the window. |
69
+ | VS Code + Copilot | ide-activity | Experimental | Same heuristic. Use a generous `quietSeconds` and an absolute `workspace`. |
70
+ | Other GUI IDEs (Cursor, etc.) | ide-activity | Experimental | Add a `registeredAgents` entry with the app's process/title and a workspace. |
71
+ | 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). |
72
+
73
+ A terminal agent without hooks can also be wrapped with
74
+ `doomscroller run -- <command>` (for example `doomscroller run -- cmdc`). The
75
+ hook path is more accurate for Command Code, because it no longer depends on
76
+ terminal silence to decide the session is finished.
77
+
78
+ macOS and Linux support is **planned**. The state machine and tests are
79
+ platform-neutral, but window control is Win32-only. See
80
+ [CONTRIBUTING.md](https://github.com/Gurugokul05/doomscroller/blob/main/CONTRIBUTING.md#adding-os-support)
81
+ for the `WindowController` interface.
82
+
83
+ ## Config
84
+
85
+ `doomscroller init` (or `doomscroller init claude-code`) creates
86
+ `~/.doomscroller.json`. Every field is optional. Invalid values are ignored or
87
+ fall back to the defaults, and the tool keeps running.
88
+
89
+ ```json
90
+ {
91
+ "targets": [
92
+ "https://www.instagram.com/",
93
+ "https://x.com/",
94
+ "https://www.youtube.com/shorts"
95
+ ],
96
+ "delaySeconds": 10,
97
+ "quietSeconds": 15,
98
+ "maxDoomscrollSeconds": 600,
99
+ "hotkey": "Ctrl+Alt+D",
100
+ "sound": true,
101
+ "browser": "auto",
102
+ "port": 45871,
103
+ "idleCpuPercent": 5,
104
+ "registeredAgents": [
105
+ {
106
+ "name": "Antigravity",
107
+ "adapter": "ide-activity",
108
+ "match": { "process": "Antigravity", "title": "Antigravity" },
109
+ "workspace": "C:\\code\\my-project",
110
+ "quietSeconds": 20
111
+ }
112
+ ],
113
+ "logPath": "~/.doomscroller/doomscroller.log",
114
+ "sessionLogPath": "~/.doomscroller/sessions.jsonl"
115
+ }
116
+ ```
117
+
118
+ | Field | Default | Meaning |
119
+ | --- | --- | --- |
120
+ | `targets` | Instagram, X, YouTube Shorts | URLs rotated one per doomscroll session. An empty or invalid list falls back to the defaults. |
121
+ | `delaySeconds` | `10` | Grace period after `agent:started`. A `finished` event during the delay cancels the switch and no windows move. `0` switches immediately. |
122
+ | `quietSeconds` | `15` | CLI-wrapper silence before `agent:finished`, and the default inactivity window for IDE agents. Minimum `1`. |
123
+ | `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. |
124
+ | `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. |
125
+ | `sound` | `true` | Play a short two-tone chime when a doomscroll session ends. |
126
+ | `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. |
127
+ | `port` | `45871` | Local daemon IPC port, bound to `127.0.0.1`. |
128
+ | `idleCpuPercent` | `5` | IDE finish threshold as a percentage of one CPU core. Activity at or below this is considered idle. |
129
+ | `registeredAgents` | one Antigravity entry | GUI windows for the IDE adapter. `[]` disables IDE activity. |
130
+ | `logPath` | `~/.doomscroller/doomscroller.log` | Daemon log. Absolute paths and `~/...` are supported. |
131
+ | `sessionLogPath` | `~/.doomscroller/sessions.jsonl` | Session records read by `doomscroller stats`. Absolute paths and `~/...` are supported. |
132
+
133
+ Each `registeredAgents` entry:
134
+
135
+ | Field | Required | Meaning |
136
+ | --- | --- | --- |
137
+ | `name` | Yes | Display name used in logs. |
138
+ | `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. |
139
+ | `match.process` | No | Process name, case-insensitive, `.exe` ignored. |
140
+ | `match.title` | No | Case-insensitive substring of the window title. |
141
+ | `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. |
142
+ | `quietSeconds` | No | Per-agent override for the global `quietSeconds`. |
143
+
144
+ At least one of `match.process` and `match.title` is required. If both are
145
+ provided, both must match.
146
+
147
+ Environment overrides, useful for tests and alternate installs:
148
+
149
+ - `DOOMSCROLLER_HOME` — base directory for config, logs, and sessions.
150
+ - `DOOMSCROLLER_CONFIG` — explicit config file path.
151
+ - `CLAUDE_CONFIG_DIR` — Claude Code config directory used by `init claude-code`.
152
+ - `COMMANDCODE_CONFIG_DIR` — Command Code config directory used by `init command-code`.
153
+
154
+ Use `doomscroller config` to print the config path and the effective merged
155
+ config. Use `doomscroller config --path` for only the path or
156
+ `doomscroller config --json` for only the JSON document.
157
+
158
+ ## Safety
159
+
160
+ - **Opt-in.** doomscroller does not move windows until an adapter emits
161
+ `agent:started` (a hook, Enter in a registered IDE window, `doomscroller
162
+ start`, or the manual hotkey). There is no background observation of your
163
+ screen.
164
+ - **Force return.** `Ctrl+Alt+D` force-returns to the saved agent window while
165
+ working, and starts a session while idle. It is observed globally but not
166
+ swallowed, so the foreground application also receives it. Change it in the
167
+ config or set `hotkey: null` to disable it.
168
+ - **Hard cap.** `maxDoomscrollSeconds` (default 10 minutes) ends a session even
169
+ if the agent never reports finished. Set it to `0` to disable the cap.
170
+ - **Delay before switching.** `delaySeconds` (default 10 seconds) gives short
171
+ tasks a chance to finish before anything is minimized.
172
+ - **Failure isolation.** Adapters and hooks swallow errors, and the state
173
+ machine serializes events so overlapping callbacks cannot interleave window
174
+ operations. A failed browser launch or denied focus change does not crash the
175
+ agent.
176
+ - **Non-destructive window handling.** The browser is minimized, not closed.
177
+ The agent window handle is saved before minimizing and restored on finish,
178
+ force-return, cap, or `doomscroller stop`.
179
+ - **Local only.** IPC binds to `127.0.0.1`. doomscroller has no telemetry and
180
+ makes no external network requests beyond the browser you ask it to open.
181
+ - **Same elevation.** Run doomscroller at the same elevation as the agent.
182
+ Windows can block focus changes between different integrity levels.
183
+
184
+ ## Commands
185
+
186
+ | Command | What it does |
187
+ | --- | --- |
188
+ | `doomscroller init` | Create `~/.doomscroller.json` if it does not exist. |
189
+ | `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. |
190
+ | `doomscroller init command-code` | Create the config and merge the Command Code hooks into `~/.commandcode/settings.json` (`PreToolUse` → start, `Stop` → done). Existing hooks are preserved, and `settings.json.doomscroller.bak` is created when the file already exists. |
191
+ | `doomscroller start` | Emit `agent:started`. Hook-safe: exits quickly, never blocks the agent, and auto-starts the daemon if needed. |
192
+ | `doomscroller done` | Emit `agent:finished`. Same hook-safe behavior. |
193
+ | `doomscroller run -- <command>` | Run a terminal agent in a PTY. Enter starts, output resets the quiet timer, `quietSeconds` of silence or process exit finishes. |
194
+ | `doomscroller stats [--days N\|--all]` | Show doomscroll wait time per day from `sessions.jsonl`. Defaults to the last 14 days. |
195
+ | `doomscroller config [--path\|--json]` | Print the config path, whether it exists, and the effective merged config. |
196
+ | `doomscroller daemon` | Run the daemon in the foreground. Hooks and the wrapper normally auto-start it. |
197
+ | `doomscroller hotkey` | Alias for `doomscroller daemon`. |
198
+ | `doomscroller status` | Show `state=idle\|working\|done` and the current target. |
199
+ | `doomscroller stop` | Stop the daemon. If a session is active, restore the agent window first. |
200
+
201
+ `doomscroller --help` lists the same commands. `doomscroller --version` prints
202
+ the package version. `stats` shows time actually spent doomscrolling; sessions
203
+ that finish during the delay are counted with zero waiting time.
204
+
205
+ ## How it works
206
+
207
+ The core is a small state machine in `src/core/state-machine.ts`:
208
+
209
+ ```text
210
+ IDLE --agent:started--> WORKING --agent:finished--> DONE
211
+ | |
212
+ | wait delaySeconds | minimize browser
213
+ | save + minimize agent | restore + focus agent
214
+ | open next target | chime + log session
215
+ | |
216
+ +---- hotkey / max cap ----------+
217
+ ```
218
+
219
+ Adapters only emit the two events; they never move windows themselves. The
220
+ daemon hosts the adapters and exposes a localhost IPC endpoint:
221
+
222
+ - **hotkey** watches for the configured key.
223
+ - **hook** is the `doomscroller start` / `doomscroller done` pair, plus the
224
+ Claude Code and Command Code settings mergers.
225
+ - **cli-wrapper** runs a command in a PTY and derives start/finish from Enter,
226
+ output, silence, and process exit.
227
+ - **ide-activity** watches Enter in registered windows, then uses workspace
228
+ mtimes and process-tree CPU to decide when the agent is idle.
229
+
230
+ Window operations go through the `WindowController` interface
231
+ (`src/windows/window-controller.ts`). The Win32 implementation shells out to
232
+ `scripts/win32-window.ps1`, which uses `GetForegroundWindow`, `ShowWindow`, and
233
+ `SetForegroundWindow`. Before focusing, it attaches to the foreground thread and
234
+ taps Alt to release Windows' foreground lock.
235
+
236
+ Target rotation is held in memory and starts at the first target again when the
237
+ daemon restarts. Sessions are appended to `sessions.jsonl`, and `stats` groups
238
+ them by local finish date.
239
+
240
+ ## Troubleshooting
241
+
242
+ ### Windows focus is not returned to the agent
243
+
244
+ Windows refuses `SetForegroundWindow` from processes that do not own the
245
+ foreground lock. doomscroller applies the standard workaround: `AttachThreadInput`
246
+ plus a synthetic Alt tap before focusing, with one retry. Some elevated, UWP, or
247
+ custom-chrome windows still refuse. Run the daemon at the same elevation as the
248
+ agent; if the focus attempt is not confirmed, check
249
+ `~/.doomscroller/doomscroller.log` and use `Ctrl+Alt+D` to retry. If focus moves
250
+ when you did not expect it, raise `delaySeconds`, lower
251
+ `maxDoomscrollSeconds`, or set `hotkey: null`.
252
+
253
+ ### Claude Code hooks are not firing
254
+
255
+ Confirm that `doomscroller --version` works in the same shell Claude Code uses.
256
+ Re-run `doomscroller init claude-code`; it prints the settings path and the
257
+ events it added or found. Check `~/.claude/settings.json` (or the directory in
258
+ `CLAUDE_CONFIG_DIR`) for the three commands and make sure they point at this
259
+ installation's `dist/index.js`. Invalid settings JSON is never overwritten; fix
260
+ the JSON or restore `settings.json.doomscroller.bak`. Then check
261
+ `doomscroller status` and the daemon log. If the daemon cannot start, the hook
262
+ still exits cleanly, but no session begins.
263
+
264
+ ### Command Code hooks are not firing
265
+
266
+ Confirm that `doomscroller --version` works in the same shell Command Code uses.
267
+ Re-run `doomscroller init command-code`; it prints the settings path and the
268
+ events it added or found. Check `~/.commandcode/settings.json` (or the directory
269
+ in `COMMANDCODE_CONFIG_DIR`) for the `PreToolUse` and `Stop` groups. Invalid
270
+ settings JSON is never overwritten; fix the JSON or restore
271
+ `settings.json.doomscroller.bak`. Two behaviors are expected: hooks are skipped
272
+ while Command Code is in plan mode, and start fires on a turn's first tool call,
273
+ so a turn that makes no tool calls never starts a session. Check
274
+ `doomscroller status` and the daemon log.
275
+
276
+ ### `ide-activity` says "finished" too early
277
+
278
+ The IDE adapter is a heuristic. It finishes after `quietSeconds` with no
279
+ workspace mtime changes and CPU at or below `idleCpuPercent`. An agent that is
280
+ thinking with no file writes and little CPU looks idle. Increase the global or
281
+ per-agent `quietSeconds`, make sure `workspace` points at the real project, and
282
+ lower `idleCpuPercent` if small CPU bursts should count as activity. If the
283
+ agent offers hooks or runs in a terminal, the hook or cli-wrapper adapters are
284
+ more deterministic. Press Enter in the registered window to start a session.
285
+
286
+ ### The browser window was not detected
287
+
288
+ doomscroller only recognizes new windows from Edge, Chrome, Brave, Firefox,
289
+ Opera, Vivaldi, and Arc. Set `browser` to `edge` or `chrome`, or use `default`
290
+ if your default browser is in that list. A browser that reuses an existing
291
+ window instead of creating a new one may not produce a detectable handle.
292
+
293
+ ### The hotkey does nothing
294
+
295
+ The watcher polls `GetAsyncKeyState` every 15 ms. Test it with a real key press
296
+ while another application is focused; synthetic `SendKeys` input does not
297
+ reliably update the async key state in non-interactive sessions. Check the log,
298
+ make sure the daemon is running, and avoid remote-desktop or VM layers that
299
+ capture the keyboard.
300
+
301
+ ### `node-pty` fails to load
302
+
303
+ `doomscroller run` needs the native `node-pty` module. Run `npm rebuild
304
+ node-pty`. If that fails, install Visual Studio Build Tools with the C++ desktop
305
+ workload and rebuild. The other adapters do not need node-pty.
306
+
307
+ ### Stats are empty
308
+
309
+ Sessions are written by the daemon to `~/.doomscroller/sessions.jsonl` when a
310
+ session ends. Ensure at least one `start`/`finished` pair reached the daemon,
311
+ then run `doomscroller stats`. `doomscroller status` shows whether the daemon is
312
+ running.
313
+
314
+ ## Contributing
315
+
316
+ Contributions are welcome. See
317
+ [CONTRIBUTING.md](https://github.com/Gurugokul05/doomscroller/blob/main/CONTRIBUTING.md)
318
+ for the dev setup, adapter example, OS-support notes, and PR checklist. Bugs and
319
+ agent requests have issue templates in `.github/ISSUE_TEMPLATE/`.
320
+
321
+ ## License
322
+
323
+ MIT © 2026 Gurugokul. See
324
+ [LICENSE](https://github.com/Gurugokul05/doomscroller/blob/main/LICENSE).
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.HOOK_EVENTS = exports.hookCommand = void 0;
4
+ exports.hookCommands = hookCommands;
5
+ exports.mergeClaudeHooks = mergeClaudeHooks;
6
+ exports.installClaudeCodeHooks = installClaudeCodeHooks;
7
+ const paths_1 = require("../util/paths");
8
+ const hook_settings_1 = require("./hook-settings");
9
+ Object.defineProperty(exports, "hookCommand", { enumerable: true, get: function () { return hook_settings_1.hookCommand; } });
10
+ exports.HOOK_EVENTS = ['UserPromptSubmit', 'Stop', 'Notification'];
11
+ function hookCommands() {
12
+ const start = (0, hook_settings_1.hookCommand)('start');
13
+ const done = (0, hook_settings_1.hookCommand)('done');
14
+ return {
15
+ UserPromptSubmit: start,
16
+ Stop: done,
17
+ Notification: done,
18
+ };
19
+ }
20
+ const actionFor = (eventName) => (eventName === 'UserPromptSubmit' ? 'start' : 'done');
21
+ function mergeClaudeHooks(settings, commands) {
22
+ return (0, hook_settings_1.mergeHookSettings)(settings, commands, actionFor);
23
+ }
24
+ async function installClaudeCodeHooks(settingsPath = (0, paths_1.claudeSettingsPath)()) {
25
+ return (0, hook_settings_1.installHookSettings)(settingsPath, hookCommands(), actionFor);
26
+ }
27
+ //# 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":";;;AAeA,oCAQC;AAID,4CAKC;AAED,wDAIC;AAtCD,yCAAmD;AACnD,mDAOyB;AAEhB,4FALP,2BAAW,OAKO;AAGP,QAAA,WAAW,GAAG,CAAC,kBAAkB,EAAE,MAAM,EAAE,cAAc,CAAU,CAAC;AAEjF,SAAgB,YAAY;IAC1B,MAAM,KAAK,GAAG,IAAA,2BAAW,EAAC,OAAO,CAAC,CAAC;IACnC,MAAM,IAAI,GAAG,IAAA,2BAAW,EAAC,MAAM,CAAC,CAAC;IACjC,OAAO;QACL,gBAAgB,EAAE,KAAK;QACvB,IAAI,EAAE,IAAI;QACV,YAAY,EAAE,IAAI;KACnB,CAAC;AACJ,CAAC;AAED,MAAM,SAAS,GAAkB,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC,SAAS,KAAK,kBAAkB,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;AAEtG,SAAgB,gBAAgB,CAC9B,QAAiB,EACjB,QAAgC;IAEhC,OAAO,IAAA,iCAAiB,EAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,CAAC,CAAC;AAC1D,CAAC;AAEM,KAAK,UAAU,sBAAsB,CAC1C,eAAuB,IAAA,0BAAkB,GAAE;IAE3C,OAAO,IAAA,mCAAmB,EAAC,YAAY,EAAE,YAAY,EAAE,EAAE,SAAS,CAAC,CAAC;AACtE,CAAC"}
@@ -0,0 +1,230 @@
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.resolveExecutable = resolveExecutable;
7
+ exports.resolveCommandForPty = resolveCommandForPty;
8
+ exports.runCliWrapper = runCliWrapper;
9
+ const node_fs_1 = __importDefault(require("node:fs"));
10
+ const node_path_1 = __importDefault(require("node:path"));
11
+ const ipc_1 = require("../core/ipc");
12
+ const types_1 = require("../core/types");
13
+ const errors_1 = require("../util/errors");
14
+ const wrapper_tracker_1 = require("./wrapper-tracker");
15
+ function resolveExecutable(command) {
16
+ if (command.includes('/') || command.includes('\\')) {
17
+ return node_fs_1.default.existsSync(command) ? command : null;
18
+ }
19
+ const pathValue = process.env.PATH ?? '';
20
+ const directories = pathValue.split(node_path_1.default.delimiter).filter(Boolean);
21
+ const extensions = (process.env.PATHEXT ?? '.COM;.EXE;.BAT;.CMD')
22
+ .split(';')
23
+ .filter(Boolean);
24
+ if (!extensions.some((extension) => extension.toLowerCase() === '.ps1')) {
25
+ extensions.push('.PS1');
26
+ }
27
+ const hasExtension = /\.[^\\/]+$/.test(command);
28
+ for (const directory of directories) {
29
+ if (hasExtension) {
30
+ const candidate = node_path_1.default.join(directory, command);
31
+ if (node_fs_1.default.existsSync(candidate))
32
+ return candidate;
33
+ continue;
34
+ }
35
+ // Prefer real Windows executable extensions; the extensionless file in an
36
+ // npm install (C:\Program Files\nodejs\npm) is a Unix shell script.
37
+ for (const extension of [...extensions, '']) {
38
+ const candidate = node_path_1.default.join(directory, `${command}${extension}`);
39
+ if (node_fs_1.default.existsSync(candidate))
40
+ return candidate;
41
+ }
42
+ }
43
+ return null;
44
+ }
45
+ /**
46
+ * Resolves .cmd/.bat npm shims and .ps1 scripts so node-pty can launch them on
47
+ * Windows. Plain executables are spawned directly.
48
+ */
49
+ function resolveCommandForPty(command, args, platform = process.platform) {
50
+ if (platform !== 'win32')
51
+ return { file: command, args };
52
+ const resolved = resolveExecutable(command);
53
+ if (!resolved)
54
+ return { file: command, args };
55
+ const lower = resolved.toLowerCase();
56
+ if (lower.endsWith('.cmd') || lower.endsWith('.bat')) {
57
+ // node-pty launches .cmd/.bat shims through the Windows shell itself.
58
+ // Passing them to `cmd /c` as an argv array mangles quoted paths.
59
+ return { file: resolved, args };
60
+ }
61
+ if (lower.endsWith('.ps1')) {
62
+ return {
63
+ file: 'powershell.exe',
64
+ args: ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', resolved, ...args],
65
+ };
66
+ }
67
+ return { file: resolved, args };
68
+ }
69
+ function loadNodePty(logger) {
70
+ try {
71
+ // Loaded lazily so all non-PTY commands work even if the native module
72
+ // cannot be rebuilt on this machine.
73
+ return require('node-pty');
74
+ }
75
+ catch (error) {
76
+ logger.error(`node-pty is not available: ${(0, errors_1.errorMessage)(error)}`);
77
+ return null;
78
+ }
79
+ }
80
+ function terminalSize() {
81
+ const cols = process.stdout.columns && process.stdout.columns > 0 ? process.stdout.columns : 120;
82
+ const rows = process.stdout.rows && process.stdout.rows > 0 ? process.stdout.rows : 30;
83
+ return { cols, rows };
84
+ }
85
+ async function runCliWrapper(commandArgs, config, logger) {
86
+ if (commandArgs.length === 0) {
87
+ process.stderr.write('Usage: doomscroller run -- <command> [args...]\n');
88
+ process.stderr.write('Example: doomscroller run -- claude\n');
89
+ return 2;
90
+ }
91
+ const ptyModule = loadNodePty(logger);
92
+ if (!ptyModule) {
93
+ process.stderr.write('doomscroller run: node-pty is not available. Re-run "npm install" in the doomscroller directory.\n');
94
+ return 1;
95
+ }
96
+ const resolved = resolveCommandForPty(commandArgs[0], commandArgs.slice(1));
97
+ const size = terminalSize();
98
+ const env = {};
99
+ for (const [key, value] of Object.entries(process.env)) {
100
+ if (typeof value === 'string')
101
+ env[key] = value;
102
+ }
103
+ let pty;
104
+ try {
105
+ pty = ptyModule.spawn(resolved.file, resolved.args, {
106
+ name: 'xterm-256color',
107
+ cols: size.cols,
108
+ rows: size.rows,
109
+ cwd: process.cwd(),
110
+ env,
111
+ });
112
+ }
113
+ catch (error) {
114
+ process.stderr.write(`doomscroller run: could not start command: ${(0, errors_1.errorMessage)(error)}\n`);
115
+ return 1;
116
+ }
117
+ const client = new ipc_1.EventClient(config.port);
118
+ const tracker = new wrapper_tracker_1.WrapperActivityTracker({
119
+ quietMs: Math.max(1, config.quietSeconds) * 1000,
120
+ onStarted: () => client.emit(types_1.AGENT_STARTED),
121
+ onFinished: () => client.emit(types_1.AGENT_FINISHED),
122
+ });
123
+ const wasRaw = process.stdin.isTTY ? process.stdin.isRaw : false;
124
+ return new Promise((resolve) => {
125
+ let cleaned = false;
126
+ const onStdin = (chunk) => {
127
+ const text = typeof chunk === 'string' ? chunk : chunk.toString('utf8');
128
+ tracker.onInput(text);
129
+ try {
130
+ pty.write(text);
131
+ }
132
+ catch {
133
+ // The child may have exited.
134
+ }
135
+ };
136
+ const onResize = () => {
137
+ const next = terminalSize();
138
+ try {
139
+ pty.resize(next.cols, next.rows);
140
+ }
141
+ catch {
142
+ // ignore
143
+ }
144
+ };
145
+ const onSignal = () => {
146
+ try {
147
+ pty.kill();
148
+ }
149
+ catch {
150
+ // ignore
151
+ }
152
+ };
153
+ const cleanup = () => {
154
+ if (cleaned)
155
+ return;
156
+ cleaned = true;
157
+ try {
158
+ process.stdin.pause();
159
+ }
160
+ catch {
161
+ // ignore
162
+ }
163
+ if (process.stdin.isTTY) {
164
+ try {
165
+ process.stdin.setRawMode(Boolean(wasRaw));
166
+ }
167
+ catch {
168
+ // ignore
169
+ }
170
+ }
171
+ try {
172
+ process.stdin.off('data', onStdin);
173
+ }
174
+ catch {
175
+ // ignore
176
+ }
177
+ try {
178
+ process.stdout.off('resize', onResize);
179
+ }
180
+ catch {
181
+ // ignore
182
+ }
183
+ try {
184
+ process.off('SIGINT', onSignal);
185
+ process.off('SIGTERM', onSignal);
186
+ }
187
+ catch {
188
+ // ignore
189
+ }
190
+ tracker.dispose();
191
+ };
192
+ const dataSubscription = pty.onData((data) => {
193
+ try {
194
+ process.stdout.write(data);
195
+ }
196
+ catch {
197
+ // ignore
198
+ }
199
+ tracker.onOutput(data);
200
+ });
201
+ if (process.stdin.isTTY) {
202
+ try {
203
+ process.stdin.setRawMode(true);
204
+ }
205
+ catch {
206
+ // Not fatal; line-buffered input still works.
207
+ }
208
+ }
209
+ process.stdin.resume();
210
+ process.stdin.on('data', onStdin);
211
+ process.stdout.on('resize', onResize);
212
+ process.on('SIGINT', onSignal);
213
+ process.on('SIGTERM', onSignal);
214
+ pty.onExit(({ exitCode }) => {
215
+ tracker.onExit();
216
+ void (async () => {
217
+ await client.flush();
218
+ try {
219
+ dataSubscription.dispose();
220
+ }
221
+ catch {
222
+ // ignore
223
+ }
224
+ cleanup();
225
+ resolve(typeof exitCode === 'number' ? exitCode : 0);
226
+ })();
227
+ });
228
+ });
229
+ }
230
+ //# sourceMappingURL=cli-wrapper.js.map