killm 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Aidan Neel
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,174 @@
1
+ # killm
2
+
3
+ **Temporarily block your machine from reaching LLM services, to curb AI dependency.**
4
+
5
+ `killm` is a zero-dependency CLI. You give it a duration and a scope, and for
6
+ that window it points the relevant LLM hostnames at a dead address in your
7
+ system hosts file. When the timer runs out — or you hit `Ctrl+C` — it restores
8
+ everything automatically.
9
+
10
+ ```bash
11
+ npx killm for 1h --agents # no coding agents for an hour
12
+ npx killm 30m --web # no chat websites for 30 minutes
13
+ npx killm for 2h --all # cut everything off for two hours
14
+ ```
15
+
16
+ No account, no proxy, no daemon. Just a timed edit to your hosts file that
17
+ cleans up after itself.
18
+
19
+ ---
20
+
21
+ ## Why
22
+
23
+ It's easy to reach for an LLM by reflex — autocomplete in the editor, a chat tab
24
+ for every small question — and lose the muscle of thinking a problem through.
25
+ `killm` lets you put a hard, time-boxed wall between you and that reflex without
26
+ uninstalling your tools or changing your accounts. The wall comes down on its
27
+ own; you don't have to remember to turn anything back on.
28
+
29
+ ## Install / run
30
+
31
+ No install needed — run it on demand with `npx`:
32
+
33
+ ```bash
34
+ npx killm for 1h --agents
35
+ ```
36
+
37
+ Editing the hosts file needs elevated privileges:
38
+
39
+ - **macOS / Linux:** prefix with `sudo` → `sudo npx killm for 1h --agents`
40
+ - **Windows:** run from an **Administrator** terminal
41
+
42
+ ## The key idea: agents vs. web
43
+
44
+ The whole point of `killm` is that an agentic coding tool and a chat website
45
+ talk to **different hostnames**, so you can cut one without losing the other:
46
+
47
+ | Tool | Hostname | Scope |
48
+ | --------------------------- | ------------------- | ---------- |
49
+ | Claude Code, Aider, raw API | `api.anthropic.com` | `--agents` |
50
+ | Claude chat website | `claude.ai` | `--web` |
51
+ | OpenAI API, Codex | `api.openai.com` | `--agents` |
52
+ | ChatGPT website | `chatgpt.com` | `--web` |
53
+
54
+ So:
55
+
56
+ - **`--agents`** blocks API endpoints and dedicated coding-assistant backends
57
+ (Cursor, GitHub Copilot, Codeium/Windsurf, Cody, Tabnine, Continue, …). Your
58
+ coding agents go dark, but you can still open `claude.ai` or `chatgpt.com` in a
59
+ browser if you genuinely need to.
60
+ - **`--web`** blocks the consumer chat websites (`claude.ai`, `chatgpt.com`,
61
+ `gemini.google.com`, `perplexity.ai`, …). Your API-based tooling keeps working.
62
+ - **`--all`** blocks both. This is the default if you don't pass a scope.
63
+
64
+ ## Usage
65
+
66
+ ```
67
+ npx killm for <duration> [scope] [options]
68
+ npx killm <duration> [scope] [options]
69
+ ```
70
+
71
+ The word `for` is optional sugar — `killm for 1h --agents` and
72
+ `killm 1h --agents` are identical.
73
+
74
+ ### Duration
75
+
76
+ Combine units `d` / `h` / `m` / `s`:
77
+
78
+ ```
79
+ 90s 30m 1h 1h30m 2d
80
+ ```
81
+
82
+ A bare number is treated as minutes (`45` = `45m`).
83
+
84
+ ### Scope
85
+
86
+ | Flag | Blocks |
87
+ | ---------- | ---------------------------------------- |
88
+ | `--agents` | Agentic coding tools + raw API endpoints |
89
+ | `--web` | Consumer chat websites |
90
+ | `--all` | Both (default) |
91
+
92
+ You can combine `--agents` and `--web`; that's the same as `--all`.
93
+
94
+ ### Options
95
+
96
+ | Option | Effect |
97
+ | ----------------- | ------------------------------------------------ |
98
+ | `--restore` | Lift any active block right now and exit |
99
+ | `--status` | Report whether a block is currently active |
100
+ | `--list` | Print the hostnames a given scope would block |
101
+ | `--dry-run` | Show what would change without touching anything |
102
+ | `-y`, `--yes` | Skip the confirmation prompt |
103
+ | `-h`, `--help` | Show help |
104
+ | `-v`, `--version` | Show version |
105
+
106
+ ### Examples
107
+
108
+ ```bash
109
+ npx killm for 1h --agents # coding agents off for an hour
110
+ npx killm 30m --web # chat websites off for 30 minutes
111
+ npx killm for 2h --all # everything off for two hours
112
+ npx killm --list --agents # see exactly what --agents blocks
113
+ npx killm for 25m --web --dry-run # preview, change nothing
114
+ npx killm --restore # end an active block early
115
+ npx killm --status # is a block running right now?
116
+ ```
117
+
118
+ ## How it works
119
+
120
+ `killm` writes a clearly-marked block into your system hosts file
121
+ (`/etc/hosts`, or `…\System32\drivers\etc\hosts` on Windows) that points each
122
+ target hostname at `0.0.0.0` (and `::1` for IPv6):
123
+
124
+ ```
125
+ # >>> killm block (do not edit between markers) >>>
126
+ # added by killm at 2026-06-09T17:58:07.849Z
127
+ # auto-removed at 2026-06-09T18:58:07.849Z (or when killm exits)
128
+ 0.0.0.0 api.anthropic.com
129
+ ::1 api.anthropic.com
130
+ ...
131
+ # <<< killm block <<<
132
+ ```
133
+
134
+ It then flushes the OS DNS cache so the change takes effect immediately. When
135
+ the timer expires, or on `Ctrl+C`, `SIGTERM`, or process exit, it strips that
136
+ block back out and flushes again. The markers mean it only ever touches its own
137
+ lines — your existing hosts entries are left alone.
138
+
139
+ If something goes wrong and a block is left behind, `sudo npx killm --restore`
140
+ (or the Administrator equivalent) cleans it up.
141
+
142
+ ## Limitations & honesty
143
+
144
+ `killm` is a **speed bump, not a vault.** It's designed to defeat reflex, not a
145
+ determined workaround:
146
+
147
+ - It blocks by **hostname**, so it can't block a single path on a shared domain.
148
+ - Anyone with admin rights can edit the hosts file back, use a different DNS
149
+ resolver, a VPN, or a phone. That's fine — the goal is to make the easy thing
150
+ hard, not to make it impossible.
151
+ - Hostname lists drift as providers add endpoints. `--list` shows the current
152
+ set; PRs to keep it current are welcome.
153
+
154
+ ## Development
155
+
156
+ The CLI is written in TypeScript (ESM) with zero runtime dependencies; tests
157
+ run against the compiled output, so they exercise exactly what ships.
158
+
159
+ ```bash
160
+ npm ci # install dev toolchain
161
+ npm run build # compile TypeScript to dist/
162
+ npm test # build + run the test suite (node:test)
163
+ npm run lint # eslint
164
+ npm run format # prettier --write
165
+ npm run typecheck # tsc --noEmit
166
+ ```
167
+
168
+ CI runs lint, format, typecheck, and the test matrix (Linux/macOS/Windows ×
169
+ Node 18/20/22) on every push and PR. Publishing happens automatically when a
170
+ GitHub release is published (requires the `NPM_TOKEN` repo secret).
171
+
172
+ ## License
173
+
174
+ MIT © Aidan Neel
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env node
2
+ import { main } from './index.js';
3
+ // Exit quietly if our output is piped into something that closes early
4
+ // (e.g. `killm --list | head`), rather than crashing on EPIPE.
5
+ process.stdout.on('error', (err) => {
6
+ if (err?.code === 'EPIPE')
7
+ process.exit(0);
8
+ throw err;
9
+ });
10
+ main(process.argv.slice(2))
11
+ .then((code) => {
12
+ process.exitCode = code || 0;
13
+ })
14
+ .catch((err) => {
15
+ const detail = err instanceof Error ? (err.stack ?? err.message) : String(err);
16
+ process.stderr.write(`killm: unexpected error: ${detail}\n`);
17
+ process.exitCode = 1;
18
+ });
@@ -0,0 +1,136 @@
1
+ import fs from 'node:fs';
2
+ import { parseDuration } from './duration.js';
3
+ export const VERSION = JSON.parse(fs.readFileSync(new URL('../../package.json', import.meta.url), 'utf8')).version;
4
+ export const HELP = `killm — temporarily block your machine from reaching LLM services
5
+
6
+ USAGE
7
+ npx killm for <duration> [scope] [options]
8
+ npx killm <duration> [scope] [options]
9
+
10
+ The word "for" is optional sugar: killm for 1h --agents == killm 1h --agents
11
+
12
+ DURATION
13
+ Combine units d/h/m/s, e.g. 90s 30m 1h 1h30m 2d
14
+ A bare number means minutes, e.g. 45 == 45m
15
+
16
+ SCOPE (pick one or more; default is --all)
17
+ --agents Block agentic coding + raw API endpoints (api.anthropic.com,
18
+ api.openai.com, Cursor, Copilot, Codeium, ...). Leaves the
19
+ chat websites (claude.ai, chatgpt.com) reachable.
20
+ --web Block consumer chat websites (claude.ai, chatgpt.com,
21
+ gemini.google.com, perplexity.ai, ...). Leaves API endpoints
22
+ reachable so other tooling keeps working.
23
+ --all Block both of the above. This is the default if no scope is given.
24
+
25
+ OPTIONS
26
+ --restore Remove any active killm block right now and exit.
27
+ --status Show whether a block is currently active and exit.
28
+ --list Print the hostnames that would be blocked and exit.
29
+ --dry-run Show what would change without touching the hosts file.
30
+ -y, --yes Don't prompt for confirmation.
31
+ -h, --help Show this help.
32
+ -v, --version Show version.
33
+
34
+ EXAMPLES
35
+ npx killm for 1h --agents Block coding agents for an hour.
36
+ npx killm 30m --web Block chat websites for 30 minutes.
37
+ npx killm for 2h --all Block everything for two hours.
38
+ npx killm --restore Lift an active block early.
39
+
40
+ NOTE
41
+ Editing the hosts file requires elevated privileges. Run with sudo on
42
+ macOS/Linux, or from an Administrator terminal on Windows.
43
+ `;
44
+ /**
45
+ * Parse argv (without node/script) into a structured command.
46
+ */
47
+ export function parseArgs(argv) {
48
+ const result = {
49
+ command: 'run',
50
+ scope: { agents: false, web: false, all: false },
51
+ dryRun: false,
52
+ yes: false,
53
+ };
54
+ let explicit;
55
+ let durationInput;
56
+ for (const arg of argv) {
57
+ switch (arg) {
58
+ case '-h':
59
+ case '--help':
60
+ return { ...result, command: 'help' };
61
+ case '-v':
62
+ case '--version':
63
+ return { ...result, command: 'version' };
64
+ case '--status':
65
+ explicit = 'status';
66
+ break;
67
+ case '--list':
68
+ explicit = 'list';
69
+ break;
70
+ case '--restore':
71
+ case '--unblock':
72
+ explicit = 'restore';
73
+ break;
74
+ case '--dry-run':
75
+ result.dryRun = true;
76
+ break;
77
+ case '-y':
78
+ case '--yes':
79
+ result.yes = true;
80
+ break;
81
+ case '--agents':
82
+ result.scope.agents = true;
83
+ break;
84
+ case '--web':
85
+ result.scope.web = true;
86
+ break;
87
+ case '--all':
88
+ result.scope.all = true;
89
+ break;
90
+ case 'for':
91
+ // Noise word, ignore.
92
+ break;
93
+ default:
94
+ if (arg.startsWith('-')) {
95
+ return { ...result, command: 'help', error: `unknown option: ${arg}` };
96
+ }
97
+ if (durationInput !== undefined) {
98
+ return { ...result, command: 'help', error: `unexpected argument: ${arg}` };
99
+ }
100
+ durationInput = arg;
101
+ break;
102
+ }
103
+ }
104
+ // Terminal sub-commands that don't need a duration.
105
+ if (explicit === 'restore')
106
+ return { ...result, command: 'restore' };
107
+ if (explicit === 'status')
108
+ return { ...result, command: 'status' };
109
+ // --list needs to know the scope but not a duration.
110
+ if (explicit === 'list') {
111
+ if (!result.scope.agents && !result.scope.web && !result.scope.all) {
112
+ result.scope.all = true;
113
+ }
114
+ return { ...result, command: 'list' };
115
+ }
116
+ // From here we're running a block, which requires a duration.
117
+ if (durationInput === undefined) {
118
+ return {
119
+ ...result,
120
+ command: 'help',
121
+ error: 'a duration is required, e.g. "killm for 1h --agents"',
122
+ };
123
+ }
124
+ let durationMs;
125
+ try {
126
+ durationMs = parseDuration(durationInput);
127
+ }
128
+ catch (err) {
129
+ return { ...result, command: 'help', error: err.message };
130
+ }
131
+ // Default scope: everything.
132
+ if (!result.scope.agents && !result.scope.web && !result.scope.all) {
133
+ result.scope.all = true;
134
+ }
135
+ return { ...result, command: 'run', durationInput, durationMs };
136
+ }
@@ -0,0 +1,69 @@
1
+ export const UNITS = {
2
+ s: 1000,
3
+ m: 60 * 1000,
4
+ h: 60 * 60 * 1000,
5
+ d: 24 * 60 * 60 * 1000,
6
+ };
7
+ /**
8
+ * Parse a human duration string into milliseconds.
9
+ *
10
+ * Accepts a single unit ("30m", "1h", "90s", "2d") or a combination
11
+ * ("1h30m", "1d12h"). A bare number is treated as minutes ("45" -> 45m).
12
+ *
13
+ * @throws {Error} when the string cannot be parsed
14
+ */
15
+ export function parseDuration(input) {
16
+ if (input == null)
17
+ throw new Error('no duration given');
18
+ const raw = String(input).trim().toLowerCase();
19
+ if (raw === '')
20
+ throw new Error('empty duration');
21
+ // Bare number -> minutes.
22
+ if (/^\d+$/.test(raw)) {
23
+ const minutes = Number(raw) * UNITS.m;
24
+ if (minutes <= 0)
25
+ throw new Error(`duration must be greater than zero: "${input}"`);
26
+ return minutes;
27
+ }
28
+ const re = /(\d+)\s*(d|h|m|s)/g;
29
+ let total = 0;
30
+ let matched = false;
31
+ let consumed = 0;
32
+ let match;
33
+ while ((match = re.exec(raw)) !== null) {
34
+ matched = true;
35
+ consumed += match[0].length;
36
+ total += Number(match[1]) * UNITS[match[2]];
37
+ }
38
+ // Reject leftover junk like "1h!" or "abc".
39
+ if (!matched || consumed !== raw.replace(/\s/g, '').length) {
40
+ throw new Error(`could not parse duration: "${input}"`);
41
+ }
42
+ if (total <= 0)
43
+ throw new Error(`duration must be greater than zero: "${input}"`);
44
+ return total;
45
+ }
46
+ /**
47
+ * Format milliseconds back into a compact human string, e.g. "1h 30m 5s".
48
+ */
49
+ export function formatDuration(ms) {
50
+ if (ms <= 0)
51
+ return '0s';
52
+ let remaining = Math.round(ms / 1000); // whole seconds
53
+ const days = Math.floor(remaining / 86400);
54
+ remaining %= 86400;
55
+ const hours = Math.floor(remaining / 3600);
56
+ remaining %= 3600;
57
+ const minutes = Math.floor(remaining / 60);
58
+ const seconds = remaining % 60;
59
+ const parts = [];
60
+ if (days)
61
+ parts.push(`${days}d`);
62
+ if (hours)
63
+ parts.push(`${hours}h`);
64
+ if (minutes)
65
+ parts.push(`${minutes}m`);
66
+ if (seconds || parts.length === 0)
67
+ parts.push(`${seconds}s`);
68
+ return parts.join(' ');
69
+ }
@@ -0,0 +1,161 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { execFile } from 'node:child_process';
5
+ export const BEGIN = '# >>> killm block (do not edit between markers) >>>';
6
+ export const END = '# <<< killm block <<<';
7
+ const SINK4 = '0.0.0.0';
8
+ const SINK6 = '::1';
9
+ function errnoCode(err) {
10
+ return err?.code;
11
+ }
12
+ /**
13
+ * Absolute path to the OS hosts file.
14
+ * KILLM_HOSTS_PATH overrides it (used by the test suite).
15
+ */
16
+ export function hostsPath() {
17
+ if (process.env.KILLM_HOSTS_PATH)
18
+ return process.env.KILLM_HOSTS_PATH;
19
+ if (process.platform === 'win32') {
20
+ const root = process.env.SystemRoot || 'C:\\Windows';
21
+ return path.join(root, 'System32', 'drivers', 'etc', 'hosts');
22
+ }
23
+ return '/etc/hosts';
24
+ }
25
+ /**
26
+ * Read the current hosts file as text. Returns '' if it does not exist.
27
+ */
28
+ export function readHosts() {
29
+ try {
30
+ return fs.readFileSync(hostsPath(), 'utf8');
31
+ }
32
+ catch (err) {
33
+ if (errnoCode(err) === 'ENOENT')
34
+ return '';
35
+ throw err;
36
+ }
37
+ }
38
+ /**
39
+ * Strip any existing killm-managed block from hosts text.
40
+ */
41
+ export function stripBlock(text) {
42
+ const begin = text.indexOf(BEGIN);
43
+ if (begin === -1)
44
+ return text;
45
+ const end = text.indexOf(END, begin);
46
+ if (end === -1) {
47
+ // Malformed (begin without end): drop everything from the marker on.
48
+ return text.slice(0, begin).replace(/\n+$/, '\n');
49
+ }
50
+ const before = text.slice(0, begin);
51
+ const after = text.slice(end + END.length);
52
+ return (before.replace(/\n+$/, '\n') + after.replace(/^\n+/, '')).replace(/\n{3,}/g, '\n\n');
53
+ }
54
+ /**
55
+ * Build the killm block text for the given hostnames.
56
+ */
57
+ export function buildBlock(hosts, opts = {}) {
58
+ const lines = [BEGIN];
59
+ lines.push(`# added by killm at ${new Date().toISOString()}`);
60
+ if (opts.until) {
61
+ lines.push(`# auto-removed at ${opts.until.toISOString()} (or when killm exits)`);
62
+ }
63
+ for (const h of hosts) {
64
+ lines.push(`${SINK4}\t${h}`);
65
+ lines.push(`${SINK4}\twww.${h}`.replace('www.www.', 'www.'));
66
+ lines.push(`${SINK6}\t${h}`);
67
+ }
68
+ lines.push(END);
69
+ return lines.join('\n');
70
+ }
71
+ /**
72
+ * Atomically write hosts text by writing to a temp file then renaming.
73
+ * Falls back to a direct write if rename across devices fails.
74
+ */
75
+ function writeHosts(text) {
76
+ const target = hostsPath();
77
+ const normalized = text.endsWith('\n') ? text : text + '\n';
78
+ const tmp = path.join(os.tmpdir(), `killm-hosts-${process.pid}-${Date.now()}.tmp`);
79
+ fs.writeFileSync(tmp, normalized, { mode: 0o644 });
80
+ try {
81
+ fs.renameSync(tmp, target);
82
+ }
83
+ catch (err) {
84
+ if (errnoCode(err) === 'EXDEV') {
85
+ // Temp dir is on a different filesystem; write in place instead.
86
+ fs.writeFileSync(target, normalized, { mode: 0o644 });
87
+ fs.unlinkSync(tmp);
88
+ }
89
+ else {
90
+ try {
91
+ fs.unlinkSync(tmp);
92
+ }
93
+ catch {
94
+ /* ignore */
95
+ }
96
+ throw err;
97
+ }
98
+ }
99
+ }
100
+ /**
101
+ * Apply a block for the given hostnames. Removes any prior killm block first
102
+ * so repeated runs stay idempotent.
103
+ */
104
+ export function applyBlock(hosts, opts = {}) {
105
+ const current = stripBlock(readHosts());
106
+ const base = current.replace(/\n+$/, '');
107
+ const next = (base ? base + '\n\n' : '') + buildBlock(hosts, opts) + '\n';
108
+ writeHosts(next);
109
+ }
110
+ /**
111
+ * Remove the killm block from the hosts file. No-op if absent.
112
+ *
113
+ * @returns whether a block was present and removed
114
+ */
115
+ export function removeBlock() {
116
+ const current = readHosts();
117
+ if (!current.includes(BEGIN))
118
+ return false;
119
+ writeHosts(stripBlock(current));
120
+ return true;
121
+ }
122
+ /**
123
+ * Whether a killm block is currently present.
124
+ */
125
+ export function isBlocked() {
126
+ return readHosts().includes(BEGIN);
127
+ }
128
+ /**
129
+ * Best-effort DNS cache flush so the new hosts entries take effect immediately.
130
+ * Silently ignores failures — the block still works, it may just take a moment.
131
+ */
132
+ export function flushDns() {
133
+ // Pointless (and slow) when operating on a fake hosts file in tests.
134
+ if (process.env.KILLM_HOSTS_PATH)
135
+ return Promise.resolve();
136
+ const run = (cmd, args) => new Promise((resolve) => {
137
+ execFile(cmd, args, () => resolve());
138
+ });
139
+ if (process.platform === 'darwin') {
140
+ return run('dscacheutil', ['-flushcache']).then(() => run('killall', ['-HUP', 'mDNSResponder']));
141
+ }
142
+ if (process.platform === 'win32') {
143
+ return run('ipconfig', ['/flushdns']);
144
+ }
145
+ // Linux: try the common resolvers; whichever exists wins, the rest no-op.
146
+ return run('resolvectl', ['flush-caches'])
147
+ .then(() => run('systemd-resolve', ['--flush-caches']))
148
+ .then(() => run('systemctl', ['restart', 'nscd']));
149
+ }
150
+ /**
151
+ * True when the process likely has the privileges needed to edit the hosts
152
+ * file (root on POSIX). On Windows we can't cheaply tell, so assume yes and
153
+ * let the write surface EPERM/EACCES if not.
154
+ */
155
+ export function hasPrivileges() {
156
+ if (process.env.KILLM_HOSTS_PATH)
157
+ return true; // test override, no root needed
158
+ if (process.platform === 'win32')
159
+ return true;
160
+ return typeof process.getuid === 'function' && process.getuid() === 0;
161
+ }
@@ -0,0 +1,233 @@
1
+ import readline from 'node:readline';
2
+ import { parseArgs, HELP, VERSION } from './cli.js';
3
+ import { resolveTargets } from './targets.js';
4
+ import { formatDuration } from './duration.js';
5
+ import * as hosts from './hosts.js';
6
+ // Minimal ANSI styling that degrades to plain text when not a TTY.
7
+ const tty = process.stdout.isTTY === true;
8
+ const c = {
9
+ bold: (s) => (tty ? `\x1b[1m${s}\x1b[0m` : s),
10
+ dim: (s) => (tty ? `\x1b[2m${s}\x1b[0m` : s),
11
+ red: (s) => (tty ? `\x1b[31m${s}\x1b[0m` : s),
12
+ green: (s) => (tty ? `\x1b[32m${s}\x1b[0m` : s),
13
+ yellow: (s) => (tty ? `\x1b[33m${s}\x1b[0m` : s),
14
+ cyan: (s) => (tty ? `\x1b[36m${s}\x1b[0m` : s),
15
+ };
16
+ function out(msg = '') {
17
+ process.stdout.write(msg + '\n');
18
+ }
19
+ function err(msg = '') {
20
+ process.stderr.write(msg + '\n');
21
+ }
22
+ function errnoCode(e) {
23
+ return e?.code;
24
+ }
25
+ function scopeLabel(scope) {
26
+ if (scope.all)
27
+ return 'everything (agents + web)';
28
+ const parts = [];
29
+ if (scope.agents)
30
+ parts.push('agentic coding + APIs');
31
+ if (scope.web)
32
+ parts.push('chat websites');
33
+ return parts.join(' + ') || 'nothing';
34
+ }
35
+ function privilegeHint() {
36
+ if (process.platform === 'win32') {
37
+ return 'Run this from an Administrator terminal (right-click > Run as administrator).';
38
+ }
39
+ return 'Re-run with sudo, e.g. sudo npx killm for 1h --agents';
40
+ }
41
+ async function confirm(question) {
42
+ if (!process.stdin.isTTY)
43
+ return true; // non-interactive: assume yes
44
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
45
+ try {
46
+ const answer = await new Promise((resolve) => rl.question(question, resolve));
47
+ return /^y(es)?$/i.test(answer.trim());
48
+ }
49
+ finally {
50
+ rl.close();
51
+ }
52
+ }
53
+ /**
54
+ * Render a single-line live countdown that rewrites in place on a TTY.
55
+ */
56
+ function makeCountdown(endTime) {
57
+ return function render() {
58
+ const remaining = Math.max(0, endTime - Date.now());
59
+ const text = ` ${c.cyan('⛔ blocked')} — ${c.bold(formatDuration(remaining))} remaining ${c.dim('(Ctrl+C to lift early)')}`;
60
+ if (tty) {
61
+ readline.clearLine(process.stdout, 0);
62
+ readline.cursorTo(process.stdout, 0);
63
+ process.stdout.write(text);
64
+ }
65
+ };
66
+ }
67
+ async function runBlock(parsed) {
68
+ const durationMs = parsed.durationMs;
69
+ const targets = resolveTargets(parsed.scope);
70
+ out();
71
+ out(`${c.bold('killm')} ${c.dim('v' + VERSION)}`);
72
+ out(` scope: ${c.bold(scopeLabel(parsed.scope))}`);
73
+ out(` duration: ${c.bold(formatDuration(durationMs))}`);
74
+ out(` hosts: ${c.dim(hosts.hostsPath())}`);
75
+ out(` blocking ${c.bold(String(targets.length))} hostnames`);
76
+ out();
77
+ if (parsed.dryRun) {
78
+ out(c.yellow(' --dry-run: no changes made. Hostnames that would be blocked:'));
79
+ for (const h of targets)
80
+ out(' ' + h);
81
+ out();
82
+ return 0;
83
+ }
84
+ if (!hosts.hasPrivileges()) {
85
+ err(c.red(' ✗ killm needs elevated privileges to edit the hosts file.'));
86
+ err(' ' + privilegeHint());
87
+ return 1;
88
+ }
89
+ if (!parsed.yes) {
90
+ const ok = await confirm(` Block ${scopeLabel(parsed.scope)} for ${formatDuration(durationMs)}? [y/N] `);
91
+ if (!ok) {
92
+ out(c.dim(' cancelled.'));
93
+ return 0;
94
+ }
95
+ }
96
+ const endTime = Date.now() + durationMs;
97
+ const until = new Date(endTime);
98
+ try {
99
+ hosts.applyBlock(targets, { until });
100
+ }
101
+ catch (e) {
102
+ const code = errnoCode(e);
103
+ if (code === 'EACCES' || code === 'EPERM') {
104
+ err(c.red(' ✗ permission denied writing the hosts file.'));
105
+ err(' ' + privilegeHint());
106
+ return 1;
107
+ }
108
+ err(c.red(' ✗ failed to apply block: ' + e.message));
109
+ return 1;
110
+ }
111
+ await hosts.flushDns();
112
+ out(c.green(` ✓ block active until ${until.toLocaleTimeString()}`));
113
+ out();
114
+ let timer = null;
115
+ let ticker = null;
116
+ let restored = false;
117
+ // Restore exactly once, no matter how we leave (timer, Ctrl+C, kill).
118
+ const restore = (reason) => {
119
+ if (restored)
120
+ return;
121
+ restored = true;
122
+ if (timer)
123
+ clearTimeout(timer);
124
+ if (ticker)
125
+ clearInterval(ticker);
126
+ try {
127
+ const removed = hosts.removeBlock();
128
+ void hosts.flushDns(); // fire and forget on the way out
129
+ if (tty) {
130
+ readline.clearLine(process.stdout, 0);
131
+ readline.cursorTo(process.stdout, 0);
132
+ }
133
+ if (removed) {
134
+ out(c.green(` ✓ block lifted (${reason}). Access restored.`));
135
+ }
136
+ else {
137
+ out(c.dim(` block already lifted (${reason}).`));
138
+ }
139
+ }
140
+ catch (e) {
141
+ err(c.red(` ✗ could not restore hosts file automatically: ${e.message}`));
142
+ err(c.red(' Run "sudo npx killm --restore" to clean up.'));
143
+ }
144
+ };
145
+ const render = makeCountdown(endTime);
146
+ render();
147
+ ticker = tty ? setInterval(render, 1000) : null;
148
+ // Last-ditch synchronous cleanup if the event loop is torn down unexpectedly.
149
+ process.once('exit', () => {
150
+ if (!restored) {
151
+ try {
152
+ hosts.removeBlock();
153
+ }
154
+ catch {
155
+ /* ignore */
156
+ }
157
+ }
158
+ });
159
+ await new Promise((resolve) => {
160
+ timer = setTimeout(() => {
161
+ restore('time elapsed');
162
+ resolve();
163
+ }, durationMs);
164
+ const onSignal = (label) => {
165
+ restore(label);
166
+ resolve();
167
+ };
168
+ process.once('SIGINT', () => onSignal('interrupted'));
169
+ process.once('SIGTERM', () => onSignal('terminated'));
170
+ });
171
+ return 0;
172
+ }
173
+ /**
174
+ * Program entry point.
175
+ *
176
+ * @param argv process.argv.slice(2)
177
+ * @returns exit code
178
+ */
179
+ export async function main(argv) {
180
+ const parsed = parseArgs(argv);
181
+ if (parsed.error) {
182
+ err(c.red('error: ' + parsed.error));
183
+ err('');
184
+ err('Run "killm --help" for usage.');
185
+ return 2;
186
+ }
187
+ switch (parsed.command) {
188
+ case 'help':
189
+ out(HELP);
190
+ return 0;
191
+ case 'version':
192
+ out(VERSION);
193
+ return 0;
194
+ case 'status': {
195
+ if (hosts.isBlocked()) {
196
+ out(c.yellow('killm: a block is currently ACTIVE.'));
197
+ out(c.dim('Run "killm --restore" to lift it.'));
198
+ }
199
+ else {
200
+ out(c.green('killm: no block active.'));
201
+ }
202
+ return 0;
203
+ }
204
+ case 'list': {
205
+ const targets = resolveTargets(parsed.scope);
206
+ out(`# ${scopeLabel(parsed.scope)} — ${targets.length} hostnames`);
207
+ for (const h of targets)
208
+ out(h);
209
+ return 0;
210
+ }
211
+ case 'restore': {
212
+ if (!hosts.hasPrivileges()) {
213
+ err(c.red('killm: needs elevated privileges to edit the hosts file.'));
214
+ err(' ' + privilegeHint());
215
+ return 1;
216
+ }
217
+ try {
218
+ const removed = hosts.removeBlock();
219
+ await hosts.flushDns();
220
+ out(removed
221
+ ? c.green('killm: block lifted. Access restored.')
222
+ : c.dim('killm: no block was active.'));
223
+ return 0;
224
+ }
225
+ catch (e) {
226
+ err(c.red('killm: failed to restore hosts file: ' + e.message));
227
+ return 1;
228
+ }
229
+ }
230
+ case 'run':
231
+ return runBlock(parsed);
232
+ }
233
+ }
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Domain target lists, split by the kind of access they represent.
3
+ *
4
+ * The split between `agents` and `web` is the whole point of killm: an agentic
5
+ * coding tool (Claude Code, Cursor, Copilot, Aider, ...) talks to a *different*
6
+ * host than the consumer chat website. Claude Code hits `api.anthropic.com`
7
+ * while the chat UI lives on `claude.ai`; the OpenAI API lives on
8
+ * `api.openai.com` while ChatGPT lives on `chatgpt.com`. Because they are
9
+ * separate hostnames we can cut off one without touching the other.
10
+ *
11
+ * Blocking is done at the hostname level (via the hosts file), so we cannot
12
+ * block by URL path. Where a product only exposes a chat product behind a path
13
+ * we block the closest dedicated hostname instead.
14
+ */
15
+ // API endpoints + dedicated coding-assistant backends. Blocking these stops
16
+ // agentic coding tools and raw API usage WITHOUT touching the chat websites.
17
+ export const AGENTS = [
18
+ // Provider API endpoints (used by Claude Code, Codex, Aider, Continue, etc.)
19
+ 'api.anthropic.com',
20
+ 'api.openai.com',
21
+ 'api.x.ai',
22
+ 'api.mistral.ai',
23
+ 'api.groq.com',
24
+ 'api.deepseek.com',
25
+ 'api.together.xyz',
26
+ 'api.fireworks.ai',
27
+ 'api.cohere.com',
28
+ 'api.perplexity.ai',
29
+ 'generativelanguage.googleapis.com',
30
+ 'api.replicate.com',
31
+ // Cursor
32
+ 'cursor.com',
33
+ 'api2.cursor.sh',
34
+ 'api3.cursor.sh',
35
+ 'api4.cursor.sh',
36
+ 'repo42.cursor.sh',
37
+ // GitHub Copilot
38
+ 'api.githubcopilot.com',
39
+ 'copilot-proxy.githubusercontent.com',
40
+ 'copilot-telemetry.githubusercontent.com',
41
+ 'proxy.individual.githubcopilot.com',
42
+ 'proxy.business.githubcopilot.com',
43
+ 'api.individual.githubcopilot.com',
44
+ 'api.business.githubcopilot.com',
45
+ // Codeium / Windsurf
46
+ 'server.codeium.com',
47
+ 'codeium.com',
48
+ 'windsurf.com',
49
+ // Sourcegraph Cody
50
+ 'cody-gateway.sourcegraph.com',
51
+ 'sourcegraph.com',
52
+ // Tabnine
53
+ 'api.tabnine.com',
54
+ 'tabnine.com',
55
+ // Continue.dev
56
+ 'api.continue.dev',
57
+ 'continue.dev',
58
+ // Supermaven
59
+ 'api.supermaven.com',
60
+ 'supermaven.com',
61
+ ];
62
+ // Consumer chat websites. Blocking these stops the browser apps WITHOUT
63
+ // touching the API endpoints that coding agents rely on.
64
+ export const WEB = [
65
+ // OpenAI ChatGPT
66
+ 'chatgpt.com',
67
+ 'chat.openai.com',
68
+ // Anthropic Claude
69
+ 'claude.ai',
70
+ // Google Gemini
71
+ 'gemini.google.com',
72
+ 'bard.google.com',
73
+ // xAI Grok
74
+ 'grok.com',
75
+ // Microsoft Copilot (consumer)
76
+ 'copilot.microsoft.com',
77
+ // Mistral Le Chat
78
+ 'chat.mistral.ai',
79
+ // DeepSeek
80
+ 'chat.deepseek.com',
81
+ // Perplexity
82
+ 'perplexity.ai',
83
+ 'www.perplexity.ai',
84
+ // Meta AI
85
+ 'meta.ai',
86
+ 'www.meta.ai',
87
+ // Poe
88
+ 'poe.com',
89
+ // Character.AI
90
+ 'character.ai',
91
+ // You.com
92
+ 'you.com',
93
+ // Phind
94
+ 'phind.com',
95
+ // Hugging Face Chat
96
+ 'huggingface.co',
97
+ ];
98
+ /**
99
+ * Resolve the set of hostnames to block from the parsed category flags.
100
+ * Returns a de-duplicated, sorted array.
101
+ */
102
+ export function resolveTargets(flags) {
103
+ const set = new Set();
104
+ if (flags.all || flags.agents) {
105
+ for (const h of AGENTS)
106
+ set.add(h);
107
+ }
108
+ if (flags.all || flags.web) {
109
+ for (const h of WEB)
110
+ set.add(h);
111
+ }
112
+ return Array.from(set).sort();
113
+ }
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "killm",
3
+ "version": "1.0.0",
4
+ "description": "Temporarily block your machine from reaching LLM services to curb AI dependency. npx killm for 1h --agents",
5
+ "bin": {
6
+ "killm": "dist/src/bin.js"
7
+ },
8
+ "type": "module",
9
+ "files": [
10
+ "dist/src/",
11
+ "README.md",
12
+ "LICENSE"
13
+ ],
14
+ "scripts": {
15
+ "build": "tsc",
16
+ "typecheck": "tsc --noEmit",
17
+ "pretest": "npm run build",
18
+ "test": "node --test dist/test/duration.test.js dist/test/targets.test.js dist/test/cli.test.js dist/test/hosts.test.js dist/test/e2e.test.js",
19
+ "lint": "eslint .",
20
+ "lint:fix": "eslint . --fix",
21
+ "format": "prettier --write .",
22
+ "format:check": "prettier --check .",
23
+ "prepublishOnly": "npm run lint && npm run format:check && npm test"
24
+ },
25
+ "keywords": [
26
+ "llm",
27
+ "ai",
28
+ "focus",
29
+ "block",
30
+ "hosts",
31
+ "productivity",
32
+ "digital-wellbeing",
33
+ "claude",
34
+ "chatgpt",
35
+ "copilot",
36
+ "cursor"
37
+ ],
38
+ "author": "Aidan Neel",
39
+ "license": "MIT",
40
+ "engines": {
41
+ "node": ">=18.17"
42
+ },
43
+ "repository": {
44
+ "type": "git",
45
+ "url": "git+https://github.com/aidan-neel/killm.git"
46
+ },
47
+ "bugs": {
48
+ "url": "https://github.com/aidan-neel/killm/issues"
49
+ },
50
+ "homepage": "https://github.com/aidan-neel/killm#readme",
51
+ "os": [
52
+ "darwin",
53
+ "linux",
54
+ "win32"
55
+ ],
56
+ "devDependencies": {
57
+ "@eslint/js": "^10.0.1",
58
+ "@types/node": "^25.9.2",
59
+ "eslint": "^10.4.1",
60
+ "prettier": "^3.8.4",
61
+ "typescript": "^6.0.3",
62
+ "typescript-eslint": "^8.61.0"
63
+ }
64
+ }