ofw-mcp 2.10.2 → 2.12.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/dist/bundle.js CHANGED
@@ -2991,7 +2991,7 @@ var require_compile = __commonJS({
2991
2991
  const schOrFunc = root.refs[ref];
2992
2992
  if (schOrFunc)
2993
2993
  return schOrFunc;
2994
- let _sch = resolve2.call(this, root, ref);
2994
+ let _sch = resolve3.call(this, root, ref);
2995
2995
  if (_sch === void 0) {
2996
2996
  const schema = (_a3 = root.localRefs) === null || _a3 === void 0 ? void 0 : _a3[ref];
2997
2997
  const { schemaId } = this.opts;
@@ -3018,7 +3018,7 @@ var require_compile = __commonJS({
3018
3018
  function sameSchemaEnv(s1, s2) {
3019
3019
  return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
3020
3020
  }
3021
- function resolve2(root, ref) {
3021
+ function resolve3(root, ref) {
3022
3022
  let sch;
3023
3023
  while (typeof (sch = this.refs[ref]) == "string")
3024
3024
  ref = sch;
@@ -3649,7 +3649,7 @@ var require_fast_uri = __commonJS({
3649
3649
  }
3650
3650
  return uri;
3651
3651
  }
3652
- function resolve2(baseURI, relativeURI, options) {
3652
+ function resolve3(baseURI, relativeURI, options) {
3653
3653
  const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
3654
3654
  const { parsed: baseParsed, malformedAuthorityOrPort: baseMalformed } = parseWithStatus(baseURI, schemelessOptions);
3655
3655
  const { parsed: relativeParsed, malformedAuthorityOrPort: relativeMalformed } = parseWithStatus(relativeURI, schemelessOptions);
@@ -3933,7 +3933,7 @@ var require_fast_uri = __commonJS({
3933
3933
  var fastUri = {
3934
3934
  SCHEMES,
3935
3935
  normalize,
3936
- resolve: resolve2,
3936
+ resolve: resolve3,
3937
3937
  resolveComponent,
3938
3938
  equal,
3939
3939
  serialize,
@@ -9161,7 +9161,7 @@ var require_websocket = __commonJS({
9161
9161
  var http = __require("http");
9162
9162
  var net = __require("net");
9163
9163
  var tls = __require("tls");
9164
- var { randomBytes, createHash: createHash2 } = __require("crypto");
9164
+ var { randomBytes: randomBytes2, createHash: createHash2 } = __require("crypto");
9165
9165
  var { Duplex, Readable } = __require("stream");
9166
9166
  var { URL: URL2 } = __require("url");
9167
9167
  var PerMessageDeflate2 = require_permessage_deflate();
@@ -9699,7 +9699,7 @@ var require_websocket = __commonJS({
9699
9699
  }
9700
9700
  }
9701
9701
  const defaultPort = isSecure ? 443 : 80;
9702
- const key = randomBytes(16).toString("base64");
9702
+ const key = randomBytes2(16).toString("base64");
9703
9703
  const request = isSecure ? https.request : http.request;
9704
9704
  const protocolSet = /* @__PURE__ */ new Set();
9705
9705
  let perMessageDeflate;
@@ -32550,7 +32550,7 @@ var Protocol = class {
32550
32550
  return;
32551
32551
  }
32552
32552
  const pollInterval = task2.pollInterval ?? this._options?.defaultTaskPollInterval ?? 1e3;
32553
- await new Promise((resolve2) => setTimeout(resolve2, pollInterval));
32553
+ await new Promise((resolve3) => setTimeout(resolve3, pollInterval));
32554
32554
  options?.signal?.throwIfAborted();
32555
32555
  }
32556
32556
  } catch (error51) {
@@ -32567,7 +32567,7 @@ var Protocol = class {
32567
32567
  */
32568
32568
  request(request, resultSchema, options) {
32569
32569
  const { relatedRequestId, resumptionToken, onresumptiontoken, task, relatedTask } = options ?? {};
32570
- return new Promise((resolve2, reject) => {
32570
+ return new Promise((resolve3, reject) => {
32571
32571
  const earlyReject = (error51) => {
32572
32572
  reject(error51);
32573
32573
  };
@@ -32645,7 +32645,7 @@ var Protocol = class {
32645
32645
  if (!parseResult.success) {
32646
32646
  reject(parseResult.error);
32647
32647
  } else {
32648
- resolve2(parseResult.data);
32648
+ resolve3(parseResult.data);
32649
32649
  }
32650
32650
  } catch (error51) {
32651
32651
  reject(error51);
@@ -32906,12 +32906,12 @@ var Protocol = class {
32906
32906
  }
32907
32907
  } catch {
32908
32908
  }
32909
- return new Promise((resolve2, reject) => {
32909
+ return new Promise((resolve3, reject) => {
32910
32910
  if (signal.aborted) {
32911
32911
  reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
32912
32912
  return;
32913
32913
  }
32914
- const timeoutId = setTimeout(resolve2, interval);
32914
+ const timeoutId = setTimeout(resolve3, interval);
32915
32915
  signal.addEventListener("abort", () => {
32916
32916
  clearTimeout(timeoutId);
32917
32917
  reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
@@ -34002,7 +34002,7 @@ var McpServer = class {
34002
34002
  let task = createTaskResult.task;
34003
34003
  const pollInterval = task.pollInterval ?? 5e3;
34004
34004
  while (task.status !== "completed" && task.status !== "failed" && task.status !== "cancelled") {
34005
- await new Promise((resolve2) => setTimeout(resolve2, pollInterval));
34005
+ await new Promise((resolve3) => setTimeout(resolve3, pollInterval));
34006
34006
  const updatedTask = await extra.taskStore.getTask(taskId);
34007
34007
  if (!updatedTask) {
34008
34008
  throw new McpError(ErrorCode.InternalError, `Task ${taskId} not found during polling`);
@@ -34666,64 +34666,17 @@ var StdioServerTransport = class {
34666
34666
  this.onclose?.();
34667
34667
  }
34668
34668
  send(message) {
34669
- return new Promise((resolve2) => {
34669
+ return new Promise((resolve3) => {
34670
34670
  const json2 = serializeMessage(message);
34671
34671
  if (this._stdout.write(json2)) {
34672
- resolve2();
34672
+ resolve3();
34673
34673
  } else {
34674
- this._stdout.once("drain", resolve2);
34674
+ this._stdout.once("drain", resolve3);
34675
34675
  }
34676
34676
  });
34677
34677
  }
34678
34678
  };
34679
34679
 
34680
- // node_modules/@chrischall/mcp-utils/dist/server/index.js
34681
- async function createMcpServer(opts) {
34682
- const server = new McpServer({ name: opts.name, version: opts.version });
34683
- if (opts.banner !== void 0) {
34684
- console.error(opts.banner);
34685
- }
34686
- const deps = opts.deps;
34687
- for (const register of opts.tools) {
34688
- await register(server, deps);
34689
- }
34690
- return server;
34691
- }
34692
- function withGracefulShutdown(server, opts = {}) {
34693
- const shouldExit = opts.exit ?? true;
34694
- let shuttingDown = false;
34695
- const handler = (signal) => {
34696
- if (shuttingDown)
34697
- return;
34698
- shuttingDown = true;
34699
- void (async () => {
34700
- try {
34701
- if (opts.onSignal)
34702
- await opts.onSignal(signal);
34703
- await server.close();
34704
- } catch (err) {
34705
- console.error(`[mcp-utils] error during graceful shutdown on ${signal}: ${err instanceof Error ? err.message : String(err)}`);
34706
- } finally {
34707
- if (shouldExit)
34708
- process.exit(0);
34709
- }
34710
- })();
34711
- };
34712
- process.on("SIGINT", () => handler("SIGINT"));
34713
- process.on("SIGTERM", () => handler("SIGTERM"));
34714
- }
34715
- async function runMcp(opts) {
34716
- const server = await createMcpServer(opts);
34717
- const shutdown = opts.shutdown ?? true;
34718
- if (shutdown !== false) {
34719
- withGracefulShutdown(server, shutdown === true ? {} : shutdown);
34720
- }
34721
- const spec = opts.transport ?? "stdio";
34722
- const transport = spec === "stdio" ? new StdioServerTransport() : spec;
34723
- await server.connect(transport);
34724
- return server;
34725
- }
34726
-
34727
34680
  // node_modules/@chrischall/mcp-utils/dist/errors/index.js
34728
34681
  var McpToolError = class extends Error {
34729
34682
  /** Actionable remediation text, when one applies. */
@@ -34736,6 +34689,9 @@ var McpToolError = class extends Error {
34736
34689
  Object.setPrototypeOf(this, new.target.prototype);
34737
34690
  }
34738
34691
  };
34692
+ function createHelpfulError(message, opts) {
34693
+ return new McpToolError(message, opts);
34694
+ }
34739
34695
  var BEARER_RE = /(bearer\s+)[A-Za-z0-9._~+/=-]{8,}/gi;
34740
34696
  var JWT_RE = /\b[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{8,}\b/g;
34741
34697
  var BASIC_AUTH_RE = /(authorization\s*:\s*basic\s+)[A-Za-z0-9+/=_-]{6,}/gi;
@@ -34773,6 +34729,81 @@ function textResult(data) {
34773
34729
  function rawTextResult(text) {
34774
34730
  return { content: [{ type: "text", text }] };
34775
34731
  }
34732
+ function errorResult(message) {
34733
+ return {
34734
+ content: [{ type: "text", text: redactSecrets(message) }],
34735
+ isError: true
34736
+ };
34737
+ }
34738
+
34739
+ // node_modules/@chrischall/mcp-utils/dist/server/index.js
34740
+ function hintResultOrRethrow(err) {
34741
+ if (err instanceof McpToolError && err.hint) {
34742
+ return errorResult(`${err.message}
34743
+
34744
+ Hint: ${err.hint}`);
34745
+ }
34746
+ throw err;
34747
+ }
34748
+ function surfaceToolHints(server) {
34749
+ const register = server.registerTool.bind(server);
34750
+ server.registerTool = (name, config2, cb) => register(name, config2, (...args) => {
34751
+ let result;
34752
+ try {
34753
+ result = cb(...args);
34754
+ } catch (err) {
34755
+ return hintResultOrRethrow(err);
34756
+ }
34757
+ return result instanceof Promise ? result.catch(hintResultOrRethrow) : result;
34758
+ });
34759
+ }
34760
+ async function createMcpServer(opts) {
34761
+ const server = new McpServer({ name: opts.name, version: opts.version });
34762
+ if (opts.surfaceHints !== false)
34763
+ surfaceToolHints(server);
34764
+ if (opts.banner !== void 0) {
34765
+ console.error(opts.banner);
34766
+ }
34767
+ const deps = opts.deps;
34768
+ for (const register of opts.tools) {
34769
+ await register(server, deps);
34770
+ }
34771
+ return server;
34772
+ }
34773
+ function withGracefulShutdown(server, opts = {}) {
34774
+ const shouldExit = opts.exit ?? true;
34775
+ let shuttingDown = false;
34776
+ const handler = (signal) => {
34777
+ if (shuttingDown)
34778
+ return;
34779
+ shuttingDown = true;
34780
+ void (async () => {
34781
+ try {
34782
+ if (opts.onSignal)
34783
+ await opts.onSignal(signal);
34784
+ await server.close();
34785
+ } catch (err) {
34786
+ console.error(`[mcp-utils] error during graceful shutdown on ${signal}: ${err instanceof Error ? err.message : String(err)}`);
34787
+ } finally {
34788
+ if (shouldExit)
34789
+ process.exit(0);
34790
+ }
34791
+ })();
34792
+ };
34793
+ process.on("SIGINT", () => handler("SIGINT"));
34794
+ process.on("SIGTERM", () => handler("SIGTERM"));
34795
+ }
34796
+ async function runMcp(opts) {
34797
+ const server = await createMcpServer(opts);
34798
+ const shutdown = opts.shutdown ?? true;
34799
+ if (shutdown !== false) {
34800
+ withGracefulShutdown(server, shutdown === true ? {} : shutdown);
34801
+ }
34802
+ const spec = opts.transport ?? "stdio";
34803
+ const transport = spec === "stdio" ? new StdioServerTransport() : spec;
34804
+ await server.connect(transport);
34805
+ return server;
34806
+ }
34776
34807
 
34777
34808
  // node_modules/@chrischall/mcp-utils/dist/config/index.js
34778
34809
  import { homedir } from "node:os";
@@ -34844,6 +34875,34 @@ async function fileBlob(path, opts = {}) {
34844
34875
  return blob;
34845
34876
  }
34846
34877
 
34878
+ // node_modules/@chrischall/mcp-utils/dist/http/index.js
34879
+ var RateLimitedError = class extends Error {
34880
+ status = 429;
34881
+ constructor(service) {
34882
+ super(`Rate limited (429) by ${service} after retries.`);
34883
+ this.name = "RateLimitedError";
34884
+ Object.setPrototypeOf(this, new.target.prototype);
34885
+ }
34886
+ };
34887
+ var RequestTimeoutError = class extends Error {
34888
+ timeoutMs;
34889
+ constructor(service, timeoutMs) {
34890
+ super(`Request to ${service} timed out after ${timeoutMs}ms.`);
34891
+ this.name = "RequestTimeoutError";
34892
+ this.timeoutMs = timeoutMs;
34893
+ Object.setPrototypeOf(this, new.target.prototype);
34894
+ }
34895
+ };
34896
+ var ApiError = class extends Error {
34897
+ status;
34898
+ constructor(status, message) {
34899
+ super(message);
34900
+ this.name = "ApiError";
34901
+ this.status = status;
34902
+ Object.setPrototypeOf(this, new.target.prototype);
34903
+ }
34904
+ };
34905
+
34847
34906
  // node_modules/@chrischall/mcp-utils/dist/zod/parse-lenient.js
34848
34907
  function parseLenient(schema, raw, opts) {
34849
34908
  const result = schema.safeParse(raw);
@@ -34878,58 +34937,329 @@ var pageSchema = {
34878
34937
  page_size: external_exports.number().int().min(1).max(200).default(50).describe("Number of items per page (1-200).")
34879
34938
  };
34880
34939
 
34940
+ // node_modules/@chrischall/mcp-utils/dist/auth/index.js
34941
+ async function resolveAuthPattern(pattern) {
34942
+ const ordered = [
34943
+ pattern.token,
34944
+ pattern.oauth,
34945
+ pattern.sessionScrape,
34946
+ pattern.fetchproxy
34947
+ ];
34948
+ for (const resolver of ordered) {
34949
+ if (resolver)
34950
+ return resolver();
34951
+ }
34952
+ throw createHelpfulError("No auth configured. Provide a token, OAuth credentials, login credentials, or sign in in your browser (fetchproxy fallback).", { hint: "Set one of the supported credential env vars, or sign in in your browser." });
34953
+ }
34954
+
34881
34955
  // node_modules/@chrischall/mcp-utils/dist/session/index.js
34956
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync, renameSync, unlinkSync } from "node:fs";
34957
+ import { dirname, join as join2, resolve as resolve2 } from "node:path";
34958
+ import { homedir as homedir2 } from "node:os";
34959
+ import { randomBytes, createHmac } from "node:crypto";
34960
+ function bindingDigest(secret, salt) {
34961
+ return createHmac("sha256", salt).update(secret).digest("hex").slice(0, 32);
34962
+ }
34963
+ function isEnvelope(raw) {
34964
+ return raw !== null && typeof raw === "object" && raw.v === 1 && "state" in raw;
34965
+ }
34966
+ var StatePersistenceError = class extends Error {
34967
+ cause;
34968
+ constructor(cause) {
34969
+ super(cause instanceof Error ? cause.message : String(cause));
34970
+ this.name = "StatePersistenceError";
34971
+ this.cause = cause;
34972
+ Object.setPrototypeOf(this, new.target.prototype);
34973
+ }
34974
+ };
34975
+ function createFileStatePersistence(opts) {
34976
+ const { filePath, validate } = opts;
34977
+ const secret = opts.boundTo;
34978
+ return {
34979
+ load() {
34980
+ if (!existsSync(filePath))
34981
+ return null;
34982
+ try {
34983
+ const raw = JSON.parse(readFileSync(filePath, "utf8"));
34984
+ let state;
34985
+ if (isEnvelope(raw)) {
34986
+ const b = raw.boundTo;
34987
+ if (secret === void 0) {
34988
+ if (b !== void 0)
34989
+ return null;
34990
+ } else {
34991
+ if (b === void 0)
34992
+ return null;
34993
+ if (bindingDigest(secret, b.salt) !== b.digest)
34994
+ return null;
34995
+ }
34996
+ state = raw.state;
34997
+ } else {
34998
+ if (secret !== void 0)
34999
+ return null;
35000
+ state = raw;
35001
+ }
35002
+ if (validate !== void 0)
35003
+ return validate(state);
35004
+ return state;
35005
+ } catch {
35006
+ return null;
35007
+ }
35008
+ },
35009
+ save(state) {
35010
+ const dir = dirname(filePath);
35011
+ const tmp = `${filePath}.tmp-${randomBytes(6).toString("hex")}`;
35012
+ const dirExisted = existsSync(dir);
35013
+ try {
35014
+ mkdirSync(dir, { recursive: true, mode: 448 });
35015
+ if (!dirExisted)
35016
+ chmodSync(dir, 448);
35017
+ let boundTo;
35018
+ if (secret !== void 0) {
35019
+ const salt = randomBytes(16).toString("hex");
35020
+ boundTo = { salt, digest: bindingDigest(secret, salt) };
35021
+ }
35022
+ const envelope = { v: 1, ...boundTo !== void 0 ? { boundTo } : {}, state };
35023
+ writeFileSync(tmp, JSON.stringify(envelope, null, 2), { mode: 384 });
35024
+ chmodSync(tmp, 384);
35025
+ renameSync(tmp, filePath);
35026
+ chmodSync(filePath, 384);
35027
+ } catch (err) {
35028
+ try {
35029
+ if (existsSync(tmp))
35030
+ unlinkSync(tmp);
35031
+ } catch {
35032
+ }
35033
+ throw err;
35034
+ }
35035
+ },
35036
+ clear() {
35037
+ try {
35038
+ if (existsSync(filePath))
35039
+ unlinkSync(filePath);
35040
+ } catch {
35041
+ }
35042
+ }
35043
+ };
35044
+ }
35045
+ function homeOf(env) {
35046
+ return readEnvVar("HOME", { env }) ?? homedir2();
35047
+ }
35048
+ function resolveStateDir(opts = {}) {
35049
+ const env = opts.env;
35050
+ return join2(readEnvVar("MCP_DATA_DIR", { env }) ?? homeOf(env), opts.subdir ?? "");
35051
+ }
35052
+ function resolveStateFile(opts) {
35053
+ const override = opts.envVar !== void 0 ? readEnvVar(opts.envVar, { env: opts.env }) : void 0;
35054
+ if (override !== void 0) {
35055
+ if (override === "~")
35056
+ return homeOf(opts.env);
35057
+ if (override.startsWith("~/"))
35058
+ return join2(homeOf(opts.env), override.slice(2));
35059
+ return resolve2(override);
35060
+ }
35061
+ const dirOpts = { env: opts.env };
35062
+ if (opts.subdir !== void 0)
35063
+ dirOpts.subdir = opts.subdir;
35064
+ return join2(resolveStateDir(dirOpts), opts.fileName);
35065
+ }
34882
35066
  var TOKEN_REFRESH_SKEW_MS = 5 * 60 * 1e3;
35067
+ function defaultIsRefreshRevoked(err) {
35068
+ if (err instanceof RateLimitedError || err instanceof RequestTimeoutError)
35069
+ return false;
35070
+ if (err instanceof ApiError && err.status >= 500)
35071
+ return false;
35072
+ return true;
35073
+ }
35074
+ function isBearerTokens(raw) {
35075
+ if (raw === null || typeof raw !== "object")
35076
+ return false;
35077
+ const t = raw;
35078
+ if (typeof t.accessToken !== "string" || t.accessToken === "")
35079
+ return false;
35080
+ if (typeof t.expiresAt !== "number" || !Number.isFinite(t.expiresAt))
35081
+ return false;
35082
+ return t.refreshToken === void 0 || typeof t.refreshToken === "string";
35083
+ }
34883
35084
  var TokenManager = class {
34884
- accessToken;
34885
- refreshToken;
34886
- expiresAt;
35085
+ tokens;
35086
+ bootstrapFn;
34887
35087
  refreshFn;
34888
35088
  skewMs;
35089
+ persistence;
35090
+ now;
35091
+ isRefreshRevokedFn;
35092
+ onPersistErrorFn;
34889
35093
  inFlight;
35094
+ bootstrapInFlight;
35095
+ /**
35096
+ * Persistence is consulted at most once per process. Without this the
35097
+ * revoked-token recovery below re-reads the SAME rejected record — `clear()`
35098
+ * is optional on {@link StatePersistence} and its failures are swallowed, so
35099
+ * recovery must not depend on it. After the first read the in-memory tokens
35100
+ * (or their deliberate absence) are the truth.
35101
+ */
35102
+ persistenceRead = false;
34890
35103
  constructor(opts) {
34891
- this.accessToken = opts.initial.accessToken;
34892
- this.refreshToken = opts.initial.refreshToken;
34893
- this.expiresAt = opts.initial.expiresAt;
35104
+ if (typeof opts.initial === "function") {
35105
+ this.bootstrapFn = opts.initial;
35106
+ } else {
35107
+ this.tokens = { ...opts.initial };
35108
+ }
34894
35109
  this.refreshFn = opts.refresh;
34895
35110
  this.skewMs = opts.skewMs ?? TOKEN_REFRESH_SKEW_MS;
35111
+ this.persistence = opts.persistence;
35112
+ this.now = opts.now ?? Date.now;
35113
+ this.isRefreshRevokedFn = opts.isRefreshRevoked ?? defaultIsRefreshRevoked;
35114
+ this.onPersistErrorFn = opts.onPersistError;
34896
35115
  }
34897
35116
  /** Whether the token is within the skew window of (or past) expiry. */
34898
35117
  needsRefresh() {
34899
- return Date.now() >= this.expiresAt - this.skewMs;
35118
+ if (this.tokens === void 0)
35119
+ return false;
35120
+ return this.now() >= this.tokens.expiresAt - this.skewMs;
35121
+ }
35122
+ /**
35123
+ * A stored token is worth using when it is still valid, OR when it carries a
35124
+ * refresh token — an expired-but-refreshable token still saves the login,
35125
+ * which is the expensive half.
35126
+ */
35127
+ isUsable(t) {
35128
+ return this.now() < t.expiresAt - this.skewMs || t.refreshToken !== void 0;
35129
+ }
35130
+ /** Read persisted tokens, guarding shape and usability. Never throws. */
35131
+ async loadPersisted() {
35132
+ if (this.persistence === void 0 || this.persistenceRead)
35133
+ return null;
35134
+ this.persistenceRead = true;
35135
+ try {
35136
+ const raw = await this.persistence.load();
35137
+ if (!isBearerTokens(raw) || !this.isUsable(raw))
35138
+ return null;
35139
+ return raw;
35140
+ } catch {
35141
+ return null;
35142
+ }
35143
+ }
35144
+ /**
35145
+ * Write tokens. Silent by default (a lost write costs a future login, not this
35146
+ * request); throws a {@link StatePersistenceError} when `onPersistError` does.
35147
+ */
35148
+ async persist(t) {
35149
+ if (this.persistence === void 0)
35150
+ return;
35151
+ try {
35152
+ await this.persistence.save(t);
35153
+ } catch (err) {
35154
+ if (this.onPersistErrorFn === void 0)
35155
+ return;
35156
+ try {
35157
+ this.onPersistErrorFn(err);
35158
+ } catch (hookErr) {
35159
+ throw new StatePersistenceError(hookErr);
35160
+ }
35161
+ }
35162
+ }
35163
+ /** Discard persisted tokens (a refresh they could not satisfy). Never throws. */
35164
+ async clearPersisted() {
35165
+ if (this.persistence?.clear === void 0)
35166
+ return;
35167
+ try {
35168
+ await this.persistence.clear();
35169
+ } catch {
35170
+ }
35171
+ }
35172
+ /** The current tokens, single-flighting the bootstrap if there are none. */
35173
+ ensureTokens() {
35174
+ if (this.tokens !== void 0)
35175
+ return Promise.resolve(this.tokens);
35176
+ if (this.bootstrapInFlight === void 0) {
35177
+ this.bootstrapInFlight = this.runBootstrap().finally(() => {
35178
+ this.bootstrapInFlight = void 0;
35179
+ });
35180
+ }
35181
+ return this.bootstrapInFlight;
35182
+ }
35183
+ /** One bootstrap attempt: persisted tokens if usable, else the login. */
35184
+ async runBootstrap() {
35185
+ const stored = await this.loadPersisted();
35186
+ if (stored !== null) {
35187
+ this.tokens = stored;
35188
+ return stored;
35189
+ }
35190
+ if (this.bootstrapFn === void 0) {
35191
+ throw new Error("TokenManager: no tokens and no bootstrap function to mint them.");
35192
+ }
35193
+ const fresh = await this.bootstrapFn();
35194
+ this.tokens = fresh;
35195
+ try {
35196
+ await this.persist(fresh);
35197
+ } catch (err) {
35198
+ throw err instanceof StatePersistenceError ? err.cause : err;
35199
+ }
35200
+ return fresh;
34900
35201
  }
34901
35202
  /**
34902
35203
  * Single-flight refresh. Concurrent callers share one in-flight promise; it is
34903
35204
  * cleared on settle (success or failure) so a subsequent refresh can proceed.
34904
35205
  */
34905
35206
  refreshNow() {
34906
- if (!this.inFlight) {
34907
- const rt = this.refreshToken;
34908
- if (rt === void 0) {
34909
- return Promise.reject(new Error("TokenManager: cannot refresh \u2014 no refresh token is available."));
34910
- }
34911
- this.inFlight = (async () => {
34912
- const tok = await this.refreshFn(rt);
34913
- this.accessToken = tok.accessToken;
34914
- if (tok.refreshToken !== void 0 && tok.refreshToken !== "") {
34915
- this.refreshToken = tok.refreshToken;
34916
- }
34917
- this.expiresAt = tok.expiresAt;
34918
- })().finally(() => {
35207
+ if (this.inFlight === void 0) {
35208
+ this.inFlight = this.runRefresh().finally(() => {
34919
35209
  this.inFlight = void 0;
34920
35210
  });
34921
35211
  }
34922
35212
  return this.inFlight;
34923
35213
  }
35214
+ /** One refresh attempt against the current refresh token. */
35215
+ async runRefresh() {
35216
+ const current = this.tokens ?? await this.ensureTokens();
35217
+ const rt = current.refreshToken;
35218
+ if (rt === void 0) {
35219
+ throw new Error("TokenManager: cannot refresh \u2014 no refresh token is available.");
35220
+ }
35221
+ const tok = await this.refreshFn(rt);
35222
+ this.tokens = {
35223
+ accessToken: tok.accessToken,
35224
+ // Rotation is optional: keep the current refresh token when none comes back.
35225
+ refreshToken: tok.refreshToken !== void 0 && tok.refreshToken !== "" ? tok.refreshToken : rt,
35226
+ expiresAt: tok.expiresAt
35227
+ };
35228
+ await this.persist(this.tokens);
35229
+ }
35230
+ /**
35231
+ * Recover from a refresh the current credential could not satisfy — commonly
35232
+ * a refresh token restored from a previous process and revoked since. Without
35233
+ * a bootstrap to fall back on this is terminal; with one, re-minting beats
35234
+ * staying broken forever. Shared so the two entry points cannot diverge.
35235
+ */
35236
+ async reBootstrap(err) {
35237
+ if (err instanceof StatePersistenceError)
35238
+ throw err.cause;
35239
+ if (this.bootstrapFn === void 0)
35240
+ throw err;
35241
+ if (!this.isRefreshRevokedFn(err))
35242
+ throw err;
35243
+ this.tokens = void 0;
35244
+ await this.clearPersisted();
35245
+ return this.ensureTokens();
35246
+ }
34924
35247
  /** Get a valid access token, refreshing proactively inside the skew window. */
34925
35248
  async getAccessToken() {
34926
- if (this.needsRefresh())
34927
- await this.refreshNow();
34928
- return this.accessToken;
35249
+ let tokens = this.tokens ?? await this.ensureTokens();
35250
+ if (this.needsRefresh()) {
35251
+ try {
35252
+ await this.refreshNow();
35253
+ } catch (err) {
35254
+ return (await this.reBootstrap(err)).accessToken;
35255
+ }
35256
+ tokens = this.tokens ?? tokens;
35257
+ }
35258
+ return tokens.accessToken;
34929
35259
  }
34930
- /** Current absolute expiry (epoch ms). */
35260
+ /** Current absolute expiry (epoch ms), or `0` before the first bootstrap. */
34931
35261
  getExpiresAt() {
34932
- return this.expiresAt;
35262
+ return this.tokens?.expiresAt ?? 0;
34933
35263
  }
34934
35264
  /**
34935
35265
  * Run an authenticated request with reactive 401-replay. `call` receives a
@@ -34947,16 +35277,21 @@ var TokenManager = class {
34947
35277
  const usedToken = await this.getAccessToken();
34948
35278
  let res = await call(usedToken);
34949
35279
  if (res.status === 401) {
34950
- if (this.accessToken === usedToken)
34951
- await this.refreshNow();
34952
- res = await call(this.accessToken);
35280
+ if (this.tokens?.accessToken === usedToken) {
35281
+ try {
35282
+ await this.refreshNow();
35283
+ } catch (err) {
35284
+ await this.reBootstrap(err);
35285
+ }
35286
+ }
35287
+ res = await call(this.tokens?.accessToken ?? usedToken);
34953
35288
  }
34954
35289
  return res;
34955
35290
  }
34956
35291
  };
34957
35292
 
34958
35293
  // src/client.ts
34959
- import { dirname, join as join4 } from "path";
35294
+ import { dirname as dirname2, join as join5 } from "path";
34960
35295
  import { fileURLToPath } from "url";
34961
35296
 
34962
35297
  // node_modules/@fetchproxy/protocol/dist/frames.js
@@ -36133,7 +36468,7 @@ var DEFAULT_BIND_TIMEOUT_MS = 5e3;
36133
36468
  async function electRole(opts) {
36134
36469
  const server = createServer();
36135
36470
  const bindTimeoutMs = opts.bindTimeoutMs ?? DEFAULT_BIND_TIMEOUT_MS;
36136
- return new Promise((resolve2, reject) => {
36471
+ return new Promise((resolve3, reject) => {
36137
36472
  let timer;
36138
36473
  const clearBindTimer = () => {
36139
36474
  if (timer) {
@@ -36149,7 +36484,7 @@ async function electRole(opts) {
36149
36484
  server.close();
36150
36485
  } catch {
36151
36486
  }
36152
- resolve2({ role: "peer" });
36487
+ resolve3({ role: "peer" });
36153
36488
  } else {
36154
36489
  reject(e);
36155
36490
  }
@@ -36157,7 +36492,7 @@ async function electRole(opts) {
36157
36492
  const onListening = () => {
36158
36493
  clearBindTimer();
36159
36494
  server.removeListener("error", onError);
36160
- resolve2({ role: "host", server });
36495
+ resolve3({ role: "host", server });
36161
36496
  };
36162
36497
  server.once("error", onError);
36163
36498
  server.once("listening", onListening);
@@ -36319,16 +36654,16 @@ async function awaitSessionReady(ready, opts) {
36319
36654
 
36320
36655
  // node_modules/@fetchproxy/server/dist/extension-trust.js
36321
36656
  import { readFile as readFile2, writeFile as writeFile2, rename, unlink, mkdir as mkdir2, chmod as chmod2 } from "node:fs/promises";
36322
- import { join as join3 } from "node:path";
36657
+ import { join as join4 } from "node:path";
36323
36658
 
36324
36659
  // node_modules/@fetchproxy/server/dist/identity.js
36325
36660
  import { readFile, writeFile, mkdir, chmod } from "node:fs/promises";
36326
- import { join as join2 } from "node:path";
36327
- import { homedir as homedir2 } from "node:os";
36661
+ import { join as join3 } from "node:path";
36662
+ import { homedir as homedir3 } from "node:os";
36328
36663
  var SAFE_PLAIN = /^[A-Za-z0-9._-]+$/;
36329
36664
  var SAFE_SCOPED = /^@[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/;
36330
36665
  function defaultIdentityDir() {
36331
- return join2(homedir2(), ".fetchproxy", "identity");
36666
+ return join3(homedir3(), ".fetchproxy", "identity");
36332
36667
  }
36333
36668
  function safeIdentityFileBase(serverName) {
36334
36669
  if (!serverName || serverName === ".." || serverName.includes("..") || !SAFE_PLAIN.test(serverName) && !SAFE_SCOPED.test(serverName)) {
@@ -36338,7 +36673,7 @@ function safeIdentityFileBase(serverName) {
36338
36673
  }
36339
36674
  async function loadOrCreateIdentity(serverName, dir = defaultIdentityDir()) {
36340
36675
  const safeFile = safeIdentityFileBase(serverName);
36341
- const path = join2(dir, `${safeFile}.json`);
36676
+ const path = join3(dir, `${safeFile}.json`);
36342
36677
  await mkdir(dir, { recursive: true, mode: 448 });
36343
36678
  try {
36344
36679
  const raw = await readFile(path, "utf8");
@@ -36410,13 +36745,13 @@ function decideExtensionTrust(args) {
36410
36745
  };
36411
36746
  }
36412
36747
  function extensionTrustPath(serverName, dir = defaultIdentityDir()) {
36413
- return join3(dir, `${safeIdentityFileBase(serverName)}.extension-trust.json`);
36748
+ return join4(dir, `${safeIdentityFileBase(serverName)}.extension-trust.json`);
36414
36749
  }
36415
36750
  function extensionTrustPathHint(serverName) {
36416
36751
  try {
36417
36752
  return extensionTrustPath(serverName);
36418
36753
  } catch {
36419
- return join3(defaultIdentityDir(), "<server-name>.extension-trust.json");
36754
+ return join4(defaultIdentityDir(), "<server-name>.extension-trust.json");
36420
36755
  }
36421
36756
  }
36422
36757
  function isPin(x) {
@@ -36503,8 +36838,8 @@ async function startHost(opts) {
36503
36838
  let rejectOwnSession;
36504
36839
  let ownSessionReady;
36505
36840
  function resetSessionPromise() {
36506
- ownSessionReady = new Promise((resolve2, reject) => {
36507
- resolveOwnSession = resolve2;
36841
+ ownSessionReady = new Promise((resolve3, reject) => {
36842
+ resolveOwnSession = resolve3;
36508
36843
  rejectOwnSession = reject;
36509
36844
  });
36510
36845
  ownSessionReady.catch(() => {
@@ -36725,7 +37060,7 @@ async function startHost(opts) {
36725
37060
  });
36726
37061
  });
36727
37062
  return {
36728
- close: () => new Promise((resolve2) => {
37063
+ close: () => new Promise((resolve3) => {
36729
37064
  for (const client2 of wss.clients) {
36730
37065
  try {
36731
37066
  client2.terminate();
@@ -36733,7 +37068,7 @@ async function startHost(opts) {
36733
37068
  }
36734
37069
  }
36735
37070
  wss.close(() => {
36736
- opts.httpServer.close(() => resolve2());
37071
+ opts.httpServer.close(() => resolve3());
36737
37072
  });
36738
37073
  }),
36739
37074
  sendOwnInner: async (inner) => {
@@ -36763,8 +37098,8 @@ async function startHost(opts) {
36763
37098
  var enc3 = new TextEncoder();
36764
37099
  async function startPeer(opts) {
36765
37100
  const ws = new import_websocket.default(`ws://${opts.host}:${opts.port}`);
36766
- await new Promise((resolve2, reject) => {
36767
- ws.once("open", () => resolve2());
37101
+ await new Promise((resolve3, reject) => {
37102
+ ws.once("open", () => resolve3());
36768
37103
  ws.once("error", reject);
36769
37104
  });
36770
37105
  const hello = await buildServerHello({
@@ -36794,8 +37129,8 @@ async function startPeer(opts) {
36794
37129
  let pendingPairCode = null;
36795
37130
  let resolveFirstReady;
36796
37131
  let rejectFirstReady;
36797
- const sessionPromise = new Promise((resolve2, reject) => {
36798
- resolveFirstReady = resolve2;
37132
+ const sessionPromise = new Promise((resolve3, reject) => {
37133
+ resolveFirstReady = resolve3;
36799
37134
  rejectFirstReady = reject;
36800
37135
  });
36801
37136
  let extensionHello = null;
@@ -36996,6 +37331,17 @@ function classifyBridgeError(err) {
36996
37331
  }
36997
37332
 
36998
37333
  // node_modules/@fetchproxy/server/dist/ws-server.js
37334
+ function envWsPort() {
37335
+ const raw = process.env.FETCHPROXY_WS_PORT;
37336
+ if (raw === void 0 || raw.trim() === "")
37337
+ return void 0;
37338
+ if (!/^\d+$/.test(raw.trim()))
37339
+ return void 0;
37340
+ const port = Number(raw.trim());
37341
+ if (!Number.isInteger(port) || port < 1 || port > 65535)
37342
+ return void 0;
37343
+ return port;
37344
+ }
36999
37345
  var FetchproxyProtocolError = class extends Error {
37000
37346
  constructor(message) {
37001
37347
  super(message);
@@ -37243,7 +37589,7 @@ var FetchproxyServer = class {
37243
37589
  }
37244
37590
  }
37245
37591
  this.opts = {
37246
- port: opts.port ?? 37149,
37592
+ port: opts.port ?? envWsPort() ?? 37149,
37247
37593
  host: opts.host ?? "127.0.0.1",
37248
37594
  serverName: opts.serverName,
37249
37595
  version: opts.version,
@@ -37673,8 +38019,8 @@ var FetchproxyServer = class {
37673
38019
  async _fetchOnceWithTimeout(init) {
37674
38020
  const id = this.nextRequestId++;
37675
38021
  const inner = { type: "request", id, op: "fetch", init };
37676
- const pending = new Promise((resolve2) => {
37677
- this.pending.set(id, resolve2);
38022
+ const pending = new Promise((resolve3) => {
38023
+ this.pending.set(id, resolve3);
37678
38024
  });
37679
38025
  await this.sendInnerFrame(inner);
37680
38026
  const timeoutMs = this.opts.fetchTimeoutMs;
@@ -37685,12 +38031,12 @@ var FetchproxyServer = class {
37685
38031
  try {
37686
38032
  return await Promise.race([
37687
38033
  pending,
37688
- new Promise((resolve2) => {
38034
+ new Promise((resolve3) => {
37689
38035
  timer = setTimeout(() => {
37690
38036
  this.pending.delete(id);
37691
38037
  const elapsedMs = Date.now() - start;
37692
38038
  const error51 = `fetchproxy: ${init.url} did not respond within ${timeoutMs}ms`;
37693
- resolve2({
38039
+ resolve3({
37694
38040
  ok: false,
37695
38041
  error: error51,
37696
38042
  kind: "timeout",
@@ -38052,8 +38398,8 @@ var FetchproxyServer = class {
38052
38398
  const tabUrl = `https://${host}/`;
38053
38399
  inner = { type: "request", id, op: "read_cookies", init: { tabUrl } };
38054
38400
  }
38055
- const pending = new Promise((resolve2) => {
38056
- this.pendingReadCookies.set(id, resolve2);
38401
+ const pending = new Promise((resolve3) => {
38402
+ this.pendingReadCookies.set(id, resolve3);
38057
38403
  });
38058
38404
  await this.sendInnerFrame(inner);
38059
38405
  const result = await this._withVerbTimeout(pending, this.pendingReadCookies, id, `https://${host}`);
@@ -38111,8 +38457,8 @@ var FetchproxyServer = class {
38111
38457
  ...cookiePath !== void 0 ? { path: cookiePath } : {}
38112
38458
  }
38113
38459
  };
38114
- const pending = new Promise((resolve2, reject) => {
38115
- this.pendingWriteCookies.set(id, { resolve: resolve2, reject });
38460
+ const pending = new Promise((resolve3, reject) => {
38461
+ this.pendingWriteCookies.set(id, { resolve: resolve3, reject });
38116
38462
  });
38117
38463
  await this.sendInnerFrame(inner);
38118
38464
  return this._withVerbTimeout(pending, this.pendingWriteCookies, id, `https://${host}`);
@@ -38174,8 +38520,8 @@ var FetchproxyServer = class {
38174
38520
  ...opts.pointers ? { pointers: { ...opts.pointers } } : {}
38175
38521
  }
38176
38522
  };
38177
- const pending = new Promise((resolve2, reject) => {
38178
- this.pendingStorage.set(id, { resolve: resolve2, reject });
38523
+ const pending = new Promise((resolve3, reject) => {
38524
+ this.pendingStorage.set(id, { resolve: resolve3, reject });
38179
38525
  });
38180
38526
  await this.sendInnerFrame(inner);
38181
38527
  return this._withVerbTimeout(pending, this.pendingStorage, id, `https://${host}`);
@@ -38279,8 +38625,8 @@ var FetchproxyServer = class {
38279
38625
  ...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {}
38280
38626
  }
38281
38627
  };
38282
- const pending = new Promise((resolve2, reject) => {
38283
- this.pendingCapture.set(id, { resolve: resolve2, reject });
38628
+ const pending = new Promise((resolve3, reject) => {
38629
+ this.pendingCapture.set(id, { resolve: resolve3, reject });
38284
38630
  });
38285
38631
  await this.sendInnerFrame(inner);
38286
38632
  return this._withVerbTimeout(pending, this.pendingCapture, id, `https://${opts.host}${opts.path ?? "/*"}`);
@@ -38364,8 +38710,8 @@ var FetchproxyServer = class {
38364
38710
  ...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {}
38365
38711
  }
38366
38712
  };
38367
- const pending = new Promise((resolve2, reject) => {
38368
- this.pendingRedirect.set(id, { resolve: resolve2, reject });
38713
+ const pending = new Promise((resolve3, reject) => {
38714
+ this.pendingRedirect.set(id, { resolve: resolve3, reject });
38369
38715
  });
38370
38716
  await this.sendInnerFrame(inner);
38371
38717
  return this._withVerbTimeout(pending, this.pendingRedirect, id, `https://${opts.host}${opts.path ?? "/*"}`);
@@ -38446,8 +38792,8 @@ var FetchproxyServer = class {
38446
38792
  ...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {}
38447
38793
  }
38448
38794
  };
38449
- const pending = new Promise((resolve2, reject) => {
38450
- this.pendingDownload.set(id, { resolve: resolve2, reject });
38795
+ const pending = new Promise((resolve3, reject) => {
38796
+ this.pendingDownload.set(id, { resolve: resolve3, reject });
38451
38797
  });
38452
38798
  await this.sendInnerFrame(inner);
38453
38799
  return this._withVerbTimeout(pending, this.pendingDownload, id, opts.url);
@@ -38494,8 +38840,8 @@ var FetchproxyServer = class {
38494
38840
  keys: [...opts.keys]
38495
38841
  }
38496
38842
  };
38497
- const pending = new Promise((resolve2, reject) => {
38498
- this.pendingIdb.set(id, { resolve: resolve2, reject });
38843
+ const pending = new Promise((resolve3, reject) => {
38844
+ this.pendingIdb.set(id, { resolve: resolve3, reject });
38499
38845
  });
38500
38846
  await this.sendInnerFrame(inner);
38501
38847
  return this._withVerbTimeout(pending, this.pendingIdb, id, origin);
@@ -38534,8 +38880,8 @@ var FetchproxyServer = class {
38534
38880
  op: "read_dom",
38535
38881
  init: { origin, names: [...opts.names] }
38536
38882
  };
38537
- const pending = new Promise((resolve2, reject) => {
38538
- this.pendingStorage.set(id, { resolve: resolve2, reject });
38883
+ const pending = new Promise((resolve3, reject) => {
38884
+ this.pendingStorage.set(id, { resolve: resolve3, reject });
38539
38885
  });
38540
38886
  await this.sendInnerFrame(inner);
38541
38887
  return this._withVerbTimeout(pending, this.pendingStorage, id, origin);
@@ -38580,8 +38926,8 @@ var FetchproxyServer = class {
38580
38926
  ...opts.tabUrl !== void 0 ? { tabUrl: opts.tabUrl } : {}
38581
38927
  }
38582
38928
  };
38583
- const pending = new Promise((resolve2, reject) => {
38584
- this.pendingGraphql.set(id, { resolve: resolve2, reject });
38929
+ const pending = new Promise((resolve3, reject) => {
38930
+ this.pendingGraphql.set(id, { resolve: resolve3, reject });
38585
38931
  });
38586
38932
  await this.sendInnerFrame(inner);
38587
38933
  return this._withVerbTimeout(pending, this.pendingGraphql, id, opts.name);
@@ -39074,7 +39420,7 @@ async function loginWithPassword(username, password) {
39074
39420
  // package.json
39075
39421
  var package_default = {
39076
39422
  name: "ofw-mcp",
39077
- version: "2.10.2",
39423
+ version: "2.12.0",
39078
39424
  license: "MIT",
39079
39425
  mcpName: "io.github.chrischall/ofw-mcp",
39080
39426
  description: "OurFamilyWizard MCP server for Claude \u2014 developed and maintained by AI (Claude Code)",
@@ -39095,18 +39441,20 @@ var package_default = {
39095
39441
  ".claude-plugin",
39096
39442
  "skills",
39097
39443
  ".mcp.json",
39098
- "server.json"
39444
+ "server.json",
39445
+ "mint.yaml"
39099
39446
  ],
39100
39447
  scripts: {
39101
39448
  build: "tsc && npm run bundle",
39102
39449
  bundle: `esbuild src/index.ts --bundle --platform=node --format=esm --external:dotenv --banner:js='import { createRequire as __createRequire } from "module"; const require = __createRequire(import.meta.url);' --outfile=dist/bundle.js`,
39103
39450
  dev: "node --env-file=.env dist/index.js",
39104
- test: "vitest run",
39105
- "test:coverage": "vitest run --coverage",
39106
- "test:watch": "vitest"
39451
+ test: "npm run typecheck && vitest run",
39452
+ "test:coverage": "npm run typecheck && vitest run --coverage",
39453
+ "test:watch": "vitest",
39454
+ typecheck: "tsc -p tsconfig.json --noEmit"
39107
39455
  },
39108
39456
  dependencies: {
39109
- "@chrischall/mcp-utils": "^0.14.0",
39457
+ "@chrischall/mcp-utils": "^0.18.0",
39110
39458
  "@fetchproxy/bootstrap": "^2.0.0",
39111
39459
  "@modelcontextprotocol/sdk": "^1.29.0",
39112
39460
  dotenv: "^17.4.2",
@@ -39126,66 +39474,118 @@ function fetchproxyDisabled() {
39126
39474
  return parseBoolEnv("OFW_DISABLE_FETCHPROXY");
39127
39475
  }
39128
39476
  async function resolveAuth() {
39477
+ const pattern = {};
39129
39478
  const username = readEnvVar("OFW_USERNAME");
39130
39479
  const password = readEnvVar("OFW_PASSWORD");
39131
39480
  if (username && password) {
39132
- const { token, expiresAt } = await loginWithPassword(username, password);
39133
- return { token, expiresAt, source: "env" };
39481
+ pattern.sessionScrape = async () => {
39482
+ const { token, expiresAt: expiresAt2 } = await loginWithPassword(username, password);
39483
+ return { credential: token, source: "env", expiresAt: expiresAt2 };
39484
+ };
39134
39485
  }
39135
39486
  if (!fetchproxyDisabled()) {
39136
- try {
39137
- const session = await bootstrap({
39138
- serverName: package_default.name,
39139
- version: package_default.version,
39140
- // OFW serves both ofw.ourfamilywizard.com and www.ourfamilywizard.com;
39141
- // the API + auth token live on the apex. The extension matches on
39142
- // suffix, so listing the apex covers both.
39143
- domains: ["ourfamilywizard.com"],
39144
- declare: {
39145
- cookies: [],
39146
- // The web app stores the Bearer token in localStorage["auth"] and
39147
- // its expiry (ISO string) in localStorage["tokenExpiry"]. Mirroring
39148
- // both means our 401-replay logic can be slightly smarter, and the
39149
- // expiry surfaces correctly in diagnostics.
39150
- localStorage: ["auth", "tokenExpiry"],
39151
- sessionStorage: [],
39152
- captureHeaders: []
39487
+ pattern.fetchproxy = async () => {
39488
+ try {
39489
+ const session = await bootstrap({
39490
+ serverName: package_default.name,
39491
+ version: package_default.version,
39492
+ // OFW serves both ofw.ourfamilywizard.com and www.ourfamilywizard.com;
39493
+ // the API + auth token live on the apex. The extension matches on
39494
+ // suffix, so listing the apex covers both.
39495
+ domains: ["ourfamilywizard.com"],
39496
+ declare: {
39497
+ cookies: [],
39498
+ // The web app stores the Bearer token in localStorage["auth"] and
39499
+ // its expiry (ISO string) in localStorage["tokenExpiry"]. Mirroring
39500
+ // both means our 401-replay logic can be slightly smarter, and the
39501
+ // expiry surfaces correctly in diagnostics.
39502
+ localStorage: ["auth", "tokenExpiry"],
39503
+ sessionStorage: [],
39504
+ captureHeaders: []
39505
+ }
39506
+ });
39507
+ const token = session.localStorage["auth"];
39508
+ const expiryRaw = session.localStorage["tokenExpiry"];
39509
+ if (!token) {
39510
+ throw new Error(
39511
+ 'localStorage["auth"] missing on ourfamilywizard.com. Sign into OFW in your browser (with the fetchproxy extension installed) and retry.'
39512
+ );
39153
39513
  }
39154
- });
39155
- const token = session.localStorage["auth"];
39156
- const expiryRaw = session.localStorage["tokenExpiry"];
39157
- if (!token) {
39158
- throw new Error(
39159
- 'localStorage["auth"] missing on ourfamilywizard.com. Sign into OFW in your browser (with the fetchproxy extension installed) and retry.'
39160
- );
39161
- }
39162
- return {
39163
- token,
39164
- expiresAt: expiryRaw ? new Date(expiryRaw) : void 0,
39165
- source: "fetchproxy"
39166
- };
39167
- } catch (e) {
39168
- if (classifyBridgeError(e) === "bridge_down") {
39169
- const downErr = e;
39514
+ return {
39515
+ credential: token,
39516
+ source: "fetchproxy",
39517
+ ...expiryRaw ? { expiresAt: new Date(expiryRaw) } : {}
39518
+ };
39519
+ } catch (e) {
39520
+ if (classifyBridgeError(e) === "bridge_down") {
39521
+ const downErr = e;
39522
+ throw new Error(
39523
+ `OFW auth: fetchproxy bridge is down (extension service worker unreachable after retry). ${downErr.hint}`
39524
+ );
39525
+ }
39526
+ const msg = e instanceof Error ? e.message : String(e);
39170
39527
  throw new Error(
39171
- `OFW auth: fetchproxy bridge is down (extension service worker unreachable after retry). ${downErr.hint}`
39528
+ `OFW auth: no OFW_USERNAME/OFW_PASSWORD set, and fetchproxy fallback failed: ${msg}`
39172
39529
  );
39173
39530
  }
39174
- const msg = e instanceof Error ? e.message : String(e);
39175
- throw new Error(
39176
- `OFW auth: no OFW_USERNAME/OFW_PASSWORD set, and fetchproxy fallback failed: ${msg}`
39177
- );
39178
- }
39531
+ };
39179
39532
  }
39180
- throw new Error(
39181
- "OFW auth: set OFW_USERNAME + OFW_PASSWORD, or install the fetchproxy extension and sign into ourfamilywizard.com (unset OFW_DISABLE_FETCHPROXY if it is set)."
39533
+ if (!pattern.sessionScrape && !pattern.fetchproxy) {
39534
+ throw new Error(
39535
+ "OFW auth: set OFW_USERNAME + OFW_PASSWORD, or install the fetchproxy extension and sign into ourfamilywizard.com (unset OFW_DISABLE_FETCHPROXY if it is set)."
39536
+ );
39537
+ }
39538
+ const { credential, source, expiresAt } = await resolveAuthPattern(pattern);
39539
+ return {
39540
+ token: credential,
39541
+ // The pattern's `source` is a free-form string; ours is a two-value union,
39542
+ // and both resolvers above set it to a member of that union.
39543
+ source,
39544
+ ...expiresAt ? { expiresAt } : {}
39545
+ };
39546
+ }
39547
+
39548
+ // src/session-cache.ts
39549
+ function sessionCachePath(env = process.env) {
39550
+ return resolveStateFile({
39551
+ env,
39552
+ envVar: "OFW_SESSION_FILE",
39553
+ subdir: ".ofw-mcp",
39554
+ fileName: "session.json"
39555
+ });
39556
+ }
39557
+ function isTokens(raw) {
39558
+ if (raw === null || typeof raw !== "object") return false;
39559
+ const t = raw;
39560
+ return typeof t.accessToken === "string" && t.accessToken !== "" && typeof t.expiresAt === "number" && (t.refreshToken === void 0 || typeof t.refreshToken === "string");
39561
+ }
39562
+ function createSessionCache(opts = {}) {
39563
+ const env = opts.env ?? process.env;
39564
+ if (opts.injectedResolver === true) return null;
39565
+ if (!parseBoolEnv("OFW_SESSION_CACHE", { env, default: true })) return null;
39566
+ const username = readEnvVar("OFW_USERNAME", { env });
39567
+ const password = readEnvVar("OFW_PASSWORD", { env });
39568
+ if (username === void 0 || password === void 0) return null;
39569
+ return createFileStatePersistence({
39570
+ filePath: sessionCachePath(env),
39571
+ // Joined on a NUL, written as an escape rather than a literal byte: a
39572
+ // password may contain spaces, so a space-joined pair could collide with
39573
+ // a different pair by shifting the boundary between the two halves.
39574
+ boundTo: [username.trim().toLowerCase(), password].join("\0"),
39575
+ validate: (raw) => isTokens(raw) ? raw : null
39576
+ });
39577
+ }
39578
+ function reportCacheWriteFailure(err) {
39579
+ const detail = err instanceof Error ? err.message : String(err);
39580
+ console.error(
39581
+ `[ofw-mcp] could not cache the session token (${detail}); continuing without the cache \u2014 every restart will re-authenticate until this is fixed.`
39182
39582
  );
39183
39583
  }
39184
39584
 
39185
39585
  // src/client.ts
39186
39586
  try {
39187
- const dir = dirname(fileURLToPath(import.meta.url));
39188
- await loadDotenvSafely({ path: join4(dir, "..", ".env") });
39587
+ const dir = dirname2(fileURLToPath(import.meta.url));
39588
+ await loadDotenvSafely({ path: join5(dir, "..", ".env") });
39189
39589
  } catch {
39190
39590
  }
39191
39591
  function parseContentDispositionFilename(cd) {
@@ -39216,9 +39616,9 @@ var OFWClient = class {
39216
39616
  // Bearer-token lifecycle is delegated to the shared, race-safe TokenManager
39217
39617
  // (proactive refresh inside the skew window, single-flight refresh so a burst
39218
39618
  // of concurrent callers coalesces onto ONE `resolveAuth()`, and a 401-replay
39219
- // guarded against double-refresh). It is created lazily, seeded with an
39220
- // already-expired placeholder token so the first request drives the refresh
39221
- // callback i.e. the original "log in on first request" behavior.
39619
+ // guarded against double-refresh). It is created lazily, and mints on first
39620
+ // use through the function form of `initial` see `mint` below for why that
39621
+ // form rather than a seeded placeholder.
39222
39622
  tokenManager;
39223
39623
  // Optional injected auth resolver. When set, the refresh callback uses it
39224
39624
  // instead of the module-level global `resolveAuth` (env-var → fetchproxy
@@ -39232,22 +39632,28 @@ var OFWClient = class {
39232
39632
  }
39233
39633
  getTokenManager() {
39234
39634
  if (!this.tokenManager) {
39635
+ const mint = async () => {
39636
+ const { token, expiresAt } = await (this.authResolver ?? resolveAuth)();
39637
+ return {
39638
+ accessToken: token,
39639
+ refreshToken: OFW_REFRESH_SENTINEL,
39640
+ expiresAt: (expiresAt ?? new Date(Date.now() + OFW_TOKEN_TTL_MS)).getTime()
39641
+ };
39642
+ };
39235
39643
  this.tokenManager = new TokenManager({
39236
- initial: { accessToken: "", refreshToken: OFW_REFRESH_SENTINEL, expiresAt: 0 },
39644
+ initial: mint,
39645
+ persistence: createSessionCache({ injectedResolver: this.authResolver !== void 0 }) ?? void 0,
39646
+ onPersistError: reportCacheWriteFailure,
39647
+ // A failed renewal IS a failed login here, so the library's
39648
+ // re-mint-on-revoked recovery would just repeat the call that failed.
39649
+ isRefreshRevoked: () => false,
39237
39650
  skewMs: OFW_TOKEN_EXPIRY_SKEW_MS,
39238
39651
  // Map OFW's mint/refresh onto the refresh callback. `resolveAuth()`
39239
39652
  // returns a token and a best-effort expiry; when the fetchproxy path
39240
39653
  // can't supply one we fall back to the same 6h estimate the password
39241
39654
  // path uses (the 401-replay covers a wrong guess). We re-arm the
39242
39655
  // sentinel so the manager can refresh again later.
39243
- refresh: async () => {
39244
- const { token, expiresAt } = await (this.authResolver ?? resolveAuth)();
39245
- return {
39246
- accessToken: token,
39247
- refreshToken: OFW_REFRESH_SENTINEL,
39248
- expiresAt: (expiresAt ?? new Date(Date.now() + OFW_TOKEN_TTL_MS)).getTime()
39249
- };
39250
- }
39656
+ refresh: mint
39251
39657
  });
39252
39658
  }
39253
39659
  return this.tokenManager;
@@ -40071,25 +40477,25 @@ async function syncAll(client2, opts, store) {
40071
40477
 
40072
40478
  // src/config.ts
40073
40479
  import { createHash } from "node:crypto";
40074
- import { homedir as homedir3 } from "node:os";
40075
- import { join as join5 } from "node:path";
40480
+ import { homedir as homedir4 } from "node:os";
40481
+ import { join as join6 } from "node:path";
40076
40482
  function readCacheIdentity() {
40077
40483
  return readEnvVar("OFW_CACHE_IDENTITY") ?? readEnvVar("OFW_USERNAME") ?? "_default";
40078
40484
  }
40079
40485
  function getCacheDir() {
40080
40486
  const override = process.env.OFW_CACHE_DIR;
40081
40487
  if (override && override.trim().length > 0) return override.trim();
40082
- return join5(homedir3(), ".cache", "ofw-mcp");
40488
+ return join6(homedir4(), ".cache", "ofw-mcp");
40083
40489
  }
40084
40490
  function getCacheDbPath() {
40085
40491
  const identity = readCacheIdentity();
40086
40492
  const hash2 = createHash("sha256").update(identity).digest("hex").slice(0, 16);
40087
- return join5(getCacheDir(), `${hash2}.db`);
40493
+ return join6(getCacheDir(), `${hash2}.db`);
40088
40494
  }
40089
40495
  function getAttachmentsDir() {
40090
40496
  const override = process.env.OFW_ATTACHMENTS_DIR;
40091
40497
  if (override && override.trim().length > 0) return override.trim();
40092
- return join5(homedir3(), "Downloads", "ofw-mcp");
40498
+ return join6(homedir4(), "Downloads", "ofw-mcp");
40093
40499
  }
40094
40500
  function getWriteMode() {
40095
40501
  const raw = process.env.OFW_WRITE_MODE;
@@ -41459,8 +41865,8 @@ async function extractAttachment(bytes, mimeType, fileName, opts = {}) {
41459
41865
  }
41460
41866
 
41461
41867
  // src/tools/attachments.ts
41462
- import { readFileSync, statSync, mkdirSync, writeFileSync } from "node:fs";
41463
- import { basename, dirname as dirname2, extname } from "node:path";
41868
+ import { readFileSync as readFileSync2, statSync, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "node:fs";
41869
+ import { basename, dirname as dirname3, extname } from "node:path";
41464
41870
  var MIME_BY_EXT = {
41465
41871
  ".pdf": "application/pdf",
41466
41872
  ".png": "image/png",
@@ -41534,14 +41940,14 @@ var NodeAttachmentIO = class {
41534
41940
  }
41535
41941
  readDownloaded(path) {
41536
41942
  try {
41537
- return readFileSync(path);
41943
+ return readFileSync2(path);
41538
41944
  } catch {
41539
41945
  return null;
41540
41946
  }
41541
41947
  }
41542
41948
  writeDownload(dest, bytes) {
41543
- mkdirSync(dirname2(dest), { recursive: true });
41544
- writeFileSync(dest, bytes);
41949
+ mkdirSync2(dirname3(dest), { recursive: true });
41950
+ writeFileSync2(dest, bytes);
41545
41951
  }
41546
41952
  };
41547
41953
 
@@ -41605,7 +42011,54 @@ async function buildInlineDelivery(input) {
41605
42011
  }
41606
42012
 
41607
42013
  // src/tools/messages.ts
41608
- import { basename as basename2, join as join6 } from "node:path";
42014
+ import { basename as basename2, join as join7 } from "node:path";
42015
+
42016
+ // src/tools/pagination.ts
42017
+ function pageState(input) {
42018
+ const hasMore = input.page * input.size < input.total;
42019
+ return { hasMore, nextPage: hasMore ? input.page + 1 : null };
42020
+ }
42021
+ function asRecord(value) {
42022
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
42023
+ }
42024
+ function recordArray(root) {
42025
+ if (Array.isArray(root.data)) return root.data;
42026
+ for (const value of Object.values(root)) {
42027
+ if (Array.isArray(value)) return value;
42028
+ }
42029
+ return null;
42030
+ }
42031
+ function readUpstreamPaging(payload) {
42032
+ const root = asRecord(payload);
42033
+ const rows = root === null ? null : recordArray(root);
42034
+ const meta3 = root === null ? null : asRecord(root.metadata);
42035
+ const total = meta3 !== null && typeof meta3.totalElements === "number" && Number.isFinite(meta3.totalElements) ? meta3.totalElements : null;
42036
+ const last = meta3 !== null && typeof meta3.last === "boolean" ? meta3.last : null;
42037
+ return { returned: rows?.length ?? 0, total, last };
42038
+ }
42039
+ function offsetState(input) {
42040
+ const last = input.last ?? null;
42041
+ const consumed = input.start - input.base + input.returned;
42042
+ const hasMore = last !== null ? !last : input.total !== null ? consumed < input.total : input.returned >= input.max;
42043
+ return { hasMore, nextStart: hasMore ? input.start + input.max : null };
42044
+ }
42045
+ function withPaginationFirst(input) {
42046
+ const body = asRecord(input.payload);
42047
+ if (body === null) return null;
42048
+ const scope = input.total !== null ? ` of ${input.total}` : "";
42049
+ const head = {
42050
+ hasMore: input.state.hasMore,
42051
+ nextStart: input.state.nextStart,
42052
+ start: input.start,
42053
+ max: input.max,
42054
+ returned: input.returned,
42055
+ ...input.total !== null ? { total: input.total } : {},
42056
+ paginationNote: input.state.hasMore ? `PARTIAL: this response holds ${input.returned} record(s) starting at ${input.start}${scope}. ${input.hint} Do not state a total or an absence from this response alone.` : `This response reaches the end of the list${scope === "" ? "" : ` (${input.total} record(s) in total)`}.`
42057
+ };
42058
+ return { ...head, ...body, ...head };
42059
+ }
42060
+
42061
+ // src/tools/messages.ts
41609
42062
  var DateSchema = external_exports.looseObject({ dateTime: external_exports.string() });
41610
42063
  var SentDetailSchema = external_exports.looseObject({
41611
42064
  subject: external_exports.string().optional(),
@@ -41741,7 +42194,7 @@ function registerMessageTools(server, client2, cacheProvider, attachmentIO) {
41741
42194
  return jsonResponse({ folders: data, freshness });
41742
42195
  });
41743
42196
  server.registerTool("ofw_list_messages", {
41744
- description: 'List messages from the local OurFamilyWizard cache. Supports filtering by folder, date range, and a substring query on subject+body. Pagination is offset-based but if you know what you want (a date range, a topic), prefer the filters over walking pages \u2014 the cache may have 1000+ messages. Returns an explicit `complete` boolean describing the RESULT SET: true means "this is every message on OurFamilyWizard matching these filters as of freshness.asOf" \u2014 check it before asserting a count. An empty result from a cache that is not verified-fresh is REFUSED (result:"UNVERIFIED_EMPTY") rather than reported as an absence; pass autoRefresh:true to sync and answer instead.',
42197
+ description: 'List messages from the local OurFamilyWizard cache. Supports filtering by folder, date range, and a substring query on subject+body. Pagination is offset-based (1-based `page`) but if you know what you want (a date range, a topic), prefer the filters over walking pages \u2014 the cache may have 1000+ messages. Results are newest-first by default; `sort:"oldest"` starts at the old end of a range instead of paging to it. Returns an explicit `complete` boolean describing the RESULT SET: true means "this is every message on OurFamilyWizard matching these filters as of freshness.asOf" \u2014 check it before asserting a count. An empty result from a cache that is not verified-fresh is REFUSED (result:"UNVERIFIED_EMPTY") rather than reported as an absence; pass autoRefresh:true to sync and answer instead.',
41745
42198
  annotations: { readOnlyHint: false },
41746
42199
  inputSchema: {
41747
42200
  folderId: external_exports.string().describe('Folder name: "inbox", "sent", or "both" (default "both")').optional(),
@@ -41750,11 +42203,13 @@ function registerMessageTools(server, client2, cacheProvider, attachmentIO) {
41750
42203
  since: external_exports.string().describe("ISO date or datetime \u2014 only messages with sent_at >= since (inclusive)").optional(),
41751
42204
  until: external_exports.string().describe("ISO date or datetime \u2014 only messages with sent_at < until (exclusive)").optional(),
41752
42205
  q: external_exports.string().describe("Substring match on subject AND body (case-insensitive). Use to find messages on a specific topic.").optional(),
42206
+ sort: external_exports.enum(["newest", "oldest"]).describe('Result order: "newest" (default, newest first) or "oldest" (oldest first). This decides which end a truncated page keeps \u2014 with "newest" page 1 of a wide date range holds its most RECENT slice, with "oldest" its earliest. Use "oldest" to start at the old end of a range instead of paging to it.').optional(),
41753
42207
  autoRefresh: external_exports.boolean().describe(AUTO_REFRESH_DESC).optional()
41754
42208
  }
41755
42209
  }, async (args) => {
41756
42210
  const page = args.page ?? 1;
41757
42211
  const size = args.size ?? 50;
42212
+ const sort = args.sort ?? "newest";
41758
42213
  const folderArg = args.folderId ?? "both";
41759
42214
  let folder;
41760
42215
  if (folderArg === "inbox") folder = "inbox";
@@ -41780,7 +42235,7 @@ function registerMessageTools(server, client2, cacheProvider, attachmentIO) {
41780
42235
  isEmpty: (v) => v.total === 0,
41781
42236
  read: async () => {
41782
42237
  const total2 = await cache.countMessages(filter);
41783
- const messages2 = (await cache.listMessages({ ...filter, page, size })).map((m) => withReadState(m));
42238
+ const messages2 = (await cache.listMessages({ ...filter, page, size, sort })).map((m) => withReadState(m));
41784
42239
  const freshness2 = await buildFreshness(cache, { source: "cache", folders });
41785
42240
  return { messages: messages2, total: total2, freshness: freshness2 };
41786
42241
  }
@@ -41797,7 +42252,19 @@ function registerMessageTools(server, client2, cacheProvider, attachmentIO) {
41797
42252
  const { messages, total, freshness } = value;
41798
42253
  const fullSlice = page === 1 && messages.length === total;
41799
42254
  const complete = fullSlice && freshness.staleness === "fresh" && freshness.historyComplete;
41800
- const payload = { messages, total, page, size, complete, freshness };
42255
+ const { hasMore, nextPage } = pageState({ page, size, total });
42256
+ const payload = {
42257
+ complete,
42258
+ hasMore,
42259
+ nextPage,
42260
+ // The honest record count, as a scalar and ahead of the array — a
42261
+ // consumer never has to reach `messages` to learn how many came back.
42262
+ returned: messages.length,
42263
+ total,
42264
+ page,
42265
+ size,
42266
+ sort
42267
+ };
41801
42268
  if (!complete) {
41802
42269
  payload.completeNote = [
41803
42270
  !fullSlice ? `this page holds ${messages.length} of ${total} matching cached messages` : null,
@@ -41808,11 +42275,13 @@ function registerMessageTools(server, client2, cacheProvider, attachmentIO) {
41808
42275
  if (total === 0) {
41809
42276
  payload.note = 'No messages match these filters, and the cache IS verified-fresh for these folders \u2014 so this is a real "nothing matched", not a stale-cache artefact. If you expected results, relax the filters.';
41810
42277
  } else if (page * size < total) {
41811
- payload.note = `Showing ${(page - 1) * size + 1}\u2013${(page - 1) * size + messages.length} of ${total}. Increase 'page' to see more, or narrow with since/until/q.`;
42278
+ payload.note = `Showing ${(page - 1) * size + 1}\u2013${(page - 1) * size + messages.length} of ${total}, ${sort} first. Increase 'page' to see more, narrow with since/until/q, or set sort:"${sort === "newest" ? "oldest" : "newest"}" to start from the other end.`;
41812
42279
  }
41813
42280
  if (refreshed) {
41814
42281
  payload.autoRefreshed = true;
41815
42282
  }
42283
+ payload.freshness = freshness;
42284
+ payload.messages = messages;
41816
42285
  return jsonResponse(payload);
41817
42286
  });
41818
42287
  server.registerTool("ofw_get_message", {
@@ -42259,7 +42728,16 @@ ${JSON.stringify(
42259
42728
  const { drafts, total, freshness, serverConfirmed } = value;
42260
42729
  const fullSlice = page === 1 && drafts.length === total;
42261
42730
  const complete = serverConfirmed && fullSlice;
42262
- const payload = { drafts, total, page, size, complete, freshness };
42731
+ const { hasMore, nextPage } = pageState({ page, size, total });
42732
+ const payload = {
42733
+ complete,
42734
+ hasMore,
42735
+ nextPage,
42736
+ returned: drafts.length,
42737
+ total,
42738
+ page,
42739
+ size
42740
+ };
42263
42741
  if (!complete) {
42264
42742
  payload.completeNote = [
42265
42743
  !fullSlice ? `this page holds ${drafts.length} of ${total} cached drafts` : null,
@@ -42278,6 +42756,8 @@ ${JSON.stringify(
42278
42756
  if (verifyNote !== null) {
42279
42757
  payload.verifyNote = verifyNote;
42280
42758
  }
42759
+ payload.freshness = freshness;
42760
+ payload.drafts = drafts;
42281
42761
  return jsonResponse(payload);
42282
42762
  });
42283
42763
  if (allowDrafts) server.registerTool("ofw_save_draft", {
@@ -42509,7 +42989,16 @@ ${text}` : text);
42509
42989
  unread.push({ id: msg.id, subject: msg.subject, sentAt: msg.sentAt, unreadBy });
42510
42990
  }
42511
42991
  }
42512
- const payload = { unread, scanned: sent.length, total, complete, freshness };
42992
+ const { hasMore, nextPage } = pageState({ page, size, total });
42993
+ const payload = {
42994
+ complete,
42995
+ hasMore,
42996
+ nextPage,
42997
+ total,
42998
+ scanned: sent.length,
42999
+ page,
43000
+ size
43001
+ };
42513
43002
  if (!complete) {
42514
43003
  payload.completeNote = `This verdict covers the ${sent.length} of ${total} cached sent messages on this page${freshness.staleness === "fresh" ? "" : `, from a cache that is "${freshness.staleness}"`}. It is not a statement about every message you have sent.`;
42515
43004
  }
@@ -42519,6 +43008,8 @@ ${text}` : text);
42519
43008
  if (refreshed) {
42520
43009
  payload.autoRefreshed = true;
42521
43010
  }
43011
+ payload.freshness = freshness;
43012
+ payload.unread = unread;
42522
43013
  return jsonResponse(payload);
42523
43014
  });
42524
43015
  if (allowDrafts) server.registerTool("ofw_upload_attachment", {
@@ -42615,9 +43106,9 @@ ${text}` : text);
42615
43106
  if (args.saveTo) {
42616
43107
  const isDirArg = args.saveTo.endsWith("/") || args.saveTo.endsWith("\\");
42617
43108
  const abs = expandPath2(args.saveTo);
42618
- dest = isDirArg ? join6(abs, `${fileId}-${safeName}`) : abs;
43109
+ dest = isDirArg ? join7(abs, `${fileId}-${safeName}`) : abs;
42619
43110
  } else {
42620
- dest = join6(getAttachmentsDir(), `${fileId}-${safeName}`);
43111
+ dest = join7(getAttachmentsDir(), `${fileId}-${safeName}`);
42621
43112
  }
42622
43113
  const extractOnDisk = args.extract === true;
42623
43114
  if (!args.force && cached2.downloadedPath === dest) {
@@ -43037,17 +43528,27 @@ function registerExpenseTools(server, client2) {
43037
43528
  return jsonResponse(data);
43038
43529
  });
43039
43530
  server.registerTool("ofw_list_expenses", {
43040
- description: "List OurFamilyWizard expenses with pagination",
43531
+ description: "List OurFamilyWizard expenses. Offset-paged via start/max. The response leads with its paging state \u2014 `hasMore` and `nextStart` (null when the list is exhausted) \u2014 BEFORE the records, so a truncated or partially-read response still says whether more remain. Never state an expense total or an absence from one page.",
43041
43532
  annotations: { readOnlyHint: true },
43042
43533
  inputSchema: {
43043
- start: external_exports.number().int().min(0).describe("Start offset (default 0)").optional(),
43534
+ start: external_exports.number().int().min(0).describe("Start offset, 0-based (default 0). To continue a listing, pass the `nextStart` from the previous response.").optional(),
43044
43535
  max: external_exports.number().int().min(1).describe("Max results (default 20)").optional()
43045
43536
  }
43046
43537
  }, async (args) => {
43047
43538
  const start = args.start ?? 0;
43048
43539
  const max = args.max ?? 20;
43049
43540
  const data = await client2.request("GET", `/pub/v2/expense/expenses?start=${start}&max=${max}`);
43050
- return jsonResponse(data);
43541
+ const { returned, total, last } = readUpstreamPaging(data);
43542
+ const wrapped = withPaginationFirst({
43543
+ state: offsetState({ start, max, returned, total, last, base: 0 }),
43544
+ start,
43545
+ max,
43546
+ returned,
43547
+ total,
43548
+ hint: `Re-call ofw_list_expenses with start:${start + max}.`,
43549
+ payload: data
43550
+ });
43551
+ return jsonResponse(wrapped ?? data);
43051
43552
  });
43052
43553
  if (allowWrites) server.registerTool("ofw_create_expense", {
43053
43554
  description: "Log a new expense in OurFamilyWizard",
@@ -43066,17 +43567,27 @@ function registerExpenseTools(server, client2) {
43066
43567
  function registerJournalTools(server, client2) {
43067
43568
  const allowWrites = getWriteMode() === "all";
43068
43569
  server.registerTool("ofw_list_journal_entries", {
43069
- description: "List OurFamilyWizard journal entries",
43570
+ description: "List OurFamilyWizard journal entries. Offset-paged via start/max (1-based). The response leads with its paging state \u2014 `hasMore` and `nextStart` (null when the list is exhausted) \u2014 BEFORE the records, so a truncated or partially-read response still says whether more remain. Never state an entry count or an absence from one page.",
43070
43571
  annotations: { readOnlyHint: true },
43071
43572
  inputSchema: {
43072
- start: external_exports.number().int().min(1).describe("Start offset (default 1)").optional(),
43573
+ start: external_exports.number().int().min(1).describe("Start offset, 1-based (default 1). To continue a listing, pass the `nextStart` from the previous response.").optional(),
43073
43574
  max: external_exports.number().int().min(1).describe("Max results (default 10)").optional()
43074
43575
  }
43075
43576
  }, async (args) => {
43076
43577
  const start = args.start ?? 1;
43077
43578
  const max = args.max ?? 10;
43078
43579
  const data = await client2.request("GET", `/pub/v1/journals?start=${start}&max=${max}`);
43079
- return jsonResponse(data);
43580
+ const { returned, total, last } = readUpstreamPaging(data);
43581
+ const wrapped = withPaginationFirst({
43582
+ state: offsetState({ start, max, returned, total, last, base: 1 }),
43583
+ start,
43584
+ max,
43585
+ returned,
43586
+ total,
43587
+ hint: `Re-call ofw_list_journal_entries with start:${start + max}.`,
43588
+ payload: data
43589
+ });
43590
+ return jsonResponse(wrapped ?? data);
43080
43591
  });
43081
43592
  if (allowWrites) server.registerTool("ofw_create_journal_entry", {
43082
43593
  description: "Create a new journal entry in OurFamilyWizard",
@@ -43093,8 +43604,8 @@ function registerJournalTools(server, client2) {
43093
43604
 
43094
43605
  // src/cache/node.ts
43095
43606
  import { DatabaseSync } from "node:sqlite";
43096
- import { mkdirSync as mkdirSync2, chmodSync, existsSync } from "node:fs";
43097
- import { dirname as dirname3 } from "node:path";
43607
+ import { mkdirSync as mkdirSync3, chmodSync as chmodSync2, existsSync as existsSync2 } from "node:fs";
43608
+ import { dirname as dirname4 } from "node:path";
43098
43609
 
43099
43610
  // src/cache/store.ts
43100
43611
  function rowFromDb(r) {
@@ -43332,9 +43843,10 @@ var OFWCacheCore = class {
43332
43843
  listMessages(opts) {
43333
43844
  const { where, params } = buildMessageFilter(opts);
43334
43845
  const offset = (opts.page - 1) * opts.size;
43846
+ const dir = opts.sort === "oldest" ? "ASC" : "DESC";
43335
43847
  const rows = this.db.all(
43336
43848
  `SELECT * FROM messages ${where}
43337
- ORDER BY sent_at DESC, id DESC
43849
+ ORDER BY sent_at ${dir}, id ${dir}
43338
43850
  LIMIT ? OFFSET ?`,
43339
43851
  [...params, opts.size, offset]
43340
43852
  );
@@ -43677,10 +44189,10 @@ var NodeSqlDriver = class {
43677
44189
  }
43678
44190
  };
43679
44191
  function enforceCachePermissions(dbPath) {
43680
- chmodSync(dirname3(dbPath), 448);
43681
- chmodSync(dbPath, 384);
44192
+ chmodSync2(dirname4(dbPath), 448);
44193
+ chmodSync2(dbPath, 384);
43682
44194
  for (const sibling of [`${dbPath}-wal`, `${dbPath}-shm`]) {
43683
- if (existsSync(sibling)) chmodSync(sibling, 384);
44195
+ if (existsSync2(sibling)) chmodSync2(sibling, 384);
43684
44196
  }
43685
44197
  }
43686
44198
  var OFWCache = class _OFWCache extends LocalCacheStore {
@@ -43691,7 +44203,7 @@ var OFWCache = class _OFWCache extends LocalCacheStore {
43691
44203
  db;
43692
44204
  static open(path) {
43693
44205
  const memory = path === ":memory:";
43694
- if (!memory) mkdirSync2(dirname3(path), { recursive: true });
44206
+ if (!memory) mkdirSync3(dirname4(path), { recursive: true });
43695
44207
  const db = new DatabaseSync(path);
43696
44208
  if (!memory) enforceCachePermissions(path);
43697
44209
  db.exec("PRAGMA journal_mode = WAL");
@@ -43721,7 +44233,7 @@ var nodeCacheProvider = () => nodeCache ??= OFWCache.open(getCacheDbPath());
43721
44233
  var nodeAttachmentIO = new NodeAttachmentIO();
43722
44234
  await runMcp({
43723
44235
  name: "ofw",
43724
- version: "2.10.2",
44236
+ version: "2.12.0",
43725
44237
  // x-release-please-version
43726
44238
  deps: client,
43727
44239
  tools: [