killm 1.0.2 → 1.0.3

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/README.md CHANGED
@@ -163,12 +163,26 @@ target hostname at `0.0.0.0` (and `::1` for IPv6):
163
163
  ```
164
164
 
165
165
  It then flushes the OS DNS cache so the change takes effect immediately. When
166
- the timer expires, or on `Ctrl+C`, `SIGTERM`, or process exit, it strips that
167
- block back out and flushes again. The markers mean it only ever touches its own
168
- lines — your existing hosts entries are left alone.
166
+ the timer expires or on `Ctrl+C`, `SIGTERM`, or closing the terminal
167
+ (`SIGHUP`) — it strips that block back out and flushes again. The markers mean
168
+ it only ever touches its own lines — your existing hosts entries are left
169
+ alone.
169
170
 
170
- If something goes wrong and a block is left behind, `sudo npx killm --restore`
171
- (or the Administrator equivalent) cleans it up.
171
+ ### If the killm process dies, the block stays until killm heals it
172
+
173
+ The timer lives in the killm process, so **keep it running** for the duration.
174
+ Ctrl+C and closing the terminal both lift the block cleanly, but a `kill -9`,
175
+ a crash, or a machine shutdown can strand the block in your hosts file.
176
+
177
+ killm writes the expiry time into the block itself, so it self-heals: **any
178
+ later killm command** (`--status`, `--restore`, or starting a new block)
179
+ notices an expired stranded block and removes it. To lift one immediately:
180
+
181
+ ```bash
182
+ sudo npx killm --restore
183
+ ```
184
+
185
+ `killm --status` also tells you when an active block is scheduled to lift.
172
186
 
173
187
  ## Firewall mode (`--firewall`)
174
188
 
package/dist/src/hosts.js CHANGED
@@ -4,6 +4,8 @@ import path from 'node:path';
4
4
  import { execFile } from 'node:child_process';
5
5
  export const BEGIN = '# >>> killm block (do not edit between markers) >>>';
6
6
  export const END = '# <<< killm block <<<';
7
+ /** Machine-parseable expiry marker inside the block. */
8
+ export const EXPIRES_PREFIX = '# killm-expires: ';
7
9
  const SINK4 = '0.0.0.0';
8
10
  const SINK6 = '::1';
9
11
  function errnoCode(err) {
@@ -59,6 +61,7 @@ export function buildBlock(hosts, opts = {}) {
59
61
  lines.push(`# added by killm at ${new Date().toISOString()}`);
60
62
  if (opts.until) {
61
63
  lines.push(`# auto-removed at ${opts.until.toISOString()} (or when killm exits)`);
64
+ lines.push(`${EXPIRES_PREFIX}${opts.until.toISOString()}`);
62
65
  }
63
66
  for (const h of hosts) {
64
67
  lines.push(`${SINK4}\t${h}`);
@@ -125,6 +128,36 @@ export function removeBlock() {
125
128
  export function isBlocked() {
126
129
  return readHosts().includes(BEGIN);
127
130
  }
131
+ /**
132
+ * When the current block is scheduled to expire, or null when no block is
133
+ * present or it carries no (valid) expiry marker — e.g. blocks written by
134
+ * killm <= 1.0.2.
135
+ */
136
+ export function blockExpiry(text = readHosts()) {
137
+ const begin = text.indexOf(BEGIN);
138
+ if (begin === -1)
139
+ return null;
140
+ const idx = text.indexOf(EXPIRES_PREFIX, begin);
141
+ if (idx === -1)
142
+ return null;
143
+ const start = idx + EXPIRES_PREFIX.length;
144
+ const lineEnd = text.indexOf('\n', start);
145
+ const stamp = text.slice(start, lineEnd === -1 ? undefined : lineEnd).trim();
146
+ const date = new Date(stamp);
147
+ return Number.isNaN(date.getTime()) ? null : date;
148
+ }
149
+ /**
150
+ * Remove the block if its expiry has passed — heals hosts files stranded by
151
+ * a killed process (terminal closed, kill -9, crash, machine shutdown).
152
+ *
153
+ * @returns true when an expired block was found and removed
154
+ */
155
+ export function clearExpiredBlock(now = new Date()) {
156
+ const expiry = blockExpiry();
157
+ if (expiry === null || expiry.getTime() > now.getTime())
158
+ return false;
159
+ return removeBlock();
160
+ }
128
161
  /**
129
162
  * Best-effort DNS cache flush so the new hosts entries take effect immediately.
130
163
  * Silently ignores failures — the block still works, it may just take a moment.
package/dist/src/index.js CHANGED
@@ -94,6 +94,15 @@ async function runBlock(parsed) {
94
94
  return 0;
95
95
  }
96
96
  }
97
+ // Heal any expired block stranded by a killed process before applying ours.
98
+ try {
99
+ if (hosts.clearExpiredBlock()) {
100
+ out(c.dim(' (cleaned up an expired block left behind by a previous run)'));
101
+ }
102
+ }
103
+ catch {
104
+ /* no privileges; the apply below will surface the real error */
105
+ }
97
106
  const endTime = Date.now() + durationMs;
98
107
  const until = new Date(endTime);
99
108
  try {
@@ -138,6 +147,11 @@ async function runBlock(parsed) {
138
147
  '\n' +
139
148
  c.dim(' file entirely — restart the browser / disable Secure DNS if needed.'));
140
149
  out();
150
+ out(c.yellow(' ⚠ keep this process running — killm lifts the block when the timer'));
151
+ out(c.yellow(' ends or you press Ctrl+C / close the terminal. If it dies harder'));
152
+ out(c.yellow(' than that (kill -9, crash, shutdown), the block stays in place'));
153
+ out(c.yellow(' until you run any killm command again, e.g. killm --restore'));
154
+ out();
141
155
  let timer = null;
142
156
  let ticker = null;
143
157
  let restored = false;
@@ -199,6 +213,9 @@ async function runBlock(parsed) {
199
213
  };
200
214
  process.once('SIGINT', () => onSignal('interrupted'));
201
215
  process.once('SIGTERM', () => onSignal('terminated'));
216
+ // Closing the terminal window sends SIGHUP; without this handler Node
217
+ // dies without cleanup and the block is stranded in the hosts file.
218
+ process.once('SIGHUP', () => onSignal('terminal closed'));
202
219
  });
203
220
  return 0;
204
221
  }
@@ -224,9 +241,31 @@ export async function main(argv) {
224
241
  out(VERSION);
225
242
  return 0;
226
243
  case 'status': {
244
+ const expiry = hosts.blockExpiry();
245
+ const expired = expiry !== null && expiry.getTime() <= Date.now();
246
+ if (expired) {
247
+ // A previous run was killed before it could clean up. Heal it now.
248
+ try {
249
+ hosts.clearExpiredBlock();
250
+ await hosts.flushDns();
251
+ out(c.green('killm: no block active.'));
252
+ out(c.dim('(cleaned up an expired block left behind by a previous run)'));
253
+ return 0;
254
+ }
255
+ catch {
256
+ err(c.yellow('killm: an EXPIRED block is still stuck in the hosts file.'));
257
+ err(' Clean it up with elevated privileges: sudo npx killm --restore');
258
+ return 1;
259
+ }
260
+ }
227
261
  if (hosts.isBlocked()) {
228
262
  out(c.yellow('killm: a block is currently ACTIVE.'));
229
- out(c.dim('Run "killm --restore" to lift it.'));
263
+ if (expiry) {
264
+ out(c.dim(`Scheduled to lift at ${expiry.toLocaleString()}.`));
265
+ out(c.dim('If the killm process that created it is no longer running, it will not\n' +
266
+ 'lift itself on time — but any later killm command cleans it up once expired.'));
267
+ }
268
+ out(c.dim('Run "killm --restore" to lift it now.'));
230
269
  }
231
270
  else {
232
271
  out(c.green('killm: no block active.'));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "killm",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "description": "Temporarily block your machine from reaching LLM services to curb AI dependency. npx killm for 1h --agents",
5
5
  "bin": {
6
6
  "killm": "dist/src/bin.js"