- src/move.ts entry point with CLI parsing, --help, --version - src/cli.ts: parseCliArgs, printHelp, ParsedCliArgs, CliError, VERSION - src/config.ts: Config type, DEFAULT_CONFIG, resolveConfig - src/keeper.ts: synthetic-activity sweep + idle-watch loop - package.json bin entry + shebang for 'bun link' global install - install.sh: contributor bootstrap (will be repurposed; see DISTRIBUTION-PLAN.md for the end-user installer design) - DISTRIBUTION-PLAN.md captures the tabled end-user distribution work
Teams Status Keeper
Keeps Microsoft Teams (or any presence-tracking app) from marking you as "Away" by nudging the mouse cursor when the machine has been idle long enough to trigger an idle timeout.
Real user movement always wins: the script never fires while the user is actively using the mouse, and any synthetic sweep aborts the moment the cursor leaves the position the script just commanded.
Requirements
- Bun >= 1.0.0
- macOS, Linux, or Windows (relies on
@nut-tree-fork/nut-jsfor cross-platform mouse + screen control) - On macOS: Accessibility permission for the terminal running Bun (System Settings > Privacy & Security > Accessibility)
Install
./install.sh
The installer verifies bun is on PATH and runs bun install. It does
not auto-install Bun; if Bun is missing it prints a hard error pointing at
https://bun.sh.
Run
bun run start
Or directly:
bun run src/move.ts
Or install it as a global move command via Bun's bin-linking:
bun link
move
bun link registers the bin.move entry from package.json (./src/move.ts,
which carries #!/usr/bin/env bun) as an executable on your PATH.
Stop with Ctrl+C. If SIGINT arrives mid-sweep, the cursor stays at
whichever step was last commanded — by design.
Usage
Usage: move [options]
Options:
-h, --help Show this help and exit.
-v, --version Print version and exit.
-m, --move-interval <seconds> Idle time before a sweep fires. Default: 240.
-c, --check-interval <seconds> Cursor poll cadence. Default: 10.
-d, --step-delay <ms> Pause between synthetic steps. Default: 50.
-n, --step-count <pixels> Steps per sweep. Default: 250.
-V, --verbose Log every sweep, interrupt, and bounds event
(default prints only the startup banner).
Run bun run src/move.ts --help for the full text.
All flags are wired. Numeric overrides are layered onto the defaults via
resolveConfig in src/config.ts; time-valued inputs (-m, -c) are
expressed in seconds at the CLI boundary and converted to milliseconds
internally.
Logging is quiet by default: only the startup banner ("Teams Status
Keeper started…") and any error from an unhandled rejection print on a
default run. -V / --verbose opens up per-sweep, user-interrupt, and
out-of-bounds events.
Invalid input (unknown flag, missing value, non-positive number) prints an
error to stderr and exits with code 2.
How it works
The source lives under src/, split into an entry point plus three logic
modules:
src/move.tsis a thin entry point: parses args, dispatches--help/--version, resolves the runtime config, and callsrunKeeper(config, verbose).src/cli.tsowns argument parsing, validation, and help/version output.src/config.tsexports theConfigtype,DEFAULT_CONFIG, and theresolveConfigoverlay function.src/keeper.tsowns the synthetic-activity sweep and the idle-watch loop.
Defaults live in src/config.ts as DEFAULT_CONFIG:
| Field | Default | CLI flag | Purpose |
|---|---|---|---|
moveInterval |
4 * 60_000 |
-m, --move-interval |
Idle time (ms) required before a synthetic sweep fires. |
checkInterval |
10_000 |
-c, --check-interval |
How often (ms) the main loop polls the cursor for real activity. |
stepDelay |
50 |
-d, --step-delay |
Pause (ms) between individual synthetic steps in a sweep. |
stepCount |
250 |
-n, --step-count |
Pixel-steps per sweep. |
-m and -c are accepted in seconds at the CLI; resolveConfig converts
to milliseconds before handing the resolved Config to runKeeper.
Main loop (runKeeper)
- Print the startup banner (unconditional).
- Snapshot
lastPosandlastActivity = now. - Every
config.checkInterval:- If the cursor moved since the last check, the user is active — reset
lastActivityandlastPos, continue. - Otherwise, if
now - lastActivity >= config.moveInterval, callsimulateActivityand reset the idleness clock.
- If the cursor moved since the last check, the user is active — reset
Synthetic sweep (simulateActivity)
- Read the starting position and current screen dimensions.
- Pick a horizontal direction (
dx = +1if there's room to the right, else-1) so the sweep stays on-screen. Vertical isdy = 0for now. - For each of
config.stepCountsteps:- Compute and bounds-check the next target.
- Move the cursor there, sleep
config.stepDelay. - Re-read the cursor. If it isn't where we put it, the user moved it —
log (when
--verbose) and return early without snapping back.
- On a clean full sweep, restore the cursor to its starting position so the next idle-check sees "no movement" and doesn't misread the synthetic activity as real user input.
Why mouse.config.autoDelayMs = 0
nut.js inserts a 100ms delay after every action by default. With two mouse
calls per step that would silently more-than-double the duration of a
sweep. The script controls cadence itself via config.stepDelay, so the
implicit delay is disabled at module load (a side effect of importing
keeper.ts).
Files
| File | Purpose |
|---|---|
src/move.ts |
CLI entry point: parses args, dispatches help/version, starts the loop. |
src/cli.ts |
Argument parsing, validation, and help/version output. |
src/config.ts |
Config type, DEFAULT_CONFIG, and resolveConfig overlay. |
src/keeper.ts |
Synthetic-activity sweep and idle-watch loop. |
install.sh |
Bootstrap: verify Bun, bun install, print macOS permission hint. |
package.json |
Bun project manifest. Single runtime dep: @nut-tree-fork/nut-js. |
tsconfig.json |
Strict TypeScript config tuned for Bun (ESNext, bundler resolution). |
bun.lock |
Bun's lockfile. Commit this. |
LICENSE |
GPLv3 license text. |
CHANGES.md |
History of the post-port review fixes. |
MouseMover.java |
Original Java source, kept for reference. |
License
GPL-3.0-only. See LICENSE.