ofw-mcp 2.2.0 → 2.3.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.
@@ -6,7 +6,7 @@
6
6
  },
7
7
  "metadata": {
8
8
  "description": "OurFamilyWizard tools for Claude Code",
9
- "version": "2.2.0"
9
+ "version": "2.3.0"
10
10
  },
11
11
  "plugins": [
12
12
  {
@@ -14,7 +14,7 @@
14
14
  "displayName": "OurFamilyWizard",
15
15
  "source": "./",
16
16
  "description": "OurFamilyWizard co-parenting tools for Claude — messages, calendar, expenses, and journal via MCP",
17
- "version": "2.2.0",
17
+ "version": "2.3.0",
18
18
  "author": {
19
19
  "name": "Chris Chall"
20
20
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ofw",
3
3
  "displayName": "OurFamilyWizard",
4
- "version": "2.2.0",
4
+ "version": "2.3.0",
5
5
  "description": "OurFamilyWizard co-parenting tools for Claude — messages, calendar, expenses, and journal via MCP",
6
6
  "author": {
7
7
  "name": "Chris Chall"
package/dist/bundle.js CHANGED
@@ -7395,14 +7395,14 @@ var require_permessage_deflate = __commonJS({
7395
7395
  }
7396
7396
  };
7397
7397
  module.exports = PerMessageDeflate2;
7398
- function deflateOnData(chunk) {
7399
- this[kBuffers].push(chunk);
7400
- this[kTotalLength] += chunk.length;
7398
+ function deflateOnData(chunk2) {
7399
+ this[kBuffers].push(chunk2);
7400
+ this[kTotalLength] += chunk2.length;
7401
7401
  }
7402
- function inflateOnData(chunk) {
7403
- this[kTotalLength] += chunk.length;
7402
+ function inflateOnData(chunk2) {
7403
+ this[kTotalLength] += chunk2.length;
7404
7404
  if (this[kPerMessageDeflate]._maxPayload < 1 || this[kTotalLength] <= this[kPerMessageDeflate]._maxPayload) {
7405
- this[kBuffers].push(chunk);
7405
+ this[kBuffers].push(chunk2);
7406
7406
  return;
7407
7407
  }
7408
7408
  this[kError] = new RangeError("Max payload size exceeded");
@@ -7702,7 +7702,7 @@ var require_receiver = __commonJS({
7702
7702
  * @param {Function} cb Callback
7703
7703
  * @private
7704
7704
  */
7705
- _write(chunk, encoding, cb) {
7705
+ _write(chunk2, encoding, cb) {
7706
7706
  if (this._opcode === 8 && this._state == GET_INFO) return cb();
7707
7707
  if (this._maxBufferedChunks > 0 && this._buffers.length >= this._maxBufferedChunks) {
7708
7708
  cb(
@@ -7716,8 +7716,8 @@ var require_receiver = __commonJS({
7716
7716
  );
7717
7717
  return;
7718
7718
  }
7719
- this._bufferedBytes += chunk.length;
7720
- this._buffers.push(chunk);
7719
+ this._bufferedBytes += chunk2.length;
7720
+ this._buffers.push(chunk2);
7721
7721
  this.startLoop(cb);
7722
7722
  }
7723
7723
  /**
@@ -9991,8 +9991,8 @@ var require_websocket = __commonJS({
9991
9991
  this.removeListener("end", socketOnEnd);
9992
9992
  websocket._readyState = WebSocket2.CLOSING;
9993
9993
  if (!this._readableState.endEmitted && !websocket._closeFrameReceived && !websocket._receiver._writableState.errorEmitted && this._readableState.length !== 0) {
9994
- const chunk = this.read(this._readableState.length);
9995
- websocket._receiver.write(chunk);
9994
+ const chunk2 = this.read(this._readableState.length);
9995
+ websocket._receiver.write(chunk2);
9996
9996
  }
9997
9997
  websocket._receiver.end();
9998
9998
  this[kWebSocket] = void 0;
@@ -10004,8 +10004,8 @@ var require_websocket = __commonJS({
10004
10004
  websocket._receiver.on("finish", receiverOnFinish);
10005
10005
  }
10006
10006
  }
10007
- function socketOnData(chunk) {
10008
- if (!this[kWebSocket]._receiver.write(chunk)) {
10007
+ function socketOnData(chunk2) {
10008
+ if (!this[kWebSocket]._receiver.write(chunk2)) {
10009
10009
  this.pause();
10010
10010
  }
10011
10011
  }
@@ -10108,14 +10108,14 @@ var require_stream = __commonJS({
10108
10108
  duplex._read = function() {
10109
10109
  if (ws.isPaused) ws.resume();
10110
10110
  };
10111
- duplex._write = function(chunk, encoding, callback) {
10111
+ duplex._write = function(chunk2, encoding, callback) {
10112
10112
  if (ws.readyState === ws.CONNECTING) {
10113
10113
  ws.once("open", function open() {
10114
- duplex._write(chunk, encoding, callback);
10114
+ duplex._write(chunk2, encoding, callback);
10115
10115
  });
10116
10116
  return;
10117
10117
  }
10118
- ws.send(chunk, callback);
10118
+ ws.send(chunk2, callback);
10119
10119
  };
10120
10120
  duplex.on("end", duplexOnEnd);
10121
10121
  duplex.on("error", duplexOnError);
@@ -34547,8 +34547,8 @@ import process3 from "node:process";
34547
34547
 
34548
34548
  // node_modules/@modelcontextprotocol/sdk/dist/esm/shared/stdio.js
34549
34549
  var ReadBuffer = class {
34550
- append(chunk) {
34551
- this._buffer = this._buffer ? Buffer.concat([this._buffer, chunk]) : chunk;
34550
+ append(chunk2) {
34551
+ this._buffer = this._buffer ? Buffer.concat([this._buffer, chunk2]) : chunk2;
34552
34552
  }
34553
34553
  readMessage() {
34554
34554
  if (!this._buffer) {
@@ -34580,8 +34580,8 @@ var StdioServerTransport = class {
34580
34580
  this._stdout = _stdout;
34581
34581
  this._readBuffer = new ReadBuffer();
34582
34582
  this._started = false;
34583
- this._ondata = (chunk) => {
34584
- this._readBuffer.append(chunk);
34583
+ this._ondata = (chunk2) => {
34584
+ this._readBuffer.append(chunk2);
34585
34585
  this.processReadBuffer();
34586
34586
  };
34587
34587
  this._onerror = (error51) => {
@@ -35999,6 +35999,19 @@ function classifyFetchError(error51) {
35999
35999
  return "other";
36000
36000
  }
36001
36001
 
36002
+ // node_modules/@fetchproxy/server/dist/classify-bridge-error.js
36003
+ function classifyBridgeError(err) {
36004
+ if (err instanceof FetchproxyTimeoutError)
36005
+ return "timeout";
36006
+ if (err instanceof FetchproxyBridgeDownError)
36007
+ return "bridge_down";
36008
+ if (err instanceof FetchproxyHttpError)
36009
+ return "http";
36010
+ if (err instanceof FetchproxyProtocolError)
36011
+ return "protocol";
36012
+ return "other";
36013
+ }
36014
+
36002
36015
  // node_modules/@fetchproxy/server/dist/ws-server.js
36003
36016
  var FetchproxyProtocolError = class extends Error {
36004
36017
  constructor(message) {
@@ -36028,7 +36041,7 @@ var FetchproxyBridgeDownError = class extends FetchproxyProtocolError {
36028
36041
  const retryAttempted = args.retryAttempted ?? false;
36029
36042
  const op = args.op ?? "fetch";
36030
36043
  const retryClause = retryAttempted ? `Server already burned a one-shot lazy-revive retry; SW is still down. ` : `Server lazy-revive retry was disabled (bridgeReviveDelayMs unset/0). `;
36031
- const hint = `the fetchproxy extension's service worker is not responding ("${args.originalError}"). Chrome evicts extension service workers after ~30s idle by default. ${retryClause}Wake it by clicking the fetchproxy extension toolbar icon, then retry. If it keeps happening, reload the extension from chrome://extensions.`;
36044
+ const hint = `the fetchproxy extension's service worker is not responding ("${args.originalError}"). Chrome evicts extension service workers after ~30s idle by default. ${retryClause}Make sure a tab for this domain is open, fully loaded, and signed in (the bridge fetches through that tab) \u2014 then retry. If it keeps happening, reload the extension from chrome://extensions and reload the tab.`;
36032
36045
  super(`fetchproxy bridge down during ${op}${args.url ? ` (${args.url})` : ""}. ${hint}`);
36033
36046
  this.name = "FetchproxyBridgeDownError";
36034
36047
  this.originalError = args.originalError;
@@ -36050,6 +36063,14 @@ var FetchproxyTimeoutError = class extends FetchproxyProtocolError {
36050
36063
  port;
36051
36064
  /** 0.8.0+: actual elapsed milliseconds when the timer won the race. */
36052
36065
  elapsedMs;
36066
+ /**
36067
+ * 0.11.0+ (#90/#91): true when the server's lazy-revive retry path
36068
+ * fired for this timeout (a cold-start `timeout` symptom followed by
36069
+ * a warm-and-retry that also timed out). False when the retry was
36070
+ * disabled (`bridgeReviveDelayMs` unset/0) so the timeout surfaced on
36071
+ * the first attempt.
36072
+ */
36073
+ retryAttempted;
36053
36074
  constructor(args) {
36054
36075
  super(`fetchproxy: ${args.url} did not respond within ${args.timeoutMs}ms`);
36055
36076
  this.name = "FetchproxyTimeoutError";
@@ -36058,6 +36079,7 @@ var FetchproxyTimeoutError = class extends FetchproxyProtocolError {
36058
36079
  this.role = args.role ?? null;
36059
36080
  this.port = args.port ?? 0;
36060
36081
  this.elapsedMs = args.elapsedMs ?? args.timeoutMs;
36082
+ this.retryAttempted = args.retryAttempted ?? false;
36061
36083
  }
36062
36084
  };
36063
36085
  var SUBDOMAIN_LABEL_RE = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i;
@@ -36131,6 +36153,25 @@ var FetchproxyServer = class {
36131
36153
  // for "we're connecting right now" so two parallel first-calls don't
36132
36154
  // race the port bind.
36133
36155
  connectingPromise = null;
36156
+ // 0.8.1+ (#67): server-initiated keep-alive ping. Active when
36157
+ // `keepAliveIntervalMs` is set AND we've seen recent activity
36158
+ // (fetch/capture success or failure, or markActive()) within
36159
+ // `keepAliveMaxIdleMs`. The interval handle is created lazily on
36160
+ // first activity and torn down on close() / extension disconnect.
36161
+ keepAliveTimer = null;
36162
+ lastActiveAt = null;
36163
+ // 0.10.0+ (#73): observability counters surfaced via
36164
+ // bridgeHealth().keepAlive / .swEviction. Monotonic across the process
36165
+ // lifetime so a downstream healthcheck tool can verify the keep-alive
36166
+ // is actually preventing SW eviction. `lastPingAt` and `totalPings`
36167
+ // are stamped from `startKeepaliveIfIdle`'s tick. `lazyRevive*` and
36168
+ // `lastEvictionDetectedAt` are stamped from the lazy-revive code path
36169
+ // in fetch() / captureRequestHeader().
36170
+ lastPingAt = null;
36171
+ totalPings = 0;
36172
+ lazyReviveAttempts = 0;
36173
+ lazyReviveSuccesses = 0;
36174
+ lastEvictionDetectedAt = null;
36134
36175
  constructor(opts) {
36135
36176
  if (!Array.isArray(opts.domains) || opts.domains.length === 0) {
36136
36177
  throw new Error("FetchproxyServer: opts.domains must be a non-empty array of hostnames");
@@ -36183,6 +36224,22 @@ var FetchproxyServer = class {
36183
36224
  // the legacy hang-forever / fail-once-on-SW-eviction behavior.
36184
36225
  fetchTimeoutMs: opts.fetchTimeoutMs ?? 3e4,
36185
36226
  bridgeReviveDelayMs: opts.bridgeReviveDelayMs ?? 2e3,
36227
+ // 0.10.0+ (#72): keep-alive defaults to 25s — round-3 #71 cohort
36228
+ // wave showed every Pattern A consumer was opting into this same
36229
+ // value. Pass `0` to disable; the existing `<= 0` guards in
36230
+ // `startKeepaliveIfIdle` / `noteActivityForKeepalive` honour that.
36231
+ //
36232
+ // #90 (P1-1): tightened to 20s. 25s left only ~5s of slack under
36233
+ // Chrome's ~30s SW-eviction window — slack that timer drift, a
36234
+ // busy host event loop (CPU-bound response parsing between calls),
36235
+ // and the ping's own round-trip latency routinely ate, so the SW
36236
+ // evicted before the next ping landed and the next call cold-
36237
+ // started. 20s restores real margin. (The extension
36238
+ // `chrome.alarms` backstop is clamped by Chrome to a 30s minimum
36239
+ // period, firing *at* the edge — it can't rescue a sub-30s race;
36240
+ // the server ping is the real defense.)
36241
+ keepAliveIntervalMs: opts.keepAliveIntervalMs ?? 2e4,
36242
+ keepAliveMaxIdleMs: opts.keepAliveMaxIdleMs ?? 5 * 60 * 1e3,
36186
36243
  identityDir: opts.identityDir,
36187
36244
  onPairCode: opts.onPairCode
36188
36245
  };
@@ -36283,7 +36340,10 @@ var FetchproxyServer = class {
36283
36340
  onPairCode: this.opts.onPairCode
36284
36341
  });
36285
36342
  this.hostHandle.onOwnInner((inner) => this.onInner(inner));
36286
- this.hostHandle.onExtensionDisconnect(() => this.rejectAllPending());
36343
+ this.hostHandle.onExtensionDisconnect(() => {
36344
+ this.stopKeepalive();
36345
+ this.rejectAllPending();
36346
+ });
36287
36347
  this.hostHandle.onPendingPair((code) => {
36288
36348
  this.rejectAllPending(this.pairingErrorMessage(code));
36289
36349
  });
@@ -36307,7 +36367,10 @@ var FetchproxyServer = class {
36307
36367
  sessionStoragePointers: this.opts.sessionStoragePointers
36308
36368
  });
36309
36369
  this.peerHandle.onInner((inner) => this.onInner(inner));
36310
- this.peerHandle.onRenegotiate(() => this.rejectAllPending());
36370
+ this.peerHandle.onRenegotiate(() => {
36371
+ this.stopKeepalive();
36372
+ this.rejectAllPending();
36373
+ });
36311
36374
  this.peerHandle.onPendingPair((code) => {
36312
36375
  this.rejectAllPending(this.pairingErrorMessage(code));
36313
36376
  });
@@ -36348,13 +36411,18 @@ var FetchproxyServer = class {
36348
36411
  }
36349
36412
  const first = await this._fetchOnceWithTimeout(init);
36350
36413
  const reviveMs = this.opts.bridgeReviveDelayMs;
36351
- let final = first;
36352
- if (!first.ok && first.kind === "content_script_unreachable" && reviveMs !== void 0 && reviveMs > 0) {
36414
+ const isColdStartSymptom = !first.ok && (first.kind === "content_script_unreachable" || first.kind === "timeout");
36415
+ if (isColdStartSymptom) {
36416
+ this.lastEvictionDetectedAt = Date.now();
36417
+ }
36418
+ if (isColdStartSymptom && reviveMs !== void 0 && reviveMs > 0) {
36419
+ this.lazyReviveAttempts += 1;
36353
36420
  await new Promise((r) => setTimeout(r, reviveMs));
36354
36421
  const second = await this._fetchOnceWithTimeout(init);
36355
- if (second.ok)
36422
+ if (second.ok) {
36423
+ this.lazyReviveSuccesses += 1;
36356
36424
  this.recordSuccess();
36357
- else
36425
+ } else
36358
36426
  this.recordFailure(`${second.kind ?? "other"}: ${second.error}`);
36359
36427
  return { ...second, retryAttempted: true };
36360
36428
  }
@@ -36376,6 +36444,9 @@ var FetchproxyServer = class {
36376
36444
  * call (addresses #23 ask 4).
36377
36445
  */
36378
36446
  bridgeHealth() {
36447
+ const intervalMs = this.opts.keepAliveIntervalMs;
36448
+ const maxIdleMs = this.opts.keepAliveMaxIdleMs;
36449
+ const idleSinceMs = this.lastActiveAt === null ? null : Date.now() - this.lastActiveAt;
36379
36450
  return {
36380
36451
  role: this.role,
36381
36452
  port: this.opts.port,
@@ -36386,17 +36457,85 @@ var FetchproxyServer = class {
36386
36457
  lastFailureAt: this.lastFailureAt,
36387
36458
  lastFailureReason: this.lastFailureReason,
36388
36459
  consecutiveFailures: this.consecutiveFailures,
36389
- lastExtensionMessageAt: this.lastExtensionMessageAt
36460
+ lastExtensionMessageAt: this.lastExtensionMessageAt,
36461
+ keepAlive: {
36462
+ enabled: intervalMs > 0,
36463
+ intervalMs,
36464
+ maxIdleMs,
36465
+ lastPingAt: this.lastPingAt,
36466
+ totalPings: this.totalPings,
36467
+ idleSinceMs
36468
+ },
36469
+ swEviction: {
36470
+ lazyReviveAttempts: this.lazyReviveAttempts,
36471
+ lazyReviveSuccesses: this.lazyReviveSuccesses,
36472
+ lastEvictionDetectedAt: this.lastEvictionDetectedAt
36473
+ }
36390
36474
  };
36391
36475
  }
36392
36476
  recordSuccess() {
36393
36477
  this.lastSuccessAt = Date.now();
36394
36478
  this.consecutiveFailures = 0;
36479
+ this.noteActivityForKeepalive();
36395
36480
  }
36396
36481
  recordFailure(reason) {
36397
36482
  this.lastFailureAt = Date.now();
36398
36483
  this.lastFailureReason = reason;
36399
36484
  this.consecutiveFailures += 1;
36485
+ this.noteActivityForKeepalive();
36486
+ }
36487
+ /**
36488
+ * 0.8.1+ (#67): caller-side hint that work is happening or about to
36489
+ * happen — bumps the keep-alive idle gate so the server keeps pinging
36490
+ * the extension. Useful for MCPs that do a chain of side-effectful
36491
+ * work between bridge calls and don't want the SW to evict in the
36492
+ * gap (e.g. server-side parsing of a previous response that takes
36493
+ * tens of seconds). No-op when `keepAliveIntervalMs` is `0`.
36494
+ */
36495
+ markActive() {
36496
+ this.noteActivityForKeepalive();
36497
+ }
36498
+ noteActivityForKeepalive() {
36499
+ const intervalMs = this.opts.keepAliveIntervalMs;
36500
+ if (intervalMs <= 0)
36501
+ return;
36502
+ this.lastActiveAt = Date.now();
36503
+ this.startKeepaliveIfIdle();
36504
+ }
36505
+ startKeepaliveIfIdle() {
36506
+ if (this.keepAliveTimer !== null)
36507
+ return;
36508
+ const intervalMs = this.opts.keepAliveIntervalMs;
36509
+ if (intervalMs <= 0)
36510
+ return;
36511
+ this.keepAliveTimer = setInterval(() => {
36512
+ const now = Date.now();
36513
+ if (this.lastActiveAt === null || now - this.lastActiveAt > this.opts.keepAliveMaxIdleMs) {
36514
+ this.stopKeepalive();
36515
+ return;
36516
+ }
36517
+ this.totalPings += 1;
36518
+ this.lastPingAt = now;
36519
+ void this.sendKeepalivePing();
36520
+ }, intervalMs);
36521
+ }
36522
+ async sendKeepalivePing() {
36523
+ try {
36524
+ const inner = { type: "ping" };
36525
+ if (this.hostHandle) {
36526
+ await this.hostHandle.sendOwnInner(inner);
36527
+ } else if (this.peerHandle) {
36528
+ await this.peerHandle.sendInner(inner);
36529
+ }
36530
+ } catch (e) {
36531
+ console.error("[fetchproxy] keepalive ping send failed:", e);
36532
+ }
36533
+ }
36534
+ stopKeepalive() {
36535
+ if (this.keepAliveTimer !== null) {
36536
+ clearInterval(this.keepAliveTimer);
36537
+ this.keepAliveTimer = null;
36538
+ }
36400
36539
  }
36401
36540
  /**
36402
36541
  * Single bridge round-trip, wrapped by `fetchTimeoutMs` when set.
@@ -36455,7 +36594,8 @@ var FetchproxyServer = class {
36455
36594
  timeoutMs: this.opts.fetchTimeoutMs ?? 0,
36456
36595
  role: this.role,
36457
36596
  port: this.opts.port,
36458
- elapsedMs: result.elapsedMs
36597
+ elapsedMs: result.elapsedMs,
36598
+ retryAttempted
36459
36599
  });
36460
36600
  }
36461
36601
  if (result.kind === "content_script_unreachable") {
@@ -36586,6 +36726,108 @@ var FetchproxyServer = class {
36586
36726
  const response = await this.get(path, this.applyJsonDefaults(opts));
36587
36727
  return response.body;
36588
36728
  }
36729
+ /**
36730
+ * 0.11.0+: method-generic JSON convenience helper. Generalizes the
36731
+ * `fetchJson<T>(path, { method, headers, body })` that
36732
+ * zillow/redfin/compass/homes hand-rolled char-for-char in their
36733
+ * `src/client.ts`:
36734
+ *
36735
+ * - sets `Accept: application/json`;
36736
+ * - adds `Content-Type: application/json` only for a non-GET request
36737
+ * that carries a `body` (and only if the caller didn't set one);
36738
+ * - `JSON.stringify`s the body (GET / no-body sends nothing);
36739
+ * - treats a `204` or an empty body as `data: null` (no parse);
36740
+ * - otherwise `JSON.parse`s the body.
36741
+ *
36742
+ * Scope is serialization + header defaults + 204-handling +
36743
+ * JSON.parse ONLY. It deliberately does NOT assert on the HTTP status
36744
+ * or look for a sign-in interstitial — those guards differ per site
36745
+ * (Zillow's `captcha-delivery`, Redfin's AWS-WAF challenge, …), so it
36746
+ * returns BOTH the parsed `data` and the raw `FetchResult` and leaves
36747
+ * the consumer to run its own `throwIfNotOk` / `throwIfSignInPage`
36748
+ * over `result`.
36749
+ *
36750
+ * Bridge-level failures (no signed-in tab, SW down, timeout) still
36751
+ * throw the typed errors via `request()`, exactly like the verb
36752
+ * helpers — only successful round-trips (any HTTP status) return.
36753
+ */
36754
+ async requestJson(method, path, opts = {}) {
36755
+ const isGet = method.toUpperCase() === "GET";
36756
+ const sendBody = !isGet && opts.body !== void 0;
36757
+ const headers = {
36758
+ Accept: "application/json",
36759
+ ...sendBody && !this.hasContentType(opts.headers ?? {}) ? { "Content-Type": "application/json" } : {},
36760
+ ...opts.headers ?? {}
36761
+ };
36762
+ const response = await this.request(method, path, {
36763
+ headers,
36764
+ body: sendBody ? JSON.stringify(opts.body) : void 0,
36765
+ ...opts.subdomain !== void 0 ? { subdomain: opts.subdomain } : {},
36766
+ ...opts.domain !== void 0 ? { domain: opts.domain } : {}
36767
+ });
36768
+ const result = {
36769
+ ok: true,
36770
+ status: response.status,
36771
+ url: response.url,
36772
+ body: response.body
36773
+ };
36774
+ if (response.status === 204 || response.body === "") {
36775
+ return { data: null, result };
36776
+ }
36777
+ let data;
36778
+ try {
36779
+ data = JSON.parse(response.body);
36780
+ } catch (e) {
36781
+ throw new Error(`fetchproxy ${method} ${path} \u2014 response was not JSON: ${e instanceof Error ? e.message : String(e)}`);
36782
+ }
36783
+ return { data, result };
36784
+ }
36785
+ /**
36786
+ * 0.11.0+: run a single healthcheck probe through `fetchFn`, measure
36787
+ * the elapsed round-trip, classify any thrown error, and project the
36788
+ * post-probe `bridgeHealth()` into a snake-cased `bridge` sub-object.
36789
+ *
36790
+ * This is the transport half of the probe loop zillow/redfin/homes
36791
+ * had duplicated verbatim in `src/tools/healthcheck.ts`. The MCP
36792
+ * supplies its own probe call (`(path) => client.fetchHtml(path)`)
36793
+ * and probe path (e.g. `'/robots.txt'`); the tool registration and
36794
+ * the site-specific plain-English hint text STAY in the consumer.
36795
+ *
36796
+ * `bridgeHealth()` is read AFTER the probe so its freshness counters
36797
+ * (`lastSuccessAt` / `consecutiveFailures` / …) reflect this very
36798
+ * round-trip rather than a stale pre-probe snapshot.
36799
+ */
36800
+ async runProbe(fetchFn, probePath) {
36801
+ const start = Date.now();
36802
+ let ok = false;
36803
+ let error51;
36804
+ try {
36805
+ await fetchFn(probePath);
36806
+ ok = true;
36807
+ } catch (e) {
36808
+ error51 = {
36809
+ kind: classifyBridgeError(e),
36810
+ message: e instanceof Error ? e.message : String(e)
36811
+ };
36812
+ }
36813
+ const elapsed_ms = Date.now() - start;
36814
+ const health = this.bridgeHealth();
36815
+ return {
36816
+ ok,
36817
+ elapsed_ms,
36818
+ bridge: {
36819
+ role: health.role,
36820
+ port: health.port,
36821
+ server_version: health.serverVersion,
36822
+ fetch_timeout_ms: health.fetchTimeoutMs,
36823
+ last_success_at: health.lastSuccessAt,
36824
+ last_failure_at: health.lastFailureAt,
36825
+ last_failure_reason: health.lastFailureReason,
36826
+ consecutive_failures: health.consecutiveFailures
36827
+ },
36828
+ ...error51 ? { error: error51 } : {}
36829
+ };
36830
+ }
36589
36831
  /**
36590
36832
  * Snapshot the user's non-HttpOnly cookies for the chosen domain.
36591
36833
  *
@@ -36754,11 +36996,14 @@ var FetchproxyServer = class {
36754
36996
  this.recordFailure(`capture_request_header: ${err.message ?? String(err)}`);
36755
36997
  throw err;
36756
36998
  }
36999
+ this.lastEvictionDetectedAt = Date.now();
36757
37000
  const reviveMs = this.opts.bridgeReviveDelayMs ?? 0;
36758
37001
  if (reviveMs > 0) {
37002
+ this.lazyReviveAttempts += 1;
36759
37003
  await new Promise((r) => setTimeout(r, reviveMs));
36760
37004
  try {
36761
37005
  const result = await this._captureRequestHeaderOnce(callOpts);
37006
+ this.lazyReviveSuccesses += 1;
36762
37007
  this.recordSuccess();
36763
37008
  return result;
36764
37009
  } catch (retryErr) {
@@ -37051,6 +37296,7 @@ var FetchproxyServer = class {
37051
37296
  * twice in a row.
37052
37297
  */
37053
37298
  async close() {
37299
+ this.stopKeepalive();
37054
37300
  this.rejectAllPending();
37055
37301
  if (this.connectingPromise) {
37056
37302
  await this.connectingPromise.catch(() => void 0);
@@ -37066,19 +37312,6 @@ var FetchproxyServer = class {
37066
37312
  }
37067
37313
  };
37068
37314
 
37069
- // node_modules/@fetchproxy/server/dist/classify-bridge-error.js
37070
- function classifyBridgeError(err) {
37071
- if (err instanceof FetchproxyTimeoutError)
37072
- return "timeout";
37073
- if (err instanceof FetchproxyBridgeDownError)
37074
- return "bridge_down";
37075
- if (err instanceof FetchproxyHttpError)
37076
- return "http";
37077
- if (err instanceof FetchproxyProtocolError)
37078
- return "protocol";
37079
- return "other";
37080
- }
37081
-
37082
37315
  // node_modules/@fetchproxy/bootstrap/dist/index.js
37083
37316
  var defaultFactory = (opts) => new FetchproxyServer(opts);
37084
37317
  async function bootstrap(opts) {
@@ -37322,7 +37555,7 @@ function getDefaultInlineAttachments() {
37322
37555
  // package.json
37323
37556
  var package_default = {
37324
37557
  name: "ofw-mcp",
37325
- version: "2.2.0",
37558
+ version: "2.3.0",
37326
37559
  mcpName: "io.github.chrischall/ofw-mcp",
37327
37560
  description: "OurFamilyWizard MCP server for Claude \u2014 developed and maintained by AI (Claude Code)",
37328
37561
  author: "Claude Code (AI) <https://www.anthropic.com/claude>",
@@ -37349,8 +37582,8 @@ var package_default = {
37349
37582
  "test:watch": "vitest"
37350
37583
  },
37351
37584
  dependencies: {
37352
- "@fetchproxy/bootstrap": "^0.8.0",
37353
- "@fetchproxy/server": "^0.8.0",
37585
+ "@fetchproxy/bootstrap": "^0.11.0",
37586
+ "@fetchproxy/server": "^0.11.0",
37354
37587
  "@modelcontextprotocol/sdk": "^1.29.0",
37355
37588
  dotenv: "^17.4.2",
37356
37589
  zod: "^4.4.3"
@@ -38784,7 +39017,7 @@ process.emit = function(event, ...args) {
38784
39017
  }
38785
39018
  return originalEmit(event, ...args);
38786
39019
  };
38787
- var server = new McpServer({ name: "ofw", version: "2.2.0" });
39020
+ var server = new McpServer({ name: "ofw", version: "2.3.0" });
38788
39021
  registerUserTools(server, client);
38789
39022
  registerMessageTools(server, client);
38790
39023
  registerCalendarTools(server, client);
package/dist/index.js CHANGED
@@ -17,7 +17,7 @@ import { registerMessageTools } from './tools/messages.js';
17
17
  import { registerCalendarTools } from './tools/calendar.js';
18
18
  import { registerExpenseTools } from './tools/expenses.js';
19
19
  import { registerJournalTools } from './tools/journal.js';
20
- const server = new McpServer({ name: 'ofw', version: '2.2.0' }); // x-release-please-version
20
+ const server = new McpServer({ name: 'ofw', version: '2.3.0' }); // x-release-please-version
21
21
  registerUserTools(server, client);
22
22
  registerMessageTools(server, client);
23
23
  registerCalendarTools(server, client);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ofw-mcp",
3
- "version": "2.2.0",
3
+ "version": "2.3.0",
4
4
  "mcpName": "io.github.chrischall/ofw-mcp",
5
5
  "description": "OurFamilyWizard MCP server for Claude — developed and maintained by AI (Claude Code)",
6
6
  "author": "Claude Code (AI) <https://www.anthropic.com/claude>",
@@ -27,8 +27,8 @@
27
27
  "test:watch": "vitest"
28
28
  },
29
29
  "dependencies": {
30
- "@fetchproxy/bootstrap": "^0.8.0",
31
- "@fetchproxy/server": "^0.8.0",
30
+ "@fetchproxy/bootstrap": "^0.11.0",
31
+ "@fetchproxy/server": "^0.11.0",
32
32
  "@modelcontextprotocol/sdk": "^1.29.0",
33
33
  "dotenv": "^17.4.2",
34
34
  "zod": "^4.4.3"
package/server.json CHANGED
@@ -6,12 +6,12 @@
6
6
  "url": "https://github.com/chrischall/ofw-mcp",
7
7
  "source": "github"
8
8
  },
9
- "version": "2.2.0",
9
+ "version": "2.3.0",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "ofw-mcp",
14
- "version": "2.2.0",
14
+ "version": "2.3.0",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },