@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.
- package/CHANGELOG.md +57 -0
- package/LICENSE +21 -0
- package/README.md +323 -2
- package/dist/adapters/claude-code-hooks.js +27 -0
- package/dist/adapters/claude-code-hooks.js.map +1 -0
- package/dist/adapters/cli-wrapper.js +230 -0
- package/dist/adapters/cli-wrapper.js.map +1 -0
- package/dist/adapters/command-code-hooks.js +32 -0
- package/dist/adapters/command-code-hooks.js.map +1 -0
- package/dist/adapters/file-activity.js +78 -0
- package/dist/adapters/file-activity.js.map +1 -0
- package/dist/adapters/hook-settings.js +164 -0
- package/dist/adapters/hook-settings.js.map +1 -0
- package/dist/adapters/hook.js +17 -0
- package/dist/adapters/hook.js.map +1 -0
- package/dist/adapters/hotkey.js +50 -0
- package/dist/adapters/hotkey.js.map +1 -0
- package/dist/adapters/ide-activity.js +245 -0
- package/dist/adapters/ide-activity.js.map +1 -0
- package/dist/adapters/ports.js +3 -0
- package/dist/adapters/ports.js.map +1 -0
- package/dist/adapters/wrapper-tracker.js +77 -0
- package/dist/adapters/wrapper-tracker.js.map +1 -0
- package/dist/cli.js +343 -0
- package/dist/cli.js.map +1 -0
- package/dist/core/config.js +192 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/daemon.js +156 -0
- package/dist/core/daemon.js.map +1 -0
- package/dist/core/ipc.js +215 -0
- package/dist/core/ipc.js.map +1 -0
- package/dist/core/logger.js +53 -0
- package/dist/core/logger.js.map +1 -0
- package/dist/core/sound.js +38 -0
- package/dist/core/sound.js.map +1 -0
- package/dist/core/state-machine.js +272 -0
- package/dist/core/state-machine.js.map +1 -0
- package/dist/core/stats.js +109 -0
- package/dist/core/stats.js.map +1 -0
- package/dist/core/targets.js +27 -0
- package/dist/core/targets.js.map +1 -0
- package/dist/core/types.js +6 -0
- package/dist/core/types.js.map +1 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/util/errors.js +16 -0
- package/dist/util/errors.js.map +1 -0
- package/dist/util/paths.js +77 -0
- package/dist/util/paths.js.map +1 -0
- package/dist/windows/cpu-probe.js +108 -0
- package/dist/windows/cpu-probe.js.map +1 -0
- package/dist/windows/key-listener.js +198 -0
- package/dist/windows/key-listener.js.map +1 -0
- package/dist/windows/win32.js +158 -0
- package/dist/windows/win32.js.map +1 -0
- package/dist/windows/window-controller.js +3 -0
- package/dist/windows/window-controller.js.map +1 -0
- package/package.json +57 -4
- package/scripts/cpu-sampler.ps1 +83 -0
- package/scripts/key-watcher.ps1 +132 -0
- 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
|
-
#
|
|
1
|
+
# doomscroller
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Doomscroll while your AI coding agent works, then get pulled back when the agent finishes.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@gurugokul/doomscroller)
|
|
6
|
+
[](https://github.com/Gurugokul05/doomscroller/blob/main/LICENSE)
|
|
7
|
+
[](#quick-start)
|
|
8
|
+
|
|
9
|
+

|
|
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
|