untappd-mcp 1.3.0 → 1.4.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
@@ -101,7 +101,7 @@ var require_code = __commonJS({
101
101
  }
102
102
  exports._ = _;
103
103
  var plus = new _Code("+");
104
- function str(strs, ...args) {
104
+ function str2(strs, ...args) {
105
105
  const expr = [safeStringify(strs[0])];
106
106
  let i = 0;
107
107
  while (i < args.length) {
@@ -112,7 +112,7 @@ var require_code = __commonJS({
112
112
  optimize(expr);
113
113
  return new _Code(expr);
114
114
  }
115
- exports.str = str;
115
+ exports.str = str2;
116
116
  function addCodeArg(code, arg) {
117
117
  if (arg instanceof _Code)
118
118
  code.push(...arg._items);
@@ -155,7 +155,7 @@ var require_code = __commonJS({
155
155
  return;
156
156
  }
157
157
  function strConcat(c1, c2) {
158
- return c2.emptyStr() ? c1 : c1.emptyStr() ? c2 : str`${c1}${c2}`;
158
+ return c2.emptyStr() ? c1 : c1.emptyStr() ? c2 : str2`${c1}${c2}`;
159
159
  }
160
160
  exports.strConcat = strConcat;
161
161
  function interpolate(x) {
@@ -1117,22 +1117,22 @@ var require_util = __commonJS({
1117
1117
  return (0, codegen_1._)`${topSchemaRef}${schemaPath}${(0, codegen_1.getProperty)(keyword)}`;
1118
1118
  }
1119
1119
  exports.schemaRefOrVal = schemaRefOrVal;
1120
- function unescapeFragment(str) {
1121
- return unescapeJsonPointer(decodeURIComponent(str));
1120
+ function unescapeFragment(str2) {
1121
+ return unescapeJsonPointer(decodeURIComponent(str2));
1122
1122
  }
1123
1123
  exports.unescapeFragment = unescapeFragment;
1124
- function escapeFragment(str) {
1125
- return encodeURIComponent(escapeJsonPointer(str));
1124
+ function escapeFragment(str2) {
1125
+ return encodeURIComponent(escapeJsonPointer(str2));
1126
1126
  }
1127
1127
  exports.escapeFragment = escapeFragment;
1128
- function escapeJsonPointer(str) {
1129
- if (typeof str == "number")
1130
- return `${str}`;
1131
- return str.replace(/~/g, "~0").replace(/\//g, "~1");
1128
+ function escapeJsonPointer(str2) {
1129
+ if (typeof str2 == "number")
1130
+ return `${str2}`;
1131
+ return str2.replace(/~/g, "~0").replace(/\//g, "~1");
1132
1132
  }
1133
1133
  exports.escapeJsonPointer = escapeJsonPointer;
1134
- function unescapeJsonPointer(str) {
1135
- return str.replace(/~1/g, "/").replace(/~0/g, "~");
1134
+ function unescapeJsonPointer(str2) {
1135
+ return str2.replace(/~1/g, "/").replace(/~0/g, "~");
1136
1136
  }
1137
1137
  exports.unescapeJsonPointer = unescapeJsonPointer;
1138
1138
  function eachItem(xs, f) {
@@ -2157,8 +2157,8 @@ var require_json_schema_traverse = __commonJS({
2157
2157
  post(schema, jsonPtr, rootSchema, parentJsonPtr, parentKeyword, parentSchema, keyIndex);
2158
2158
  }
2159
2159
  }
2160
- function escapeJsonPtr(str) {
2161
- return str.replace(/~/g, "~0").replace(/\//g, "~1");
2160
+ function escapeJsonPtr(str2) {
2161
+ return str2.replace(/~/g, "~0").replace(/\//g, "~1");
2162
2162
  }
2163
2163
  }
2164
2164
  });
@@ -3222,10 +3222,10 @@ var require_utils = __commonJS({
3222
3222
  return { host, isIPV6: false };
3223
3223
  }
3224
3224
  }
3225
- function findToken(str, token) {
3225
+ function findToken(str2, token) {
3226
3226
  let ind = 0;
3227
- for (let i = 0; i < str.length; i++) {
3228
- if (str[i] === token) ind++;
3227
+ for (let i = 0; i < str2.length; i++) {
3228
+ if (str2[i] === token) ind++;
3229
3229
  }
3230
3230
  return ind;
3231
3231
  }
@@ -3962,7 +3962,7 @@ var require_core = __commonJS({
3962
3962
  var util_1 = require_util();
3963
3963
  var $dataRefSchema = require_data();
3964
3964
  var uri_1 = require_uri();
3965
- var defaultRegExp = (str, flags) => new RegExp(str, flags);
3965
+ var defaultRegExp = (str2, flags) => new RegExp(str2, flags);
3966
3966
  defaultRegExp.code = "new RegExp";
3967
3967
  var META_IGNORE_OPTIONS = ["removeAdditional", "useDefaults", "coerceTypes"];
3968
3968
  var EXT_SCOPE_NAMES = /* @__PURE__ */ new Set([
@@ -4757,16 +4757,16 @@ var require_ucs2length = __commonJS({
4757
4757
  "node_modules/ajv/dist/runtime/ucs2length.js"(exports) {
4758
4758
  "use strict";
4759
4759
  Object.defineProperty(exports, "__esModule", { value: true });
4760
- function ucs2length(str) {
4761
- const len = str.length;
4760
+ function ucs2length(str2) {
4761
+ const len = str2.length;
4762
4762
  let length = 0;
4763
4763
  let pos = 0;
4764
4764
  let value;
4765
4765
  while (pos < len) {
4766
4766
  length++;
4767
- value = str.charCodeAt(pos++);
4767
+ value = str2.charCodeAt(pos++);
4768
4768
  if (value >= 55296 && value <= 56319 && pos < len) {
4769
- value = str.charCodeAt(pos);
4769
+ value = str2.charCodeAt(pos);
4770
4770
  if ((value & 64512) === 56320)
4771
4771
  pos++;
4772
4772
  }
@@ -6649,8 +6649,8 @@ var require_formats = __commonJS({
6649
6649
  }
6650
6650
  var DATE = /^(\d\d\d\d)-(\d\d)-(\d\d)$/;
6651
6651
  var DAYS = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
6652
- function date5(str) {
6653
- const matches = DATE.exec(str);
6652
+ function date5(str2) {
6653
+ const matches = DATE.exec(str2);
6654
6654
  if (!matches)
6655
6655
  return false;
6656
6656
  const year = +matches[1];
@@ -6669,8 +6669,8 @@ var require_formats = __commonJS({
6669
6669
  }
6670
6670
  var TIME = /^(\d\d):(\d\d):(\d\d(?:\.\d+)?)(z|([+-])(\d\d)(?::?(\d\d))?)?$/i;
6671
6671
  function getTime(strictTimeZone) {
6672
- return function time3(str) {
6673
- const matches = TIME.exec(str);
6672
+ return function time3(str2) {
6673
+ const matches = TIME.exec(str2);
6674
6674
  if (!matches)
6675
6675
  return false;
6676
6676
  const hr = +matches[1];
@@ -6716,8 +6716,8 @@ var require_formats = __commonJS({
6716
6716
  var DATE_TIME_SEPARATOR = /t|\s/i;
6717
6717
  function getDateTime(strictTimeZone) {
6718
6718
  const time3 = getTime(strictTimeZone);
6719
- return function date_time(str) {
6720
- const dateTime = str.split(DATE_TIME_SEPARATOR);
6719
+ return function date_time(str2) {
6720
+ const dateTime = str2.split(DATE_TIME_SEPARATOR);
6721
6721
  return dateTime.length === 2 && date5(dateTime[0]) && time3(dateTime[1]);
6722
6722
  };
6723
6723
  }
@@ -6742,13 +6742,13 @@ var require_formats = __commonJS({
6742
6742
  }
6743
6743
  var NOT_URI_FRAGMENT = /\/|:/;
6744
6744
  var URI = /^(?:[a-z][a-z0-9+\-.]*:)(?:\/?\/(?:(?:[a-z0-9\-._~!$&'()*+,;=:]|%[0-9a-f]{2})*@)?(?:\[(?:(?:(?:(?:[0-9a-f]{1,4}:){6}|::(?:[0-9a-f]{1,4}:){5}|(?:[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){4}|(?:(?:[0-9a-f]{1,4}:){0,1}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){3}|(?:(?:[0-9a-f]{1,4}:){0,2}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){2}|(?:(?:[0-9a-f]{1,4}:){0,3}[0-9a-f]{1,4})?::[0-9a-f]{1,4}:|(?:(?:[0-9a-f]{1,4}:){0,4}[0-9a-f]{1,4})?::)(?:[0-9a-f]{1,4}:[0-9a-f]{1,4}|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?))|(?:(?:[0-9a-f]{1,4}:){0,5}[0-9a-f]{1,4})?::[0-9a-f]{1,4}|(?:(?:[0-9a-f]{1,4}:){0,6}[0-9a-f]{1,4})?::)|[Vv][0-9a-f]+\.[a-z0-9\-._~!$&'()*+,;=:]+)\]|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)|(?:[a-z0-9\-._~!$&'()*+,;=]|%[0-9a-f]{2})*)(?::\d*)?(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*|\/(?:(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*)?|(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*)(?:\?(?:[a-z0-9\-._~!$&'()*+,;=:@/?]|%[0-9a-f]{2})*)?(?:#(?:[a-z0-9\-._~!$&'()*+,;=:@/?]|%[0-9a-f]{2})*)?$/i;
6745
- function uri(str) {
6746
- return NOT_URI_FRAGMENT.test(str) && URI.test(str);
6745
+ function uri(str2) {
6746
+ return NOT_URI_FRAGMENT.test(str2) && URI.test(str2);
6747
6747
  }
6748
6748
  var BYTE = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/gm;
6749
- function byte(str) {
6749
+ function byte(str2) {
6750
6750
  BYTE.lastIndex = 0;
6751
- return BYTE.test(str);
6751
+ return BYTE.test(str2);
6752
6752
  }
6753
6753
  var MIN_INT32 = -(2 ** 31);
6754
6754
  var MAX_INT32 = 2 ** 31 - 1;
@@ -6762,11 +6762,11 @@ var require_formats = __commonJS({
6762
6762
  return true;
6763
6763
  }
6764
6764
  var Z_ANCHOR = /[^\\]\\Z/;
6765
- function regex(str) {
6766
- if (Z_ANCHOR.test(str))
6765
+ function regex(str2) {
6766
+ if (Z_ANCHOR.test(str2))
6767
6767
  return false;
6768
6768
  try {
6769
- new RegExp(str);
6769
+ new RegExp(str2);
6770
6770
  return true;
6771
6771
  } catch (e) {
6772
6772
  return false;
@@ -11306,14 +11306,14 @@ function promiseAllObject(promisesObj) {
11306
11306
  }
11307
11307
  function randomString(length = 10) {
11308
11308
  const chars = "abcdefghijklmnopqrstuvwxyz";
11309
- let str = "";
11309
+ let str2 = "";
11310
11310
  for (let i = 0; i < length; i++) {
11311
- str += chars[Math.floor(Math.random() * chars.length)];
11311
+ str2 += chars[Math.floor(Math.random() * chars.length)];
11312
11312
  }
11313
- return str;
11313
+ return str2;
11314
11314
  }
11315
- function esc(str) {
11316
- return JSON.stringify(str);
11315
+ function esc(str2) {
11316
+ return JSON.stringify(str2);
11317
11317
  }
11318
11318
  function slugify(input) {
11319
11319
  return input.toLowerCase().trim().replace(/[^\w\s-]/g, "").replace(/[\s_-]+/g, "-").replace(/^-+|-+$/g, "");
@@ -11427,8 +11427,8 @@ var primitiveTypes = /* @__PURE__ */ new Set([
11427
11427
  "symbol",
11428
11428
  "undefined"
11429
11429
  ]);
11430
- function escapeRegex(str) {
11431
- return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
11430
+ function escapeRegex(str2) {
11431
+ return str2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
11432
11432
  }
11433
11433
  function clone(inst, def, params) {
11434
11434
  const cl = new inst._zod.constr(def ?? inst._zod.def);
@@ -31067,8 +31067,8 @@ function redactSecrets(text) {
31067
31067
  return text.replace(BEARER_RE, "$1[REDACTED]").replace(BASIC_AUTH_RE, "$1[REDACTED]").replace(SET_COOKIE_RE, "$1$2=[REDACTED]").replace(COOKIE_HEADER_RE, (_m, prefix, pairs) => `${prefix}${pairs.replace(/=[^;,\s]*/g, "=[REDACTED]")}`).replace(API_KEY_RE, "[REDACTED]").replace(QUERY_SECRET_RE, "$1[REDACTED]").replace(AWS_SIGV4_RE, "$1[REDACTED]").replace(JSON_SECRET_DQ_RE, "$1[REDACTED]$2").replace(JSON_SECRET_SQ_RE, "$1[REDACTED]$2").replace(JWT_RE, "[REDACTED]");
31068
31068
  }
31069
31069
  function truncateErrorMessage(text, max = DEFAULT_ERROR_MESSAGE_MAX) {
31070
- const str = text === null || text === void 0 ? "" : String(text);
31071
- const redacted = redactSecrets(str);
31070
+ const str2 = text === null || text === void 0 ? "" : String(text);
31071
+ const redacted = redactSecrets(str2);
31072
31072
  if (redacted.length <= max)
31073
31073
  return redacted;
31074
31074
  return `${redacted.slice(0, max)}\u2026 [truncated]`;
@@ -31184,7 +31184,7 @@ function toolAnnotations(opts = {}) {
31184
31184
  }
31185
31185
 
31186
31186
  // src/version.ts
31187
- var VERSION = "1.3.0";
31187
+ var VERSION = "1.4.0";
31188
31188
 
31189
31189
  // src/client.ts
31190
31190
  import { dirname, join } from "path";
@@ -32333,21 +32333,46 @@ function registerCheckinTools(server, client2) {
32333
32333
  );
32334
32334
  }
32335
32335
 
32336
+ // src/tools/diagnostics.ts
32337
+ function registeredToolNames(server) {
32338
+ const reg = server._registeredTools;
32339
+ return reg ? Object.keys(reg).sort() : [];
32340
+ }
32341
+ function toolsHash(names) {
32342
+ let h = 2166136261;
32343
+ const s = names.join(",");
32344
+ for (let i = 0; i < s.length; i++) {
32345
+ h ^= s.charCodeAt(i);
32346
+ h = Math.imul(h, 16777619);
32347
+ }
32348
+ return (h >>> 0).toString(16).padStart(8, "0");
32349
+ }
32350
+ function toolInventory(server) {
32351
+ const names = registeredToolNames(server);
32352
+ return { tool_count: names.length, tools_hash: toolsHash(names), tools: names };
32353
+ }
32354
+ function logRegisteredTools(server, context) {
32355
+ const names = registeredToolNames(server);
32356
+ console.error(`[untappd-mcp] ${context}: ${names.length} tools registered [${toolsHash(names)}]: ${names.join(", ")}`);
32357
+ }
32358
+
32336
32359
  // src/tools/utilities.ts
32337
32360
  function registerUtilityTools(server, client2) {
32338
32361
  server.registerTool(
32339
32362
  "untappd_healthcheck",
32340
32363
  {
32341
32364
  title: "Untappd healthcheck",
32342
- description: "Verify Untappd connectivity and that credentials are configured and can log in. Performs a lightweight authenticated request (your recent feed) and reports whether it succeeded. Read-only.",
32365
+ description: "Verify Untappd connectivity and that credentials are configured and can log in. Performs a lightweight authenticated request (your recent feed) and reports whether it succeeded, plus the running server version and the exact set of tools this build exposes (count, a stable hash, and their names) so you can confirm which build is live. Read-only.",
32343
32366
  annotations: toolAnnotations({ title: "Untappd healthcheck", readOnly: true, idempotent: true, openWorld: true }),
32344
32367
  inputSchema: {}
32345
32368
  },
32346
32369
  async () => {
32370
+ const build = { server_version: VERSION, ...toolInventory(server) };
32347
32371
  if (!client2.configured) {
32348
32372
  return textResult({
32349
32373
  ok: false,
32350
32374
  configured: false,
32375
+ ...build,
32351
32376
  note: "Untappd credentials are not set. Configure UNTAPPD_USERNAME, UNTAPPD_PASSWORD, UNTAPPD_CLIENT_ID, and UNTAPPD_CLIENT_SECRET."
32352
32377
  });
32353
32378
  }
@@ -32357,14 +32382,647 @@ function registerUtilityTools(server, client2) {
32357
32382
  configured: true,
32358
32383
  account: client2.loginName,
32359
32384
  feed_reachable: feed?.checkins !== void 0,
32385
+ ...build,
32360
32386
  note: "Logged in to Untappd and fetched the friend feed successfully."
32361
32387
  });
32362
32388
  }
32363
32389
  );
32364
32390
  }
32365
32391
 
32392
+ // src/cache/store.ts
32393
+ function asDict2(v) {
32394
+ return v && typeof v === "object" && !Array.isArray(v) ? v : {};
32395
+ }
32396
+ function num(v) {
32397
+ return typeof v === "number" && Number.isFinite(v) ? v : null;
32398
+ }
32399
+ function str(v) {
32400
+ return typeof v === "string" && v.length > 0 ? v : null;
32401
+ }
32402
+ function mapCheckinRow(username, item) {
32403
+ const c = asDict2(item);
32404
+ const checkin_id = num(c.checkin_id);
32405
+ if (checkin_id === null) return null;
32406
+ const beer = asDict2(c.beer);
32407
+ const brewery = asDict2(c.brewery);
32408
+ const venue = asDict2(c.venue);
32409
+ return {
32410
+ checkin_id,
32411
+ username,
32412
+ bid: num(beer.bid),
32413
+ beer_name: str(beer.beer_name),
32414
+ brewery_id: num(brewery.brewery_id),
32415
+ brewery_name: str(brewery.brewery_name),
32416
+ beer_style: str(beer.beer_style),
32417
+ abv: num(beer.beer_abv),
32418
+ rating: num(c.rating_score),
32419
+ comment: str(c.checkin_comment),
32420
+ venue_id: num(venue.venue_id),
32421
+ venue_name: str(venue.venue_name),
32422
+ created_at: normaliseDate(c.created_at)
32423
+ };
32424
+ }
32425
+ function normaliseDate(v) {
32426
+ const s = str(v);
32427
+ if (!s) return null;
32428
+ const t = Date.parse(s);
32429
+ return Number.isNaN(t) ? s : new Date(t).toISOString();
32430
+ }
32431
+ var SCHEMA_STATEMENTS = [
32432
+ `CREATE TABLE IF NOT EXISTS checkins (
32433
+ checkin_id INTEGER PRIMARY KEY,
32434
+ username TEXT NOT NULL,
32435
+ bid INTEGER,
32436
+ beer_name TEXT,
32437
+ brewery_id INTEGER,
32438
+ brewery_name TEXT,
32439
+ beer_style TEXT,
32440
+ abv REAL,
32441
+ rating REAL,
32442
+ comment TEXT,
32443
+ venue_id INTEGER,
32444
+ venue_name TEXT,
32445
+ created_at TEXT
32446
+ )`,
32447
+ `CREATE INDEX IF NOT EXISTS idx_checkins_user_bid ON checkins(username, bid)`,
32448
+ `CREATE INDEX IF NOT EXISTS idx_checkins_user_brewery ON checkins(username, brewery_id)`,
32449
+ `CREATE TABLE IF NOT EXISTS sync_state (
32450
+ username TEXT PRIMARY KEY,
32451
+ oldest_max_id INTEGER,
32452
+ newest_checkin_id INTEGER,
32453
+ catchup_max_id INTEGER,
32454
+ last_synced_at TEXT,
32455
+ backfill_complete INTEGER NOT NULL DEFAULT 0,
32456
+ total_checkins INTEGER
32457
+ )`
32458
+ ];
32459
+ var MIGRATIONS = ["ALTER TABLE sync_state ADD COLUMN catchup_max_id INTEGER"];
32460
+ var CheckinStoreCore = class {
32461
+ constructor(db) {
32462
+ this.db = db;
32463
+ for (const stmt of SCHEMA_STATEMENTS) this.db.execScript(stmt);
32464
+ for (const mig of MIGRATIONS) {
32465
+ try {
32466
+ this.db.execScript(mig);
32467
+ } catch {
32468
+ }
32469
+ }
32470
+ }
32471
+ db;
32472
+ /**
32473
+ * Upsert a batch of rows (dedupe on checkin_id) inside one transaction and
32474
+ * return the NET number of newly-inserted rows (updates to existing rows count
32475
+ * as 0), which is what "rows added" reports.
32476
+ */
32477
+ upsertCheckins(username, rows) {
32478
+ if (rows.length === 0) return 0;
32479
+ const key = username.toLowerCase();
32480
+ const before = this.cachedCount(username);
32481
+ const sql = `
32482
+ INSERT INTO checkins
32483
+ (checkin_id, username, bid, beer_name, brewery_id, brewery_name, beer_style, abv, rating, comment, venue_id, venue_name, created_at)
32484
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
32485
+ ON CONFLICT(checkin_id) DO UPDATE SET
32486
+ username=excluded.username, bid=excluded.bid, beer_name=excluded.beer_name,
32487
+ brewery_id=excluded.brewery_id, brewery_name=excluded.brewery_name, beer_style=excluded.beer_style,
32488
+ abv=excluded.abv, rating=excluded.rating, comment=excluded.comment,
32489
+ venue_id=excluded.venue_id, venue_name=excluded.venue_name, created_at=excluded.created_at`;
32490
+ this.db.transaction(() => {
32491
+ for (const r of rows) {
32492
+ this.db.run(sql, [
32493
+ r.checkin_id,
32494
+ key,
32495
+ r.bid,
32496
+ r.beer_name,
32497
+ r.brewery_id,
32498
+ r.brewery_name,
32499
+ r.beer_style,
32500
+ r.abv,
32501
+ r.rating,
32502
+ r.comment,
32503
+ r.venue_id,
32504
+ r.venue_name,
32505
+ r.created_at
32506
+ ]);
32507
+ }
32508
+ });
32509
+ return this.cachedCount(username) - before;
32510
+ }
32511
+ /** Total cached check-ins for a user (= distinct check-ins, since PK is checkin_id). */
32512
+ cachedCount(username) {
32513
+ const row = this.db.get("SELECT COUNT(*) AS n FROM checkins WHERE username = ?", [username.toLowerCase()]);
32514
+ return Number(row?.n ?? 0);
32515
+ }
32516
+ /** Distinct beers (unique bids) cached for a user. */
32517
+ distinctBeerCount(username) {
32518
+ const row = this.db.get(
32519
+ "SELECT COUNT(DISTINCT bid) AS n FROM checkins WHERE username = ? AND bid IS NOT NULL",
32520
+ [username.toLowerCase()]
32521
+ );
32522
+ return Number(row?.n ?? 0);
32523
+ }
32524
+ /** Highest cached checkin_id for a user (the true newest once contiguous), or null. */
32525
+ newestCachedId(username) {
32526
+ const row = this.db.get("SELECT MAX(checkin_id) AS m FROM checkins WHERE username = ?", [username.toLowerCase()]);
32527
+ return num(row?.m);
32528
+ }
32529
+ getState(username) {
32530
+ const row = this.db.get("SELECT * FROM sync_state WHERE username = ?", [username.toLowerCase()]);
32531
+ if (!row) return void 0;
32532
+ return {
32533
+ username: String(row.username),
32534
+ oldest_max_id: num(row.oldest_max_id),
32535
+ newest_checkin_id: num(row.newest_checkin_id),
32536
+ catchup_max_id: num(row.catchup_max_id),
32537
+ last_synced_at: str(row.last_synced_at),
32538
+ backfill_complete: Number(row.backfill_complete) === 1,
32539
+ total_checkins: num(row.total_checkins)
32540
+ };
32541
+ }
32542
+ /**
32543
+ * Insert-or-merge sync state. Only keys PRESENT in `patch` are written, so an
32544
+ * explicit `null` (e.g. clearing `oldest_max_id` when backfill completes)
32545
+ * overrides the stored value — a `??` merge could not do that.
32546
+ */
32547
+ setState(username, patch) {
32548
+ const key = username.toLowerCase();
32549
+ const cur = this.getState(key);
32550
+ const pick2 = (k, fallback) => k in patch ? patch[k] : cur?.[k] ?? fallback;
32551
+ const next = {
32552
+ username: key,
32553
+ oldest_max_id: pick2("oldest_max_id", null),
32554
+ newest_checkin_id: pick2("newest_checkin_id", null),
32555
+ catchup_max_id: pick2("catchup_max_id", null),
32556
+ last_synced_at: pick2("last_synced_at", null),
32557
+ backfill_complete: pick2("backfill_complete", false),
32558
+ total_checkins: pick2("total_checkins", null)
32559
+ };
32560
+ this.db.run(
32561
+ `INSERT INTO sync_state (username, oldest_max_id, newest_checkin_id, catchup_max_id, last_synced_at, backfill_complete, total_checkins)
32562
+ VALUES (?, ?, ?, ?, ?, ?, ?)
32563
+ ON CONFLICT(username) DO UPDATE SET
32564
+ oldest_max_id=excluded.oldest_max_id, newest_checkin_id=excluded.newest_checkin_id,
32565
+ catchup_max_id=excluded.catchup_max_id,
32566
+ last_synced_at=excluded.last_synced_at, backfill_complete=excluded.backfill_complete,
32567
+ total_checkins=excluded.total_checkins`,
32568
+ [
32569
+ key,
32570
+ next.oldest_max_id,
32571
+ next.newest_checkin_id,
32572
+ next.catchup_max_id,
32573
+ next.last_synced_at,
32574
+ next.backfill_complete ? 1 : 0,
32575
+ next.total_checkins
32576
+ ]
32577
+ );
32578
+ }
32579
+ /** Exact bid match or case-insensitive substring match on beer name. */
32580
+ hasHad(username, opts) {
32581
+ const where = ["username = ?"];
32582
+ const params = [username.toLowerCase()];
32583
+ if (opts.bid !== void 0) {
32584
+ where.push("bid = ?");
32585
+ params.push(opts.bid);
32586
+ }
32587
+ if (opts.beerName !== void 0) {
32588
+ where.push("beer_name LIKE ? COLLATE NOCASE");
32589
+ params.push(`%${escapeLike(opts.beerName)}%`);
32590
+ }
32591
+ const clause = where.join(" AND ");
32592
+ const agg = this.db.get(`SELECT COUNT(*) AS n, MAX(rating) AS best FROM checkins WHERE ${clause}`, params);
32593
+ const matches = this.db.all(
32594
+ `SELECT checkin_id, bid, beer_name, rating, venue_name, created_at
32595
+ FROM checkins WHERE ${clause} ORDER BY checkin_id DESC LIMIT 25`,
32596
+ params
32597
+ );
32598
+ const count2 = Number(agg?.n ?? 0);
32599
+ return {
32600
+ had: count2 > 0,
32601
+ count: count2,
32602
+ last_date: matches[0]?.created_at ?? null,
32603
+ best_rating: num(agg?.best),
32604
+ matches
32605
+ };
32606
+ }
32607
+ query(username, filters) {
32608
+ const where = ["username = ?"];
32609
+ const params = [username.toLowerCase()];
32610
+ if (filters.brewery_id !== void 0) {
32611
+ where.push("brewery_id = ?");
32612
+ params.push(filters.brewery_id);
32613
+ }
32614
+ if (filters.brewery !== void 0) {
32615
+ where.push("brewery_name LIKE ? COLLATE NOCASE");
32616
+ params.push(`%${escapeLike(filters.brewery)}%`);
32617
+ }
32618
+ if (filters.style !== void 0) {
32619
+ where.push("beer_style LIKE ? COLLATE NOCASE");
32620
+ params.push(`%${escapeLike(filters.style)}%`);
32621
+ }
32622
+ if (filters.min_rating !== void 0) {
32623
+ where.push("rating >= ?");
32624
+ params.push(filters.min_rating);
32625
+ }
32626
+ if (filters.venue !== void 0) {
32627
+ where.push("venue_name LIKE ? COLLATE NOCASE");
32628
+ params.push(`%${escapeLike(filters.venue)}%`);
32629
+ }
32630
+ if (filters.venue_id !== void 0) {
32631
+ where.push("venue_id = ?");
32632
+ params.push(filters.venue_id);
32633
+ }
32634
+ if (filters.date_from !== void 0) {
32635
+ where.push("substr(created_at, 1, 10) >= ?");
32636
+ params.push(filters.date_from);
32637
+ }
32638
+ if (filters.date_to !== void 0) {
32639
+ where.push("substr(created_at, 1, 10) <= ?");
32640
+ params.push(filters.date_to);
32641
+ }
32642
+ const order = filters.sort === "oldest" ? "checkin_id ASC" : filters.sort === "highest_rated" ? "rating DESC, checkin_id DESC" : filters.sort === "lowest_rated" ? "rating ASC, checkin_id DESC" : "checkin_id DESC";
32643
+ const limit = Math.min(Math.max(filters.limit ?? 25, 1), 200);
32644
+ return this.db.all(
32645
+ `SELECT * FROM checkins WHERE ${where.join(" AND ")} ORDER BY ${order} LIMIT ?`,
32646
+ [...params, limit]
32647
+ );
32648
+ }
32649
+ };
32650
+ var LocalCacheStore = class {
32651
+ constructor(core) {
32652
+ this.core = core;
32653
+ }
32654
+ core;
32655
+ async upsertCheckins(username, rows) {
32656
+ return this.core.upsertCheckins(username, rows);
32657
+ }
32658
+ async cachedCount(username) {
32659
+ return this.core.cachedCount(username);
32660
+ }
32661
+ async distinctBeerCount(username) {
32662
+ return this.core.distinctBeerCount(username);
32663
+ }
32664
+ async newestCachedId(username) {
32665
+ return this.core.newestCachedId(username);
32666
+ }
32667
+ async getState(username) {
32668
+ return this.core.getState(username);
32669
+ }
32670
+ async setState(username, patch) {
32671
+ this.core.setState(username, patch);
32672
+ }
32673
+ async hasHad(username, opts) {
32674
+ return this.core.hasHad(username, opts);
32675
+ }
32676
+ async query(username, filters) {
32677
+ return this.core.query(username, filters);
32678
+ }
32679
+ };
32680
+ function escapeLike(s) {
32681
+ return s.replace(/[%_]/g, " ");
32682
+ }
32683
+
32684
+ // src/cache/sync.ts
32685
+ var PAGE_LIMIT = 50;
32686
+ async function fetchPage(client2, encodedUser, maxId) {
32687
+ const data = await client2.get(
32688
+ `/user/checkins/${encodedUser}`,
32689
+ { limit: PAGE_LIMIT, max_id: maxId }
32690
+ );
32691
+ const box = data?.checkins ?? {};
32692
+ const items = Array.isArray(box.items) ? box.items : [];
32693
+ const raw = box.pagination?.max_id;
32694
+ const nextMaxId = typeof raw === "number" && raw > 0 ? raw : null;
32695
+ return { items, nextMaxId };
32696
+ }
32697
+ function rowsOf(username, items) {
32698
+ const rows = [];
32699
+ for (const it of items) {
32700
+ const r = mapCheckinRow(username, it);
32701
+ if (r) rows.push(r);
32702
+ }
32703
+ return rows;
32704
+ }
32705
+ async function fetchTotalCheckins(client2, encodedUser) {
32706
+ try {
32707
+ const info = await client2.get(`/user/info/${encodedUser}`);
32708
+ const t = info?.user?.stats?.total_checkins;
32709
+ return typeof t === "number" ? t : null;
32710
+ } catch {
32711
+ return null;
32712
+ }
32713
+ }
32714
+ async function syncCheckins(client2, cache, rawUsername, maxPages = 10) {
32715
+ const encodedUser = encodeURIComponent(rawUsername);
32716
+ const prior = await cache.getState(rawUsername);
32717
+ const priorNewest = prior?.newest_checkin_id ?? null;
32718
+ let pages = 0;
32719
+ let added = 0;
32720
+ let newest = priorNewest;
32721
+ let backfillComplete = prior?.backfill_complete ?? false;
32722
+ let oldestMaxId = prior?.oldest_max_id ?? null;
32723
+ let catchupMaxId = prior?.catchup_max_id ?? null;
32724
+ let catchupInProgress = catchupMaxId !== null;
32725
+ const now = () => (/* @__PURE__ */ new Date()).toISOString();
32726
+ const firstFetch = async (maxId) => {
32727
+ try {
32728
+ return await fetchPage(client2, encodedUser, maxId);
32729
+ } catch (e) {
32730
+ if (e instanceof RateLimitError) throw e;
32731
+ throw createHelpfulError(`Could not fetch check-ins for "${rawUsername}": ${messageOf(e)}`, {
32732
+ hint: "Untappd only returns another user's check-ins if their account is public or they're your friend. If the account is private, add them as a friend (untappd_add_friend) first, or sync only your own account."
32733
+ });
32734
+ }
32735
+ };
32736
+ if (priorNewest !== null) {
32737
+ let maxId = catchupMaxId ?? void 0;
32738
+ let caughtUp = false;
32739
+ for (; pages < maxPages; ) {
32740
+ const page = pages === 0 ? await firstFetch(maxId) : await fetchPage(client2, encodedUser, maxId);
32741
+ pages++;
32742
+ if (page.items.length === 0) {
32743
+ caughtUp = true;
32744
+ break;
32745
+ }
32746
+ const rows = rowsOf(rawUsername, page.items);
32747
+ added += await cache.upsertCheckins(rawUsername, rows);
32748
+ const oldestOnPage = rows[rows.length - 1].checkin_id;
32749
+ if (oldestOnPage <= priorNewest) {
32750
+ caughtUp = true;
32751
+ break;
32752
+ }
32753
+ if (page.nextMaxId === null) {
32754
+ caughtUp = true;
32755
+ backfillComplete = true;
32756
+ oldestMaxId = null;
32757
+ break;
32758
+ }
32759
+ catchupMaxId = page.nextMaxId;
32760
+ await cache.setState(rawUsername, { catchup_max_id: catchupMaxId, last_synced_at: now() });
32761
+ maxId = page.nextMaxId;
32762
+ }
32763
+ if (caughtUp) {
32764
+ newest = await cache.newestCachedId(rawUsername);
32765
+ catchupMaxId = null;
32766
+ catchupInProgress = false;
32767
+ await cache.setState(rawUsername, {
32768
+ newest_checkin_id: newest,
32769
+ catchup_max_id: null,
32770
+ backfill_complete: backfillComplete,
32771
+ oldest_max_id: oldestMaxId,
32772
+ last_synced_at: now()
32773
+ });
32774
+ } else {
32775
+ catchupInProgress = true;
32776
+ }
32777
+ }
32778
+ if (!backfillComplete && !catchupInProgress) {
32779
+ let maxId = oldestMaxId ?? void 0;
32780
+ for (; pages < maxPages; ) {
32781
+ const first = pages === 0;
32782
+ const page = first ? await firstFetch(maxId) : await fetchPage(client2, encodedUser, maxId);
32783
+ pages++;
32784
+ if (page.items.length === 0) {
32785
+ backfillComplete = true;
32786
+ break;
32787
+ }
32788
+ const rows = rowsOf(rawUsername, page.items);
32789
+ added += await cache.upsertCheckins(rawUsername, rows);
32790
+ if (newest === null) newest = rows[0].checkin_id;
32791
+ oldestMaxId = page.nextMaxId;
32792
+ if (page.nextMaxId === null) backfillComplete = true;
32793
+ await cache.setState(rawUsername, {
32794
+ newest_checkin_id: newest,
32795
+ oldest_max_id: oldestMaxId,
32796
+ backfill_complete: backfillComplete,
32797
+ last_synced_at: now()
32798
+ });
32799
+ if (backfillComplete) break;
32800
+ maxId = page.nextMaxId ?? void 0;
32801
+ }
32802
+ }
32803
+ const total = await fetchTotalCheckins(client2, encodedUser);
32804
+ const cached2 = await cache.cachedCount(rawUsername);
32805
+ await cache.setState(rawUsername, {
32806
+ newest_checkin_id: newest,
32807
+ oldest_max_id: oldestMaxId,
32808
+ catchup_max_id: catchupMaxId,
32809
+ backfill_complete: backfillComplete,
32810
+ total_checkins: total,
32811
+ last_synced_at: now()
32812
+ });
32813
+ const backfillPercent = total && total > 0 ? Math.min(100, Math.round(cached2 / total * 100)) : backfillComplete && !catchupInProgress ? 100 : null;
32814
+ const anotherRunNeeded = !backfillComplete || catchupInProgress;
32815
+ const note = catchupInProgress ? "More new check-ins remain than fit in this run \u2014 run untappd_sync_checkins again to finish catching up the newest check-ins." : backfillComplete ? "Full history is cached. Re-run occasionally to pick up new check-ins." : `Backfill incomplete \u2014 run untappd_sync_checkins again to fetch the next ${maxPages} pages of older check-ins.`;
32816
+ return {
32817
+ username: rawUsername,
32818
+ rows_added: added,
32819
+ pages_fetched: pages,
32820
+ cached_checkins: cached2,
32821
+ distinct_beers: await cache.distinctBeerCount(rawUsername),
32822
+ total_checkins: total,
32823
+ backfill_percent: backfillPercent,
32824
+ backfill_complete: backfillComplete,
32825
+ catchup_in_progress: catchupInProgress,
32826
+ another_run_needed: anotherRunNeeded,
32827
+ last_synced_at: now(),
32828
+ note
32829
+ };
32830
+ }
32831
+
32832
+ // src/tools/cache.ts
32833
+ var UsernameArg2 = external_exports.string().min(1).optional().describe("Untappd username. Omit to use your own configured account (UNTAPPD_USERNAME).");
32834
+ function resolveUser2(username, loginName) {
32835
+ const u = username ?? loginName;
32836
+ if (!u) {
32837
+ throw createHelpfulError("No username given and no configured account to fall back to.", {
32838
+ hint: "Pass `username`, or set UNTAPPD_USERNAME so cache tools default to your own account."
32839
+ });
32840
+ }
32841
+ return u;
32842
+ }
32843
+ async function freshness(cache, username) {
32844
+ const state = await cache.getState(username);
32845
+ const cached2 = await cache.cachedCount(username);
32846
+ const backfillComplete = state?.backfill_complete ?? false;
32847
+ const percent = state?.total_checkins && state.total_checkins > 0 ? Math.min(100, Math.round(cached2 / state.total_checkins * 100)) : backfillComplete ? 100 : null;
32848
+ const catchupInProgress = (state?.catchup_max_id ?? null) !== null;
32849
+ let caveat;
32850
+ if (!state || state.last_synced_at === null) {
32851
+ caveat = "This user has never been synced \u2014 the cache is EMPTY. Run untappd_sync_checkins first; results here are not authoritative.";
32852
+ } else if (catchupInProgress) {
32853
+ caveat = "A batch of the newest check-ins is still being caught up (last sync ran out of pages). Recent check-ins may be missing \u2014 run untappd_sync_checkins again to finish.";
32854
+ } else if (!backfillComplete) {
32855
+ caveat = `Backfill is incomplete${percent !== null ? ` (~${percent}% of history cached)` : ""}. A "not found" result may be a false negative for older check-ins \u2014 run untappd_sync_checkins again to keep backfilling.`;
32856
+ }
32857
+ return {
32858
+ last_synced_at: state?.last_synced_at ?? null,
32859
+ backfill_complete: backfillComplete,
32860
+ catchup_in_progress: catchupInProgress,
32861
+ backfill_percent: percent,
32862
+ cached_checkins: cached2,
32863
+ distinct_beers: await cache.distinctBeerCount(username),
32864
+ ...caveat ? { caveat } : {}
32865
+ };
32866
+ }
32867
+ function registerCacheTools(server, client2, cacheProvider) {
32868
+ server.registerTool(
32869
+ "untappd_sync_checkins",
32870
+ {
32871
+ title: "Sync Untappd check-ins into the cache",
32872
+ description: "Fetch a user's check-ins into a persistent cache so history can be queried without paging the API. Incremental (stops once it reaches already-cached check-ins) and resumable: if the backfill is not yet complete it pages backwards up to max_pages per call and persists progress after every page, so run it repeatedly until another_run_needed is false. Syncing another user requires their account be public or a friend. Omit username for your own account.",
32873
+ annotations: toolAnnotations({ title: "Sync Untappd check-ins into the cache", readOnly: false, idempotent: false, openWorld: true }),
32874
+ inputSchema: {
32875
+ username: UsernameArg2,
32876
+ max_pages: external_exports.number().int().min(1).max(50).optional().describe("Total pages (50 check-ins each) to fetch this run, incremental + backfill (default 10). Keep modest to respect the ~100 calls/hour rate limit.")
32877
+ }
32878
+ },
32879
+ async ({ username, max_pages }) => {
32880
+ const user = resolveUser2(username, client2.loginName);
32881
+ const summary = await syncCheckins(client2, cacheProvider(), user, max_pages ?? 10);
32882
+ return textResult(summary);
32883
+ }
32884
+ );
32885
+ server.registerTool(
32886
+ "untappd_cache_has_had",
32887
+ {
32888
+ title: "Check if a user has had a beer (from the cache)",
32889
+ description: 'Answer "has this user ever checked in this beer?" from the cache only \u2014 NO API call. Match by exact bid, or by a case-insensitive substring of the beer name. Returns whether they had it, how many times, the best rating, the last date, and the matching check-ins. Always reports cache freshness so you can caveat incomplete data. Requires bid or beer_name. Run untappd_sync_checkins first.',
32890
+ annotations: toolAnnotations({ title: "Check if a user has had a beer (from the cache)", readOnly: true, idempotent: true, openWorld: false }),
32891
+ inputSchema: {
32892
+ username: UsernameArg2,
32893
+ bid: external_exports.number().int().positive().optional().describe("Exact Untappd beer id to look for"),
32894
+ beer_name: external_exports.string().min(1).optional().describe("Case-insensitive substring match on the beer name")
32895
+ }
32896
+ },
32897
+ async ({ username, bid, beer_name }) => {
32898
+ if (bid === void 0 && beer_name === void 0) {
32899
+ throw createHelpfulError("Provide either bid or beer_name.", { hint: "bid is exact; beer_name is a case-insensitive substring match." });
32900
+ }
32901
+ const user = resolveUser2(username, client2.loginName);
32902
+ const cache = cacheProvider();
32903
+ const result = await cache.hasHad(user, { bid, beerName: beer_name });
32904
+ return textResult({ username: user, query: { bid, beer_name }, ...result, freshness: await freshness(cache, user) });
32905
+ }
32906
+ );
32907
+ server.registerTool(
32908
+ "untappd_cache_has_had_many",
32909
+ {
32910
+ title: "Batch-check many beers against the cache",
32911
+ description: "Cross-check a list of beer ids against a user's cached history in ONE call \u2014 NO API call. Returns had/not-had per bid (with count and last date when had). Ideal for checking a whole venue menu at once. Run untappd_sync_checkins first; the freshness block flags if the cache is incomplete.",
32912
+ annotations: toolAnnotations({ title: "Batch-check many beers against the cache", readOnly: true, idempotent: true, openWorld: false }),
32913
+ inputSchema: {
32914
+ username: UsernameArg2,
32915
+ bids: external_exports.array(external_exports.number().int().positive()).min(1).max(500).describe("Beer ids to check (1\u2013500)")
32916
+ }
32917
+ },
32918
+ async ({ username, bids }) => {
32919
+ const user = resolveUser2(username, client2.loginName);
32920
+ const cache = cacheProvider();
32921
+ const seen = /* @__PURE__ */ new Set();
32922
+ const results = [];
32923
+ let hadCount = 0;
32924
+ for (const bid of bids) {
32925
+ if (seen.has(bid)) continue;
32926
+ seen.add(bid);
32927
+ const r = await cache.hasHad(user, { bid });
32928
+ if (r.had) hadCount++;
32929
+ results.push({ bid, had: r.had, count: r.count, last_date: r.last_date, best_rating: r.best_rating });
32930
+ }
32931
+ return textResult({
32932
+ username: user,
32933
+ checked: seen.size,
32934
+ had: hadCount,
32935
+ not_had: seen.size - hadCount,
32936
+ results,
32937
+ freshness: await freshness(cache, user)
32938
+ });
32939
+ }
32940
+ );
32941
+ server.registerTool(
32942
+ "untappd_cache_query",
32943
+ {
32944
+ title: "Query cached check-ins with filters",
32945
+ description: "Query a user's cached check-ins by brewery, style, minimum rating, venue, and/or date range, with sorting and a limit \u2014 from the cache only, NO API call. Returns the matching check-ins plus cache freshness. Run untappd_sync_checkins first.",
32946
+ annotations: toolAnnotations({ title: "Query cached check-ins with filters", readOnly: true, idempotent: true, openWorld: false }),
32947
+ inputSchema: {
32948
+ username: UsernameArg2,
32949
+ brewery_id: external_exports.number().int().positive().optional().describe("Exact brewery id"),
32950
+ brewery: external_exports.string().min(1).optional().describe("Case-insensitive substring match on brewery name"),
32951
+ style: external_exports.string().min(1).optional().describe('Case-insensitive substring match on beer style (e.g. "IPA")'),
32952
+ min_rating: external_exports.number().min(0).max(5).optional().describe("Only check-ins you rated at least this (0\u20135)"),
32953
+ venue: external_exports.string().min(1).optional().describe("Case-insensitive substring match on venue name"),
32954
+ venue_id: external_exports.number().int().positive().optional().describe("Exact venue id"),
32955
+ date_from: external_exports.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("Only check-ins on/after this date (YYYY-MM-DD, UTC)"),
32956
+ date_to: external_exports.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("Only check-ins on/before this date (YYYY-MM-DD, UTC)"),
32957
+ sort: external_exports.enum(["recent", "oldest", "highest_rated", "lowest_rated"]).optional().describe("Sort order (default recent first)"),
32958
+ limit: external_exports.number().int().min(1).max(200).optional().describe("Max rows (1\u2013200, default 25)")
32959
+ }
32960
+ },
32961
+ async ({ username, ...filters }) => {
32962
+ const user = resolveUser2(username, client2.loginName);
32963
+ const cache = cacheProvider();
32964
+ const rows = await cache.query(user, filters);
32965
+ return textResult({ username: user, count: rows.length, filters, results: rows, freshness: await freshness(cache, user) });
32966
+ }
32967
+ );
32968
+ }
32969
+
32970
+ // src/cache/db.ts
32971
+ import { DatabaseSync } from "node:sqlite";
32972
+ import { mkdirSync } from "node:fs";
32973
+ import { dirname as dirname2, join as join2 } from "node:path";
32974
+ import { homedir } from "node:os";
32975
+ var NodeSqlDriver = class {
32976
+ constructor(db) {
32977
+ this.db = db;
32978
+ }
32979
+ db;
32980
+ execScript(sql) {
32981
+ this.db.exec(sql);
32982
+ }
32983
+ run(sql, params) {
32984
+ this.db.prepare(sql).run(...params);
32985
+ }
32986
+ get(sql, params) {
32987
+ return this.db.prepare(sql).get(...params);
32988
+ }
32989
+ all(sql, params) {
32990
+ return this.db.prepare(sql).all(...params);
32991
+ }
32992
+ transaction(fn) {
32993
+ this.db.exec("BEGIN");
32994
+ try {
32995
+ fn();
32996
+ this.db.exec("COMMIT");
32997
+ } catch (e) {
32998
+ this.db.exec("ROLLBACK");
32999
+ throw e;
33000
+ }
33001
+ }
33002
+ };
33003
+ var CheckinCache = class _CheckinCache extends LocalCacheStore {
33004
+ constructor(db) {
33005
+ super(new CheckinStoreCore(new NodeSqlDriver(db)));
33006
+ this.db = db;
33007
+ }
33008
+ db;
33009
+ /** Open (creating parent dirs) a file-backed cache, or `:memory:` for tests. */
33010
+ static open(path) {
33011
+ if (path !== ":memory:") mkdirSync(dirname2(path), { recursive: true });
33012
+ return new _CheckinCache(new DatabaseSync(path));
33013
+ }
33014
+ close() {
33015
+ this.db.close();
33016
+ }
33017
+ };
33018
+ function defaultCachePath() {
33019
+ return readEnvVar("UNTAPPD_CACHE_DB") ?? join2(homedir(), ".untappd-mcp", "checkins.db");
33020
+ }
33021
+
32366
33022
  // src/index.ts
32367
33023
  var client = new UntappdClient();
33024
+ var nodeCache;
33025
+ var nodeCacheProvider = () => nodeCache ??= CheckinCache.open(defaultCachePath());
32368
33026
  await runMcp({
32369
33027
  name: "untappd-mcp",
32370
33028
  version: VERSION,
@@ -32380,6 +33038,12 @@ await runMcp({
32380
33038
  (s) => registerFriendActionTools(s, client),
32381
33039
  (s) => registerWishlistTools(s, client),
32382
33040
  (s) => registerCheckinTools(s, client),
32383
- (s) => registerUtilityTools(s, client)
33041
+ (s) => registerUtilityTools(s, client),
33042
+ // The stdio server backs the cache with a local `node:sqlite` file; the
33043
+ // remote Cloudflare connector (src/worker.ts) backs the same cache tools
33044
+ // with a per-user Durable Object instead.
33045
+ (s) => registerCacheTools(s, client, nodeCacheProvider),
33046
+ // Keep last: logs the full registered toolset (count + names) at startup.
33047
+ (s) => logRegisteredTools(s, "stdio")
32384
33048
  ]
32385
33049
  });