pi-proxy-guard 0.6.0 → 0.6.1

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
@@ -4,7 +4,7 @@ Auto-recover [pi](https://github.com/earendil-works/pi) sessions when the proxy
4
4
 
5
5
  When all provider retries fail (`Retry failed after N attempts` / `Stream ended without finish_reason`), pi pauses the session. This extension:
6
6
 
7
- 1. **Checks connectivity** (`curl https://www.google.com/generate_204`, inheriting the same `https_proxy` env pi uses).
7
+ 1. **Checks connectivity**, two tiers: the watchdog uses a cheap ping (`generate_204` through the same `https_proxy` env pi uses); at settle/recheck time it does a **deep probe** — API host reachability (`ctx.model.baseUrl`, any HTTP response counts) **plus a sustained-transfer check** (`STREAM_MIN_BYTES` from `STREAM_URL`) — the signal a 0-byte ping can't see: whether the chain can actually hold a streaming connection. 'Down' requires `PROBE_ATTEMPTS` consecutive failures.
8
8
  2. **Auto-continues** the run if the proxy is fine (transient stream cut).
9
9
  3. **Repairs the proxy** if it's down — runs a macOS Shortcuts shortcut, falls back to `shadowrocket://` URL schemes — then continues once connectivity is verified.
10
10
  4. **Watchdog**: if pi is still paused, it keeps watching; once the proxy is verified healthy again it sends `continue` itself. It also repairs the proxy while pi is mid-retry, so many incidents never reach the pause at all.
@@ -38,18 +38,27 @@ pi install ~/Developer/pi-proxy-guard
38
38
 
39
39
  Restart pi. `/proxyguard check|restart|status` verifies it's live; logs go to `~/.pi/agent/proxy-guard.log`.
40
40
 
41
- ## Setup: the Shortcuts shortcut
41
+ ## Setup: repairing Shadowrocket on macOS
42
42
 
43
- Default name: **Reconnect Shadowrocket** (set `PI_PROXY_GUARD_SHORTCUT` to rename). Build it in macOS Shortcuts — the user already has `Disconnect VPN` / `Connect VPN` shortcuts whose actions can be reused:
43
+ Zero-setup (verified on the Apple-Silicon iOS-app runtime): the extension runs
44
44
 
45
- 1. `Disconnect VPN` (or Shadowrocket "Stop" action)
46
- 2. Wait 2s
47
- 3. `Connect VPN` (or Shadowrocket "Start" action)
48
- 4. Wait 5s
45
+ ```sh
46
+ open -g "shadowrocket://disconnect?autoclose=true" # tunnel drops
47
+ sleep 3
48
+ open -g "shadowrocket://connect?autoclose=true" # fresh tunnel
49
+ ```
50
+
51
+ `-g` keeps Shadowrocket in the background; `autoclose=true` lets it quit itself. If you have **always-on** enabled, the tunnel may auto-reconnect before the explicit connect — either way you end up on a fresh tunnel, which is the point (the dead stream is already dead; repair makes the *next* request land on a rebuilt chain).
52
+
53
+ ### Shortcut path (optional)
54
+
55
+ `PI_PROXY_GUARD_SHORTCUT` (default name **Reconnect Shadowrocket**) runs `shortcuts run <name>` first, scheme fallback after. **Verified caveat**: on macOS, `shortcuts run` does *not* fire iOS-app SiriKit intents — a shortcut wrapping Shadowrocket's `StopVPNIntent`/`StartVPNIntent` is a silent no-op via CLI (it only fires from the Shortcuts.app GUI). So on macOS the shortcut path only helps if it wraps shell-able actions (e.g. `Run Shell Script` driving another client). Set `PI_PROXY_GUARD_SHORTCUT=""` to skip it entirely.
56
+
57
+ Node switching is intentionally left to the proxy client (chain-proxy friendly). To make the *repair* more effective, you can chain actions into the URL scheme too — e.g. `shadowrocket://select?s=<node>` before `connect` selects a different entry node.
49
58
 
50
- Or, zero-setup: it falls back to `open shadowrocket://stop` → `shadowrocket://start` automatically (works when Shadowrocket registers its URL scheme — on macOS this needs the Apple Silicon iOS app).
59
+ ### Verification note
51
60
 
52
- Node switching is intentionally left to the proxy client (chain-proxy friendly). To make the *repair* more effective, build the node switch into the shortcut itself — e.g. select a different entry node inside Shadowrocket before reconnecting.
61
+ `scutil --nc show/list` reports the NE manager's *intent* state and lags reality by several seconds — during a real disconnect it still says `Connected`. The extension therefore verifies repair with real HTTP probes through the proxy, never scutil.
53
62
 
54
63
  ## Tuning pi's own retries
55
64
 
@@ -64,9 +73,15 @@ Node switching is intentionally left to the proxy client (chain-proxy friendly).
64
73
  | Var | Default | Meaning |
65
74
  |---|---|---|
66
75
  | `PI_PROXY_GUARD` | `1` | `0` disables |
67
- | `PI_PROXY_GUARD_URL` | `https://www.google.com/generate_204` | health-check target; for tighter signal use your provider's API URL |
76
+ | `PI_PROXY_GUARD_URL` | `https://www.google.com/generate_204` | cheap ping (watchdog ticks) |
77
+ | `PI_PROXY_GUARD_API_URL` | `ctx.model.baseUrl` | deep-probe target; defaults to the active model's API host — any HTTP response counts as reachable |
78
+ | `PI_PROXY_GUARD_STREAM_URL` | `https://speed.cloudflare.com/__down?bytes=65536` | sustained-transfer probe used in deep checks |
79
+ | `PI_PROXY_GUARD_STREAM_MIN_BYTES` | `60000` | min bytes the transfer must deliver; `0` disables it |
80
+ | `PI_PROXY_GUARD_VPN_SERVICE` | _unset_ | `scutil --nc` service name — tunnel-down short-circuits straight to repair |
68
81
  | `PI_PROXY_GUARD_PROXY` | _(env)_ | explicit `-x` proxy for the check |
69
- | `PI_PROXY_GUARD_TIMEOUT_MS` | `10000` | check timeout |
82
+ | `PI_PROXY_GUARD_TIMEOUT_MS` | `20000` | per-attempt check timeout |
83
+ | `PI_PROXY_GUARD_PROBE_ATTEMPTS` | `2` | consecutive failures required to declare "down" — tolerates latency spikes without mis-firing a repair |
84
+ | `PI_PROXY_GUARD_PROBE_GAP_MS` | `1500` | gap between probe attempts |
70
85
  | `PI_PROXY_GUARD_SHORTCUT` | `Reconnect Shadowrocket` | `""` disables |
71
86
  | `PI_PROXY_GUARD_SCHEME` | `1` | `0` disables `shadowrocket://` fallback |
72
87
  | `PI_PROXY_GUARD_ESCALATE_AFTER` | `2` | consecutive errors → repair despite OK ping |
@@ -32,13 +32,32 @@
32
32
  * Env knobs (defaults):
33
33
  * PI_PROXY_GUARD=0 disable
34
34
  * PI_PROXY_GUARD_URL https://www.google.com/generate_204
35
+ * (cheap ping used by the watchdog)
36
+ * PI_PROXY_GUARD_API_URL (unset = use ctx.model.baseUrl)
37
+ * PI_PROXY_GUARD_STREAM_URL https://speed.cloudflare.com/__down?bytes=65536
38
+ * PI_PROXY_GUARD_STREAM_MIN_BYTES 60000 (0 disables transfer probe)
39
+ * PI_PROXY_GUARD_VPN_SERVICE "" (scutil --nc fast-path name)
35
40
  * PI_PROXY_GUARD_PROXY (unset = inherit env proxy vars)
36
- * PI_PROXY_GUARD_TIMEOUT_MS 10000
41
+ * PI_PROXY_GUARD_TIMEOUT_MS 20000
42
+ * PI_PROXY_GUARD_PROBE_ATTEMPTS 2 (down requires N consecutive fails)
43
+ * PI_PROXY_GUARD_PROBE_GAP_MS 1500 (gap between probe attempts)
37
44
  * PI_PROXY_GUARD_SHORTCUT "Reconnect Shadowrocket" ("" disables)
38
45
  * PI_PROXY_GUARD_SHORTCUT_TIMEOUT_MS 120000
46
+ * NOTE: `shortcuts run` on macOS does NOT
47
+ * fire iOS-app SiriKit intents (verified):
48
+ * a shortcut wrapping Shadowrocket's
49
+ * StopVPNIntent is a no-op via CLI, works
50
+ * only from Shortcuts.app GUI. Only useful
51
+ * for shortcuts with shell-able actions.
39
52
  * PI_PROXY_GUARD_SCHEME "1" (shadowrocket:// fallback)
40
- * PI_PROXY_GUARD_SCHEME_STOP shadowrocket://stop
41
- * PI_PROXY_GUARD_SCHEME_START shadowrocket://start
53
+ * PI_PROXY_GUARD_SCHEME_STOP shadowrocket://disconnect?autoclose=true
54
+ * PI_PROXY_GUARD_SCHEME_START shadowrocket://connect?autoclose=true
55
+ * Verified working on macOS (iOS-app
56
+ * runtime): `open -g` delivers the URL
57
+ * without foregrounding; tunnel drops and
58
+ * comes back. If Shadowrocket "always-on"
59
+ * is enabled it may auto-reconnect before
60
+ * our explicit connect — harmless either way.
42
61
  * PI_PROXY_GUARD_ESCALATE_AFTER 2 (consecutive errors w/ OK check)
43
62
  * PI_PROXY_GUARD_PAUSED_REPAIRS 3 (active repair cap while paused)
44
63
  * PI_PROXY_GUARD_REPAIR_COOLDOWN_MS 90000
@@ -68,13 +87,21 @@ import { setTimeout as sleep } from "node:timers/promises";
68
87
 
69
88
  const DISABLED = process.env.PI_PROXY_GUARD === "0";
70
89
  const CHECK_URL = process.env.PI_PROXY_GUARD_URL ?? "https://www.google.com/generate_204";
90
+ const API_URL_OVERRIDE = process.env.PI_PROXY_GUARD_API_URL;
91
+ const STREAM_URL = process.env.PI_PROXY_GUARD_STREAM_URL ?? "https://speed.cloudflare.com/__down?bytes=65536";
92
+ const STREAM_MIN_BYTES = Number(process.env.PI_PROXY_GUARD_STREAM_MIN_BYTES ?? 60_000);
93
+ const VPN_SERVICE = process.env.PI_PROXY_GUARD_VPN_SERVICE ?? "";
71
94
  const CHECK_PROXY = process.env.PI_PROXY_GUARD_PROXY;
72
- const CHECK_TIMEOUT_MS = Number(process.env.PI_PROXY_GUARD_TIMEOUT_MS ?? 10_000);
95
+ const CHECK_TIMEOUT_MS = Number(process.env.PI_PROXY_GUARD_TIMEOUT_MS ?? 20_000);
96
+ const PROBE_ATTEMPTS = Number(process.env.PI_PROXY_GUARD_PROBE_ATTEMPTS ?? 2);
97
+ const PROBE_GAP_MS = Number(process.env.PI_PROXY_GUARD_PROBE_GAP_MS ?? 1_500);
73
98
  const SHORTCUT = process.env.PI_PROXY_GUARD_SHORTCUT ?? "Reconnect Shadowrocket";
74
99
  const SHORTCUT_TIMEOUT_MS = Number(process.env.PI_PROXY_GUARD_SHORTCUT_TIMEOUT_MS ?? 120_000);
75
100
  const USE_SCHEME = process.env.PI_PROXY_GUARD_SCHEME !== "0";
76
- const SCHEME_STOP = process.env.PI_PROXY_GUARD_SCHEME_STOP ?? "shadowrocket://stop";
77
- const SCHEME_START = process.env.PI_PROXY_GUARD_SCHEME_START ?? "shadowrocket://start";
101
+ // Verified against Shadowrocket's docs: connect/disconnect (not start/stop).
102
+ // autoclose=true lets the app quit itself after handling the action.
103
+ const SCHEME_STOP = process.env.PI_PROXY_GUARD_SCHEME_STOP ?? "shadowrocket://disconnect?autoclose=true";
104
+ const SCHEME_START = process.env.PI_PROXY_GUARD_SCHEME_START ?? "shadowrocket://connect?autoclose=true";
78
105
  const ESCALATE_AFTER = Number(process.env.PI_PROXY_GUARD_ESCALATE_AFTER ?? 2);
79
106
  const PAUSED_REPAIR_MAX = Number(process.env.PI_PROXY_GUARD_PAUSED_REPAIRS ?? 3);
80
107
  const REPAIR_COOLDOWN_MS = Number(process.env.PI_PROXY_GUARD_REPAIR_COOLDOWN_MS ?? 90_000);
@@ -128,27 +155,68 @@ interface CheckResult {
128
155
  error?: string;
129
156
  }
130
157
 
131
- function checkProxy(): Promise<CheckResult> {
158
+ function execCmd(cmd: string, args: string[], timeoutMs: number): Promise<{ ok: boolean; output?: string; error?: string }> {
159
+ return new Promise((resolve) => {
160
+ execFile(cmd, args, { timeout: timeoutMs }, (err, stdout, stderr) => {
161
+ resolve(err
162
+ ? { ok: false, output: stdout, error: ((stderr || "") + err.message).split("\n")[0].trim() }
163
+ : { ok: true, output: stdout });
164
+ });
165
+ });
166
+ }
167
+
168
+ /** Reachability probe: any HTTP response (even 404/401/502) means the
169
+ * transport path to the host is alive — that's what we measure, not
170
+ * whether the endpoint "succeeds". */
171
+ function httpProbe(url: string): Promise<CheckResult> {
132
172
  return new Promise((resolve) => {
133
173
  const args = ["-sS", "-o", "/dev/null", "-w", "%{http_code}", "--max-time", String(Math.ceil(CHECK_TIMEOUT_MS / 1000))];
134
174
  if (CHECK_PROXY) args.push("-x", CHECK_PROXY);
135
- args.push(CHECK_URL);
175
+ args.push(url);
136
176
  execFile("curl", args, { timeout: CHECK_TIMEOUT_MS + 2_000 }, (err, stdout) => {
137
177
  const status = Number.parseInt((stdout ?? "").trim(), 10);
138
- if (!err && status >= 200 && status < 400) resolve({ ok: true, status });
178
+ if (!err && status > 0) resolve({ ok: true, status });
139
179
  else resolve({ ok: false, status: Number.isFinite(status) ? status : undefined, error: (err?.message ?? "").split("\n")[0] });
140
180
  });
141
181
  });
142
182
  }
143
183
 
144
- function execCmd(cmd: string, args: string[], timeoutMs: number): Promise<{ ok: boolean; error?: string }> {
184
+ /** Sustained-transfer probe: downloads STREAM_URL and requires the body to
185
+ * actually arrive (>= STREAM_MIN_BYTES). A half-dead chain that passes tiny
186
+ * pings but cuts long-lived streams gets caught here. */
187
+ function streamProbe(url: string): Promise<CheckResult> {
145
188
  return new Promise((resolve) => {
146
- execFile(cmd, args, { timeout: timeoutMs }, (err, _stdout, stderr) => {
147
- resolve(err ? { ok: false, error: ((stderr || "") + err.message).split("\n")[0].trim() } : { ok: true });
189
+ const args = ["-sS", "-o", "/dev/null", "-w", "%{http_code} %{size_download}", "--max-time", String(Math.ceil(CHECK_TIMEOUT_MS / 1000))];
190
+ if (CHECK_PROXY) args.push("-x", CHECK_PROXY);
191
+ args.push(url);
192
+ execFile("curl", args, { timeout: CHECK_TIMEOUT_MS + 2_000 }, (err, stdout) => {
193
+ const [statusRaw, bytesRaw] = (stdout ?? "").trim().split(/\s+/);
194
+ const status = Number.parseInt(statusRaw, 10);
195
+ const bytes = Number.parseInt(bytesRaw, 10);
196
+ if (!err && status > 0 && bytes >= STREAM_MIN_BYTES) resolve({ ok: true, status });
197
+ else
198
+ resolve({
199
+ ok: false,
200
+ status: Number.isFinite(status) ? status : undefined,
201
+ error: err ? err.message.split("\n")[0] : `stream cut: got ${bytes}B < ${STREAM_MIN_BYTES}B`,
202
+ });
148
203
  });
149
204
  });
150
205
  }
151
206
 
207
+ /** Optional fast path: the VPN tunnel state itself, zero network traffic. */
208
+ async function tunnelUp(): Promise<boolean | undefined> {
209
+ if (!VPN_SERVICE) return undefined;
210
+ const r = await execCmd("scutil", ["--nc", "show", VPN_SERVICE], 5_000);
211
+ if (!r.ok || r.output === undefined) return undefined;
212
+ return /\bConnected\b/.test(r.output) ? true : /\bDisconnected\b/.test(r.output) ? false : undefined;
213
+ }
214
+
215
+ /** Back-compat shim: reachability via the configured CHECK_URL. */
216
+ function checkProxy(): Promise<CheckResult> {
217
+ return httpProbe(CHECK_URL);
218
+ }
219
+
152
220
  export default function (pi: ExtensionAPI) {
153
221
  if (DISABLED) return;
154
222
 
@@ -237,14 +305,39 @@ export default function (pi: ExtensionAPI) {
237
305
  }
238
306
  }
239
307
 
240
- /** checkProxy + episode bookkeeping: an OK probe resets the repair counter. */
241
- async function probe(): Promise<CheckResult> {
242
- const check = await checkProxy();
243
- if (check.ok) {
244
- failedRepairs = 0;
245
- gaveUpNotified = false;
308
+ /** checkProxy + episode bookkeeping: an OK probe resets the repair counter.
309
+ * A single failure never means "down" on a wobbly chain proxy — we
310
+ * require PROBE_ATTEMPTS consecutive failures before declaring dead.
311
+ * This prevents latency spikes from triggering needless VPN bounces
312
+ * (which would kill the very streams we're trying to protect).
313
+ *
314
+ * deep=true (settle/recheck): probes the actual API host (ctx.model.baseUrl
315
+ * or PI_PROXY_GUARD_API_URL) and adds a sustained-transfer check — the
316
+ * "can it hold an SSE stream" signal a 0-byte ping can't see.
317
+ * deep=false (watchdog): cheap generate_204 ping only. */
318
+ async function probe(ctx?: ExtensionContext, deep = false): Promise<CheckResult> {
319
+ const vpn = await tunnelUp();
320
+ if (vpn === false) return { ok: false, error: `VPN tunnel "${VPN_SERVICE}" disconnected` };
321
+
322
+ const target = (deep ? (API_URL_OVERRIDE ?? ctx?.model?.baseUrl) : undefined) ?? CHECK_URL;
323
+ let last: CheckResult = { ok: false };
324
+ for (let attempt = 1; attempt <= PROBE_ATTEMPTS; attempt++) {
325
+ last = await httpProbe(target);
326
+ if (last.ok && deep && STREAM_MIN_BYTES > 0) {
327
+ const stream = await streamProbe(STREAM_URL);
328
+ if (!stream.ok) last = stream;
329
+ }
330
+ if (last.ok) {
331
+ failedRepairs = 0;
332
+ gaveUpNotified = false;
333
+ return last;
334
+ }
335
+ if (attempt < PROBE_ATTEMPTS) {
336
+ log(`probe attempt ${attempt}/${PROBE_ATTEMPTS} failed (${last.error ?? `HTTP ${last.status}`}); retrying`);
337
+ await sleep(PROBE_GAP_MS);
338
+ }
246
339
  }
247
- return check;
340
+ return last;
248
341
  }
249
342
 
250
343
  /** True while we may still launch an active repair this down-episode. */
@@ -262,7 +355,7 @@ export default function (pi: ExtensionAPI) {
262
355
  try {
263
356
  const recheck = async (): Promise<boolean> => {
264
357
  await sleep(RECHECK_DELAY_MS);
265
- const check = await checkProxy();
358
+ const check = await probe(ctx, true); // deep: verify real capacity before unpausing
266
359
  if (!check.ok) log(`still down: ${check.error ?? `HTTP ${check.status}`}`);
267
360
  return check.ok;
268
361
  };
@@ -279,13 +372,14 @@ export default function (pi: ExtensionAPI) {
279
372
  const viaScheme = async (): Promise<boolean> => {
280
373
  if (!USE_SCHEME) return false;
281
374
  notify(ctx, "Bouncing Shadowrocket via URL scheme…", "warning");
282
- const stop = await execCmd("open", [SCHEME_STOP], 15_000);
375
+ // `open -g`: background — do NOT activate/foreground Shadowrocket.
376
+ const stop = await execCmd("open", ["-g", SCHEME_STOP], 15_000);
283
377
  if (!stop.ok) {
284
378
  log(`scheme stop failed: ${stop.error}`);
285
379
  return false;
286
380
  }
287
381
  await sleep(3_000);
288
- const start = await execCmd("open", [SCHEME_START], 15_000);
382
+ const start = await execCmd("open", ["-g", SCHEME_START], 15_000);
289
383
  if (!start.ok) {
290
384
  log(`scheme start failed: ${start.error}`);
291
385
  return false;
@@ -315,10 +409,9 @@ export default function (pi: ExtensionAPI) {
315
409
  // Only spend effort when pi actually needs the connection:
316
410
  // a live run (internal retries benefit) or a paused-by-us session.
317
411
  if (!runActive && !pausedByUs) return;
318
- if (budgetLeft() <= 0) return;
319
412
  ticking = true;
320
413
  try {
321
- const check = await probe();
414
+ const check = await probe(sessionCtx, false); // cheap ping for the periodic watch
322
415
  if (check.ok) {
323
416
  // Recovered while paused: resume.
324
417
  if (pausedByUs && !runActive) {
@@ -341,6 +434,8 @@ export default function (pi: ExtensionAPI) {
341
434
  }
342
435
  return;
343
436
  }
437
+ // Repairing the network is free — only resumes/continues spend
438
+ // provider requests, so the budget gates those, not the repair.
344
439
  if (await repairProxy(sessionCtx)) {
345
440
  if (pausedByUs && !runActive && budgetLeft() > 0) {
346
441
  chargeBudget();
@@ -410,13 +505,18 @@ export default function (pi: ExtensionAPI) {
410
505
  `settle error #${consecutiveErrors}; canContinue=${event.context.canContinue}; ` +
411
506
  `budget=${budgetLeft()}/${MAX_REPAIRS}; pausedByUs=${pausedByUs}`,
412
507
  );
413
- if (budgetLeft() <= 0) {
508
+ const check = await probe(ctx, true); // deep: API host + sustained transfer
509
+ const canContinue = budgetLeft() > 0;
510
+ if (!canContinue) {
511
+ // Budget is about *provider requests*, not connectivity: still
512
+ // repair a dead proxy so the user's manual take-over lands on a
513
+ // working network — then stay paused without continuing.
514
+ if (!check.ok) await repairProxy(ctx);
414
515
  notify(ctx, `Auto-continue budget exhausted (${MAX_REPAIRS}/${Math.round(WINDOW_MS / 60_000)}min). Staying paused — take over manually.`, "warning");
415
516
  systemNotify("auto-continue budget exhausted — session paused; check proxy or take over");
517
+ pausedByUs = true;
416
518
  return;
417
519
  }
418
-
419
- const check = await probe();
420
520
  const halfDead = check.ok && consecutiveErrors >= ESCALATE_AFTER;
421
521
 
422
522
  if (!check.ok && !mayRepair()) {
@@ -431,7 +531,7 @@ export default function (pi: ExtensionAPI) {
431
531
  if (!(await repairProxy(ctx))) {
432
532
  // On the half-dead path the pre-repair check was OK: don't trust the
433
533
  // failed repair to mean we're offline — verify once more.
434
- const post = await probe();
534
+ const post = await probe(ctx, true);
435
535
  if (!post.ok) {
436
536
  pausedByUs = true;
437
537
  notify(ctx, "Proxy still down after repair attempts. Staying paused; watchdog will auto-resume once it's back.", "error");
@@ -463,10 +563,10 @@ export default function (pi: ExtensionAPI) {
463
563
  handler: async (args, ctx) => {
464
564
  const sub = (args ?? "").trim().split(/\s+/)[0] || "check";
465
565
  if (sub === "check") {
466
- const check = await checkProxy();
566
+ const check = await probe(ctx, true);
467
567
  notify(
468
568
  ctx,
469
- check.ok ? `Proxy OK (HTTP ${check.status}, ${CHECK_URL})` : `Proxy DOWN (${check.error ?? `HTTP ${check.status}`}, ${CHECK_URL})`,
569
+ check.ok ? `Proxy OK (HTTP ${check.status})` : `Proxy DOWN (${check.error ?? `HTTP ${check.status}`})`,
470
570
  check.ok ? "info" : "error",
471
571
  );
472
572
  } else if (sub === "restart") {
@@ -475,7 +575,8 @@ export default function (pi: ExtensionAPI) {
475
575
  } else if (sub === "status") {
476
576
  notify(
477
577
  ctx,
478
- `url=${CHECK_URL} proxy=${CHECK_PROXY ?? "(env)"} shortcut="${SHORTCUT || "off"}" scheme=${USE_SCHEME ? "on" : "off"} ` +
578
+ `ping=${CHECK_URL} api=${API_URL_OVERRIDE ?? ctx.model?.baseUrl ?? "(none)"} stream=${STREAM_MIN_BYTES > 0 ? STREAM_URL : "off"} vpn=${VPN_SERVICE || "off"} ` +
579
+ `proxy=${CHECK_PROXY ?? "(env)"} shortcut="${SHORTCUT || "off"}" scheme=${USE_SCHEME ? "on" : "off"} ` +
479
580
  `escalateAfter=${ESCALATE_AFTER} budget=${budgetLeft()}/${MAX_REPAIRS}/${Math.round(WINDOW_MS / 60_000)}min ` +
480
581
  `watchdog=${WATCHDOG_MS}ms pausedByUs=${pausedByUs} consecErrs=${consecutiveErrors} failedRepairs=${failedRepairs}/${PAUSED_REPAIR_MAX} log=${LOG_FILE || "off"}`,
481
582
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-proxy-guard",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "Auto-recover pi sessions when the proxy drops: connectivity check, Shadowrocket restart/node-switch via Shortcuts or URL scheme, watchdog auto-resume.",
5
5
  "license": "MIT",
6
6
  "repository": {