untappd-mcp 2.1.3 → 2.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +27 -4
- package/dist/bundle.js +721 -134
- package/dist/cache/db.js +41 -4
- package/dist/cache/store.js +19 -0
- package/dist/tools/cache.js +42 -1
- package/dist/tools/checkin.js +119 -60
- package/dist/tools/confirm.js +20 -0
- package/dist/tools/friends.js +19 -15
- package/dist/tools/wishlist.js +32 -25
- package/dist/version.js +1 -1
- package/package.json +2 -2
- package/server.json +2 -2
- package/skills/untappd-mcp/SKILL.md +18 -7
package/dist/bundle.js
CHANGED
|
@@ -23315,6 +23315,33 @@ var storage2 = new AsyncLocalStorage2();
|
|
|
23315
23315
|
function withCallerCapabilities(capabilities, fn) {
|
|
23316
23316
|
return capabilities ? storage2.run(capabilities, fn) : fn();
|
|
23317
23317
|
}
|
|
23318
|
+
function currentCallerCapabilities() {
|
|
23319
|
+
return storage2.getStore();
|
|
23320
|
+
}
|
|
23321
|
+
var ENVELOPE_CAPABILITIES_KEY = "io.modelcontextprotocol/clientCapabilities";
|
|
23322
|
+
var ELICITATION_MODES = ["form", "url"];
|
|
23323
|
+
function isRecord(value) {
|
|
23324
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
23325
|
+
}
|
|
23326
|
+
function callerCapabilities(ctx) {
|
|
23327
|
+
const envelope = isRecord(ctx) && isRecord(ctx.mcpReq) ? ctx.mcpReq.envelope : void 0;
|
|
23328
|
+
if (isRecord(envelope)) {
|
|
23329
|
+
const declared = envelope[ENVELOPE_CAPABILITIES_KEY];
|
|
23330
|
+
if (isRecord(declared))
|
|
23331
|
+
return declared;
|
|
23332
|
+
}
|
|
23333
|
+
return currentCallerCapabilities();
|
|
23334
|
+
}
|
|
23335
|
+
function callerAcceptsFormElicitation(ctx) {
|
|
23336
|
+
const capabilities = callerCapabilities(ctx);
|
|
23337
|
+
if (!capabilities)
|
|
23338
|
+
return void 0;
|
|
23339
|
+
const elicitation = capabilities.elicitation;
|
|
23340
|
+
if (!isRecord(elicitation))
|
|
23341
|
+
return false;
|
|
23342
|
+
const namedModes = ELICITATION_MODES.filter((mode) => mode in elicitation);
|
|
23343
|
+
return namedModes.length === 0 || namedModes.includes("form");
|
|
23344
|
+
}
|
|
23318
23345
|
|
|
23319
23346
|
// node_modules/@modelcontextprotocol/server/dist/chunk-Br0eD_fh.mjs
|
|
23320
23347
|
var __create2 = Object.create;
|
|
@@ -27713,6 +27740,37 @@ var inputRequired = Object.assign(buildInputRequired, {
|
|
|
27713
27740
|
return { method: "roots/list" };
|
|
27714
27741
|
}
|
|
27715
27742
|
});
|
|
27743
|
+
function acceptedContent(responses, key, schema) {
|
|
27744
|
+
const view = inputResponse(responses, key);
|
|
27745
|
+
if (view.kind !== "elicit" || view.action !== "accept" || view.content === void 0) return void 0;
|
|
27746
|
+
if (schema === void 0) return view.content;
|
|
27747
|
+
const outcome = schema["~standard"].validate(view.content);
|
|
27748
|
+
if (outcome instanceof Promise) throw new TypeError("acceptedContent(responses, key, schema) requires a synchronously-validating schema");
|
|
27749
|
+
return outcome.issues === void 0 ? outcome.value : void 0;
|
|
27750
|
+
}
|
|
27751
|
+
function inputResponse(responses, key) {
|
|
27752
|
+
if (responses === void 0 || typeof responses !== "object" || responses === null) return { kind: "missing" };
|
|
27753
|
+
const entry = responses[key];
|
|
27754
|
+
if (entry === null || typeof entry !== "object" || Array.isArray(entry)) return { kind: "missing" };
|
|
27755
|
+
const candidate = entry;
|
|
27756
|
+
if (candidate["action"] === "accept" || candidate["action"] === "decline" || candidate["action"] === "cancel") {
|
|
27757
|
+
const content = candidate["content"];
|
|
27758
|
+
return {
|
|
27759
|
+
kind: "elicit",
|
|
27760
|
+
action: candidate["action"],
|
|
27761
|
+
...content !== null && typeof content === "object" && !Array.isArray(content) && { content }
|
|
27762
|
+
};
|
|
27763
|
+
}
|
|
27764
|
+
if (Array.isArray(candidate["roots"])) return {
|
|
27765
|
+
kind: "roots",
|
|
27766
|
+
roots: candidate["roots"]
|
|
27767
|
+
};
|
|
27768
|
+
if (typeof candidate["role"] === "string" && candidate["content"] !== void 0) return {
|
|
27769
|
+
kind: "sampling",
|
|
27770
|
+
result: candidate
|
|
27771
|
+
};
|
|
27772
|
+
return { kind: "missing" };
|
|
27773
|
+
}
|
|
27716
27774
|
var REQUEST_STATE_ONLY_LEG_PACING_MS = 250;
|
|
27717
27775
|
function inputRequiredRoundsExceededMessage(method, maxRounds) {
|
|
27718
27776
|
return `Multi-round-trip request '${method}' still required input after ${maxRounds} rounds (inputRequired.maxRounds)`;
|
|
@@ -37760,14 +37818,14 @@ function serveStdio(factory, options = {}) {
|
|
|
37760
37818
|
return false;
|
|
37761
37819
|
};
|
|
37762
37820
|
const answerLegacyRejection = (request, reason, requestedVersion) => {
|
|
37763
|
-
const
|
|
37821
|
+
const rejection3 = modernOnlyStrictRejection({
|
|
37764
37822
|
kind: "legacy",
|
|
37765
37823
|
reason,
|
|
37766
37824
|
...requestedVersion !== void 0 && { requestedVersion }
|
|
37767
37825
|
}, SUPPORTED_MODERN_PROTOCOL_VERSIONS);
|
|
37768
|
-
if (
|
|
37769
|
-
reportError(/* @__PURE__ */ new Error(`Rejected 2025-era request on a modern-only stdio connection (${
|
|
37770
|
-
return writeErrorResponse(request.id,
|
|
37826
|
+
if (rejection3 === void 0) return Promise.resolve();
|
|
37827
|
+
reportError(/* @__PURE__ */ new Error(`Rejected 2025-era request on a modern-only stdio connection (${rejection3.cell}): ${rejection3.message}`));
|
|
37828
|
+
return writeErrorResponse(request.id, rejection3.code, rejection3.message, rejection3.data);
|
|
37771
37829
|
};
|
|
37772
37830
|
const onInstanceClosed = (channel) => {
|
|
37773
37831
|
if (closing || channel === discarding) return;
|
|
@@ -57758,6 +57816,11 @@ var MEDIA_QUALIFIER = "(?:primary|secondary|main|default|cover|hero|profile|mast
|
|
|
57758
57816
|
var MEDIA_KEY = new RegExp(`^(?:(?:${MEDIA_QUALIFIER}|${MEDIA_NOUN})[_-]?)?${MEDIA_NOUN}s?(?:[_-]?(?:link|uri|url|src)s?)?$`, "i");
|
|
57759
57817
|
|
|
57760
57818
|
// node_modules/@chrischall/mcp-utils/dist/response/index.js
|
|
57819
|
+
function textResult(data) {
|
|
57820
|
+
return {
|
|
57821
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
|
|
57822
|
+
};
|
|
57823
|
+
}
|
|
57761
57824
|
function errorResult(message) {
|
|
57762
57825
|
return {
|
|
57763
57826
|
content: [{ type: "text", text: redactSecrets(message) }],
|
|
@@ -57765,6 +57828,418 @@ function errorResult(message) {
|
|
|
57765
57828
|
};
|
|
57766
57829
|
}
|
|
57767
57830
|
|
|
57831
|
+
// node_modules/@chrischall/mcp-utils/dist/server/confirmation.js
|
|
57832
|
+
import { createHash, createHmac, timingSafeEqual } from "node:crypto";
|
|
57833
|
+
|
|
57834
|
+
// node_modules/@chrischall/mcp-utils/dist/server/canonical.js
|
|
57835
|
+
function canonicalJson(value) {
|
|
57836
|
+
if (value === null || typeof value === "string" || typeof value === "boolean")
|
|
57837
|
+
return JSON.stringify(value);
|
|
57838
|
+
if (value === void 0)
|
|
57839
|
+
return "null";
|
|
57840
|
+
if (typeof value === "number")
|
|
57841
|
+
return Number.isFinite(value) ? JSON.stringify(value) : `{"$num":"${String(value)}"}`;
|
|
57842
|
+
if (typeof value === "bigint")
|
|
57843
|
+
return `{"$bigint":"${value.toString()}"}`;
|
|
57844
|
+
if (typeof value === "function" || typeof value === "symbol") {
|
|
57845
|
+
throw new TypeError(`confirmation: cannot bind a ${typeof value} argument value.`);
|
|
57846
|
+
}
|
|
57847
|
+
if (Array.isArray(value))
|
|
57848
|
+
return `[${value.map(canonicalJson).join(",")}]`;
|
|
57849
|
+
if (value instanceof Date) {
|
|
57850
|
+
return `{"$date":${JSON.stringify(Number.isNaN(value.getTime()) ? "Invalid Date" : value.toISOString())}}`;
|
|
57851
|
+
}
|
|
57852
|
+
if (ArrayBuffer.isView(value)) {
|
|
57853
|
+
const bytes = Buffer.from(value.buffer, value.byteOffset, value.byteLength);
|
|
57854
|
+
return `{"$bytes":${JSON.stringify(bytes.toString("base64"))}}`;
|
|
57855
|
+
}
|
|
57856
|
+
if (value instanceof ArrayBuffer)
|
|
57857
|
+
return `{"$bytes":${JSON.stringify(Buffer.from(value).toString("base64"))}}`;
|
|
57858
|
+
if (value instanceof Map) {
|
|
57859
|
+
const entries2 = [...value.entries()].map(([k, v]) => `[${canonicalJson(k)},${canonicalJson(v)}]`).sort();
|
|
57860
|
+
return `{"$map":[${entries2.join(",")}]}`;
|
|
57861
|
+
}
|
|
57862
|
+
if (value instanceof Set) {
|
|
57863
|
+
return `{"$set":[${[...value].map(canonicalJson).sort().join(",")}]}`;
|
|
57864
|
+
}
|
|
57865
|
+
const proto = Object.getPrototypeOf(value);
|
|
57866
|
+
if (proto !== Object.prototype && proto !== null) {
|
|
57867
|
+
throw new TypeError("confirmation: cannot bind a non-plain object argument value (class instance).");
|
|
57868
|
+
}
|
|
57869
|
+
const entries = Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
57870
|
+
return `{${entries.map(([k, v]) => `${JSON.stringify(escapeKey(k))}:${canonicalJson(v)}`).join(",")}}`;
|
|
57871
|
+
}
|
|
57872
|
+
function escapeKey(key) {
|
|
57873
|
+
return key.startsWith("$") ? `$${key}` : key;
|
|
57874
|
+
}
|
|
57875
|
+
|
|
57876
|
+
// node_modules/@chrischall/mcp-utils/dist/server/confirmation.js
|
|
57877
|
+
var DEFAULT_REQUEST_KEY = "confirmation";
|
|
57878
|
+
var DEFAULT_CONFIRMATION_LABEL = "Confirm this action should proceed.";
|
|
57879
|
+
var UNSUPPORTED_NOTE = "Nothing was done because this client cannot show a confirmation prompt (it declares no MCP elicitation capability), and this action is never taken without one";
|
|
57880
|
+
var STATE_PREFIX = "mcpu.confirm.v1.";
|
|
57881
|
+
function bindingKey(binding) {
|
|
57882
|
+
if (binding.ttlSeconds !== void 0 && !(Number.isFinite(binding.ttlSeconds) && binding.ttlSeconds > 0)) {
|
|
57883
|
+
throw new RangeError("requireConfirmation: binding.ttlSeconds must be a finite number greater than 0.");
|
|
57884
|
+
}
|
|
57885
|
+
const key = typeof binding.key === "string" ? Buffer.from(binding.key, "utf8") : Buffer.from(binding.key);
|
|
57886
|
+
if (key.length < 32) {
|
|
57887
|
+
throw new RangeError("requireConfirmation: binding.key must be at least 32 bytes.");
|
|
57888
|
+
}
|
|
57889
|
+
return key;
|
|
57890
|
+
}
|
|
57891
|
+
function commitment(action, args) {
|
|
57892
|
+
return createHash("sha256").update(`${action}\0${canonicalJson(args)}`).digest("base64url");
|
|
57893
|
+
}
|
|
57894
|
+
function mintState(key, action, binding) {
|
|
57895
|
+
const exp = Math.floor(Date.now() / 1e3) + (binding.ttlSeconds ?? 600);
|
|
57896
|
+
const body = Buffer.from(JSON.stringify({ c: commitment(action, binding.args), exp })).toString("base64url");
|
|
57897
|
+
const mac3 = createHmac("sha256", key).update(`${STATE_PREFIX}${body}`).digest("base64url");
|
|
57898
|
+
return `${STATE_PREFIX}${body}.${mac3}`;
|
|
57899
|
+
}
|
|
57900
|
+
function verifyState(key, action, binding, state) {
|
|
57901
|
+
if (typeof state !== "string" || !state.startsWith(STATE_PREFIX))
|
|
57902
|
+
return false;
|
|
57903
|
+
const rest = state.slice(STATE_PREFIX.length);
|
|
57904
|
+
const dot = rest.indexOf(".");
|
|
57905
|
+
if (dot < 0)
|
|
57906
|
+
return false;
|
|
57907
|
+
const body = rest.slice(0, dot);
|
|
57908
|
+
const mac3 = Buffer.from(rest.slice(dot + 1), "base64url");
|
|
57909
|
+
const expected = createHmac("sha256", key).update(`${STATE_PREFIX}${body}`).digest();
|
|
57910
|
+
if (mac3.length !== expected.length || !timingSafeEqual(mac3, expected))
|
|
57911
|
+
return false;
|
|
57912
|
+
let payload;
|
|
57913
|
+
try {
|
|
57914
|
+
payload = JSON.parse(Buffer.from(body, "base64url").toString("utf8"));
|
|
57915
|
+
} catch {
|
|
57916
|
+
return false;
|
|
57917
|
+
}
|
|
57918
|
+
if (typeof payload.exp !== "number" || payload.exp * 1e3 <= Date.now())
|
|
57919
|
+
return false;
|
|
57920
|
+
const want = Buffer.from(commitment(action, binding.args));
|
|
57921
|
+
const got = Buffer.from(typeof payload.c === "string" ? payload.c : "");
|
|
57922
|
+
return got.length === want.length && timingSafeEqual(got, want);
|
|
57923
|
+
}
|
|
57924
|
+
function echoedState(ctx) {
|
|
57925
|
+
const accessor = ctx.mcpReq.requestState;
|
|
57926
|
+
return typeof accessor === "function" ? accessor() : void 0;
|
|
57927
|
+
}
|
|
57928
|
+
function requireConfirmation(ctx, options) {
|
|
57929
|
+
const requestKey = options.requestKey ?? DEFAULT_REQUEST_KEY;
|
|
57930
|
+
const key = options.binding ? bindingKey(options.binding) : void 0;
|
|
57931
|
+
const confirmationSchema = external_exports.object({
|
|
57932
|
+
confirmed: external_exports.boolean().describe(options.confirmationLabel ?? DEFAULT_CONFIRMATION_LABEL)
|
|
57933
|
+
});
|
|
57934
|
+
const ask = () => {
|
|
57935
|
+
const preview = {
|
|
57936
|
+
action: options.action,
|
|
57937
|
+
...options.details === void 0 ? {} : { details: options.details }
|
|
57938
|
+
};
|
|
57939
|
+
return inputRequired({
|
|
57940
|
+
inputRequests: {
|
|
57941
|
+
[requestKey]: inputRequired.elicit({
|
|
57942
|
+
message: `${options.message}
|
|
57943
|
+
${JSON.stringify(preview, null, 2)}`,
|
|
57944
|
+
requestedSchema: confirmationSchema
|
|
57945
|
+
})
|
|
57946
|
+
},
|
|
57947
|
+
...key && options.binding ? { requestState: mintState(key, options.action, options.binding) } : {}
|
|
57948
|
+
});
|
|
57949
|
+
};
|
|
57950
|
+
const response = inputResponse(ctx.mcpReq.inputResponses, requestKey);
|
|
57951
|
+
const accepted = acceptedContent(ctx.mcpReq.inputResponses, requestKey, confirmationSchema);
|
|
57952
|
+
if (response.kind === "missing") {
|
|
57953
|
+
if (callerAcceptsFormElicitation(ctx) === false) {
|
|
57954
|
+
return textResult({
|
|
57955
|
+
confirmed: false,
|
|
57956
|
+
dispatched: false,
|
|
57957
|
+
action: options.action,
|
|
57958
|
+
reason: "confirmation-unsupported",
|
|
57959
|
+
note: options.unsupportedNote ? `${UNSUPPORTED_NOTE}. ${options.unsupportedNote}` : `${UNSUPPORTED_NOTE}.`
|
|
57960
|
+
});
|
|
57961
|
+
}
|
|
57962
|
+
return ask();
|
|
57963
|
+
}
|
|
57964
|
+
if (response.kind === "elicit" && response.action === "accept" && accepted?.confirmed === true) {
|
|
57965
|
+
if (key && options.binding) {
|
|
57966
|
+
const state = echoedState(ctx);
|
|
57967
|
+
if (state === void 0 || state === null) {
|
|
57968
|
+
return errorResult(`Confirmation for ${options.action} was accepted, but the retry carried no requestState, so it cannot be checked against the prompt that was shown. Nothing was done. The likely cause is that the MCP client or host does not round-trip requestState (it must echo the requestState from the input_required result back on the retry); asking again would loop.`);
|
|
57969
|
+
}
|
|
57970
|
+
if (!verifyState(key, options.action, options.binding, state))
|
|
57971
|
+
return ask();
|
|
57972
|
+
}
|
|
57973
|
+
return void 0;
|
|
57974
|
+
}
|
|
57975
|
+
return textResult({
|
|
57976
|
+
confirmed: false,
|
|
57977
|
+
cancelled: true,
|
|
57978
|
+
action: options.action,
|
|
57979
|
+
note: "Nothing was changed because the confirmation was declined, cancelled, or left unchecked."
|
|
57980
|
+
});
|
|
57981
|
+
}
|
|
57982
|
+
|
|
57983
|
+
// node_modules/@chrischall/mcp-utils/dist/server/confirm-token.js
|
|
57984
|
+
import { createHash as createHash2, createHmac as createHmac2, randomBytes, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
|
|
57985
|
+
function createSpentTokenStore() {
|
|
57986
|
+
const spent = /* @__PURE__ */ new Map();
|
|
57987
|
+
return {
|
|
57988
|
+
has: (nonce) => spent.has(nonce),
|
|
57989
|
+
add: (nonce, exp) => {
|
|
57990
|
+
spent.set(nonce, exp);
|
|
57991
|
+
},
|
|
57992
|
+
prune: (now) => {
|
|
57993
|
+
for (const [nonce, exp] of spent)
|
|
57994
|
+
if (exp < now)
|
|
57995
|
+
spent.delete(nonce);
|
|
57996
|
+
},
|
|
57997
|
+
clear: () => spent.clear(),
|
|
57998
|
+
get size() {
|
|
57999
|
+
return spent.size;
|
|
58000
|
+
}
|
|
58001
|
+
};
|
|
58002
|
+
}
|
|
58003
|
+
var PROCESS_SPENT = createSpentTokenStore();
|
|
58004
|
+
var PREFIX = "mcpu.token.v1.";
|
|
58005
|
+
var DEFAULT_TTL_SECONDS = 600;
|
|
58006
|
+
function tokenKey(key) {
|
|
58007
|
+
const bytes = typeof key === "string" ? Buffer.from(key, "utf8") : Buffer.from(key);
|
|
58008
|
+
if (bytes.length < 32)
|
|
58009
|
+
throw new RangeError("confirm token: key must be at least 32 bytes.");
|
|
58010
|
+
return bytes;
|
|
58011
|
+
}
|
|
58012
|
+
function ttlOf(ttlSeconds) {
|
|
58013
|
+
if (ttlSeconds === void 0)
|
|
58014
|
+
return DEFAULT_TTL_SECONDS;
|
|
58015
|
+
if (!(Number.isFinite(ttlSeconds) && ttlSeconds > 0)) {
|
|
58016
|
+
throw new RangeError("confirm token: ttlSeconds must be a finite number greater than 0.");
|
|
58017
|
+
}
|
|
58018
|
+
return ttlSeconds;
|
|
58019
|
+
}
|
|
58020
|
+
function sign(key, body) {
|
|
58021
|
+
return createHmac2("sha256", key).update(`${PREFIX}${body}`).digest();
|
|
58022
|
+
}
|
|
58023
|
+
function hashConfirmPayload(payload) {
|
|
58024
|
+
return createHash2("sha256").update(canonicalJson(payload)).digest("base64url");
|
|
58025
|
+
}
|
|
58026
|
+
function issueConfirmToken(key, binding, options = {}) {
|
|
58027
|
+
const k = tokenKey(key);
|
|
58028
|
+
const now = options.now ?? Date.now();
|
|
58029
|
+
const exp = now + ttlOf(options.ttlSeconds) * 1e3;
|
|
58030
|
+
const claims = {
|
|
58031
|
+
t: binding.tool,
|
|
58032
|
+
...binding.account === void 0 ? {} : { a: binding.account },
|
|
58033
|
+
g: binding.target,
|
|
58034
|
+
...binding.revision === void 0 ? {} : { r: binding.revision },
|
|
58035
|
+
h: binding.payloadHash,
|
|
58036
|
+
exp,
|
|
58037
|
+
n: randomBytes(16).toString("base64url")
|
|
58038
|
+
};
|
|
58039
|
+
const body = Buffer.from(JSON.stringify(claims), "utf8").toString("base64url");
|
|
58040
|
+
return { token: `${PREFIX}${body}.${sign(k, body).toString("base64url")}`, expiresAt: new Date(exp).toISOString() };
|
|
58041
|
+
}
|
|
58042
|
+
function parseClaims(body) {
|
|
58043
|
+
try {
|
|
58044
|
+
const claims = JSON.parse(Buffer.from(body, "base64url").toString("utf8"));
|
|
58045
|
+
return claims && typeof claims === "object" ? claims : void 0;
|
|
58046
|
+
} catch {
|
|
58047
|
+
return void 0;
|
|
58048
|
+
}
|
|
58049
|
+
}
|
|
58050
|
+
function verifyConfirmToken(key, token, binding, options = {}) {
|
|
58051
|
+
const k = tokenKey(key);
|
|
58052
|
+
const now = options.now ?? Date.now();
|
|
58053
|
+
const spent = options.spent ?? PROCESS_SPENT;
|
|
58054
|
+
spent.prune(now);
|
|
58055
|
+
if (!token.startsWith(PREFIX))
|
|
58056
|
+
return { ok: false, error: "TOKEN_INVALID" };
|
|
58057
|
+
const rest = token.slice(PREFIX.length);
|
|
58058
|
+
const dot = rest.indexOf(".");
|
|
58059
|
+
const body = dot < 0 ? "" : rest.slice(0, dot);
|
|
58060
|
+
const mac3 = Buffer.from(dot < 0 ? "" : rest.slice(dot + 1), "base64url");
|
|
58061
|
+
const expected = sign(k, body);
|
|
58062
|
+
if (!body || mac3.length !== expected.length || !timingSafeEqual2(mac3, expected))
|
|
58063
|
+
return { ok: false, error: "TOKEN_INVALID" };
|
|
58064
|
+
const claims = parseClaims(body);
|
|
58065
|
+
if (!claims)
|
|
58066
|
+
return { ok: false, error: "TOKEN_INVALID" };
|
|
58067
|
+
if (claims.t !== binding.tool || claims.a !== binding.account || claims.g !== binding.target) {
|
|
58068
|
+
return { ok: false, error: "TOKEN_INVALID" };
|
|
58069
|
+
}
|
|
58070
|
+
if (spent.has(claims.n))
|
|
58071
|
+
return { ok: false, error: "TOKEN_REUSED" };
|
|
58072
|
+
if (now > claims.exp)
|
|
58073
|
+
return { ok: false, error: "TOKEN_EXPIRED" };
|
|
58074
|
+
if (claims.r !== binding.revision)
|
|
58075
|
+
return { ok: false, error: "DRAFT_CHANGED", reason: "revision-changed" };
|
|
58076
|
+
if (claims.h !== binding.payloadHash)
|
|
58077
|
+
return { ok: false, error: "DRAFT_CHANGED", reason: "payload-changed" };
|
|
58078
|
+
spent.add(claims.n, claims.exp);
|
|
58079
|
+
return { ok: true };
|
|
58080
|
+
}
|
|
58081
|
+
var CONFIRM_TOKEN_INSTRUCTION = "Show this preview to the user verbatim and proceed only after they explicitly approve in chat. Then call again with confirmToken.";
|
|
58082
|
+
var confirmTokenParam = external_exports.string().optional().describe(`ONLY for the two-step confirmation fallback (a client without MCP elicitation). The confirmToken from this same tool's phase-1 "confirmation-required" response, passed back ONLY after the user has seen that preview and explicitly approved it in chat \u2014 never on the first call, never invented, never reused. Call again with the same arguments. Ignored when the client supports elicitation.`);
|
|
58083
|
+
var TOKEN_ERROR_NOTE = {
|
|
58084
|
+
TOKEN_EXPIRED: "Nothing was sent or changed: the confirmToken expired. Call again WITHOUT confirmToken for a fresh preview, and ask the user to approve it again.",
|
|
58085
|
+
TOKEN_REUSED: "Nothing was sent or changed by this call: this confirmToken was already used, and one approval acts once. If doing it again is really intended, call again WITHOUT confirmToken and get a new approval.",
|
|
58086
|
+
TOKEN_INVALID: "Nothing was sent or changed: this confirmToken was not issued by this server for this tool, account and target (or the server has restarted since). Call again WITHOUT confirmToken for a fresh preview and approval."
|
|
58087
|
+
};
|
|
58088
|
+
var CHANGED_NOTE = {
|
|
58089
|
+
"revision-changed": "Nothing was sent or changed: the target was edited since the user approved it (its version rotated), so what would happen is not what they saw.",
|
|
58090
|
+
"payload-changed": "Nothing was sent or changed: what would happen no longer matches what the user approved."
|
|
58091
|
+
};
|
|
58092
|
+
function isToolResult(value) {
|
|
58093
|
+
return Array.isArray(value.content);
|
|
58094
|
+
}
|
|
58095
|
+
function rejection2(data) {
|
|
58096
|
+
return { ...textResult({ status: "confirmation-rejected", confirmed: false, dispatched: false, ...data }), isError: true };
|
|
58097
|
+
}
|
|
58098
|
+
async function tokenConfirmation(action, fb) {
|
|
58099
|
+
const subject = await fb.subject();
|
|
58100
|
+
if (isToolResult(subject))
|
|
58101
|
+
return subject;
|
|
58102
|
+
const binding = {
|
|
58103
|
+
tool: fb.tool,
|
|
58104
|
+
...fb.account === void 0 ? {} : { account: fb.account },
|
|
58105
|
+
target: subject.target,
|
|
58106
|
+
...subject.revision === void 0 ? {} : { revision: subject.revision },
|
|
58107
|
+
payloadHash: hashConfirmPayload(subject.payload)
|
|
58108
|
+
};
|
|
58109
|
+
const phaseOne = () => {
|
|
58110
|
+
const { token, expiresAt } = issueConfirmToken(fb.key, binding, { ttlSeconds: fb.ttlSeconds });
|
|
58111
|
+
return {
|
|
58112
|
+
action,
|
|
58113
|
+
preview: subject.preview,
|
|
58114
|
+
confirmToken: token,
|
|
58115
|
+
expiresAt,
|
|
58116
|
+
ttlSeconds: ttlOf(fb.ttlSeconds),
|
|
58117
|
+
instruction: fb.instruction ?? CONFIRM_TOKEN_INSTRUCTION
|
|
58118
|
+
};
|
|
58119
|
+
};
|
|
58120
|
+
if (!fb.confirmToken) {
|
|
58121
|
+
return textResult({ status: "confirmation-required", confirmed: false, dispatched: false, ...phaseOne() });
|
|
58122
|
+
}
|
|
58123
|
+
const verdict = verifyConfirmToken(fb.key, fb.confirmToken, binding, { spent: fb.spent });
|
|
58124
|
+
if (verdict.ok)
|
|
58125
|
+
return void 0;
|
|
58126
|
+
if (verdict.error === "DRAFT_CHANGED") {
|
|
58127
|
+
return rejection2({
|
|
58128
|
+
error: "DRAFT_CHANGED",
|
|
58129
|
+
reason: verdict.reason,
|
|
58130
|
+
note: `${CHANGED_NOTE[verdict.reason]} The current preview and a fresh confirmToken are below.`,
|
|
58131
|
+
...phaseOne()
|
|
58132
|
+
});
|
|
58133
|
+
}
|
|
58134
|
+
return rejection2({ error: verdict.error, action, note: TOKEN_ERROR_NOTE[verdict.error] });
|
|
58135
|
+
}
|
|
58136
|
+
async function requireConfirmationWithFallback(ctx, options) {
|
|
58137
|
+
const { tokenFallback, ...confirmation } = options;
|
|
58138
|
+
if (tokenFallback && callerAcceptsFormElicitation(ctx) === false) {
|
|
58139
|
+
return tokenConfirmation(options.action, tokenFallback);
|
|
58140
|
+
}
|
|
58141
|
+
return requireConfirmation(ctx, confirmation);
|
|
58142
|
+
}
|
|
58143
|
+
|
|
58144
|
+
// node_modules/@chrischall/mcp-utils/dist/server/confirm-env.js
|
|
58145
|
+
import { createHash as createHash3, randomBytes as randomBytes2 } from "node:crypto";
|
|
58146
|
+
|
|
58147
|
+
// node_modules/@chrischall/mcp-utils/dist/config/index.js
|
|
58148
|
+
import { homedir } from "node:os";
|
|
58149
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
58150
|
+
var PLACEHOLDER_RE = /^\$\{[^}]*\}$/;
|
|
58151
|
+
function readEnvVar(key, opts = {}) {
|
|
58152
|
+
const env = opts.env ?? process.env;
|
|
58153
|
+
const raw = env[key];
|
|
58154
|
+
if (typeof raw === "string") {
|
|
58155
|
+
const trimmed = raw.trim();
|
|
58156
|
+
if (trimmed.length > 0 && trimmed !== "undefined" && trimmed !== "null" && !PLACEHOLDER_RE.test(trimmed)) {
|
|
58157
|
+
return trimmed;
|
|
58158
|
+
}
|
|
58159
|
+
}
|
|
58160
|
+
return opts.default;
|
|
58161
|
+
}
|
|
58162
|
+
function expandPath(p) {
|
|
58163
|
+
let expanded = p;
|
|
58164
|
+
if (p === "~") {
|
|
58165
|
+
expanded = homedir();
|
|
58166
|
+
} else if (p.startsWith("~/")) {
|
|
58167
|
+
expanded = join(homedir(), p.slice(2));
|
|
58168
|
+
}
|
|
58169
|
+
return isAbsolute(expanded) ? expanded : resolve(expanded);
|
|
58170
|
+
}
|
|
58171
|
+
async function loadDotenvSafely(opts = {}) {
|
|
58172
|
+
try {
|
|
58173
|
+
const mod = await import(
|
|
58174
|
+
/* @vite-ignore */
|
|
58175
|
+
"dotenv"
|
|
58176
|
+
);
|
|
58177
|
+
const result = mod.config({
|
|
58178
|
+
...opts.path !== void 0 ? { path: opts.path } : {},
|
|
58179
|
+
override: opts.override ?? false,
|
|
58180
|
+
quiet: true
|
|
58181
|
+
});
|
|
58182
|
+
return result.error === void 0;
|
|
58183
|
+
} catch {
|
|
58184
|
+
return false;
|
|
58185
|
+
}
|
|
58186
|
+
}
|
|
58187
|
+
|
|
58188
|
+
// node_modules/@chrischall/mcp-utils/dist/server/confirm-env.js
|
|
58189
|
+
var MODES = /* @__PURE__ */ new Set(["ask-user", "auto", "refuse"]);
|
|
58190
|
+
var DEFAULT_TTL_SECONDS2 = 600;
|
|
58191
|
+
var warned = /* @__PURE__ */ new Set();
|
|
58192
|
+
function readConfirmMode(env = process.env) {
|
|
58193
|
+
const raw = readEnvVar("MCP_CONFIRM_MODE", { env })?.trim().toLowerCase();
|
|
58194
|
+
if (!raw)
|
|
58195
|
+
return "ask-user";
|
|
58196
|
+
if (MODES.has(raw))
|
|
58197
|
+
return raw;
|
|
58198
|
+
if (!warned.has(raw)) {
|
|
58199
|
+
warned.add(raw);
|
|
58200
|
+
process.stderr.write(`MCP_CONFIRM_MODE="${raw}" is not one of ask-user, auto, refuse; treating it as refuse.
|
|
58201
|
+
`);
|
|
58202
|
+
}
|
|
58203
|
+
return "refuse";
|
|
58204
|
+
}
|
|
58205
|
+
function confirmTtlFromEnv(env = process.env) {
|
|
58206
|
+
const raw = readEnvVar("MCP_CONFIRM_TTL_SECONDS", { env })?.trim();
|
|
58207
|
+
return raw && /^[1-9]\d*$/.test(raw) ? Number(raw) : DEFAULT_TTL_SECONDS2;
|
|
58208
|
+
}
|
|
58209
|
+
var processKey;
|
|
58210
|
+
function confirmKeyFromEnv(env = process.env) {
|
|
58211
|
+
const secret = readEnvVar("MCP_CONFIRM_SECRET", { env });
|
|
58212
|
+
if (secret)
|
|
58213
|
+
return createHash3("sha256").update(secret, "utf8").digest();
|
|
58214
|
+
processKey ??= randomBytes2(32);
|
|
58215
|
+
return processKey;
|
|
58216
|
+
}
|
|
58217
|
+
var CONFIRM_TOKEN_AUTO_INSTRUCTION = "Nothing has been done yet. Review this preview; if it is what was intended, call again with the same arguments plus confirmToken. (This server runs with MCP_CONFIRM_MODE=auto, so the user's approval in chat is not required.)";
|
|
58218
|
+
var REFUSE_HINT = "Set MCP_CONFIRM_MODE=ask-user on the server to allow two-step confirmation instead.";
|
|
58219
|
+
function confirmationFromEnv(options) {
|
|
58220
|
+
const { tool, account, confirmToken, subject, instruction, spent, env = process.env, ...confirmation } = options;
|
|
58221
|
+
const mode = readConfirmMode(env);
|
|
58222
|
+
if (mode === "refuse") {
|
|
58223
|
+
return {
|
|
58224
|
+
...confirmation,
|
|
58225
|
+
unsupportedNote: confirmation.unsupportedNote ? `${confirmation.unsupportedNote} ${REFUSE_HINT}` : REFUSE_HINT
|
|
58226
|
+
};
|
|
58227
|
+
}
|
|
58228
|
+
return {
|
|
58229
|
+
...confirmation,
|
|
58230
|
+
tokenFallback: {
|
|
58231
|
+
key: confirmKeyFromEnv(env),
|
|
58232
|
+
tool,
|
|
58233
|
+
...account === void 0 ? {} : { account },
|
|
58234
|
+
...confirmToken === void 0 ? {} : { confirmToken },
|
|
58235
|
+
subject,
|
|
58236
|
+
ttlSeconds: confirmTtlFromEnv(env),
|
|
58237
|
+
instruction: mode === "auto" ? CONFIRM_TOKEN_AUTO_INSTRUCTION : instruction ?? CONFIRM_TOKEN_INSTRUCTION,
|
|
58238
|
+
...spent === void 0 ? {} : { spent }
|
|
58239
|
+
}
|
|
58240
|
+
};
|
|
58241
|
+
}
|
|
58242
|
+
|
|
57768
58243
|
// node_modules/@chrischall/mcp-utils/dist/server/index.js
|
|
57769
58244
|
var SERVER_PROTOCOL_VERSIONS = Object.freeze([
|
|
57770
58245
|
"2026-07-28",
|
|
@@ -57940,47 +58415,6 @@ function runMcp(opts) {
|
|
|
57940
58415
|
return handle;
|
|
57941
58416
|
}
|
|
57942
58417
|
|
|
57943
|
-
// node_modules/@chrischall/mcp-utils/dist/config/index.js
|
|
57944
|
-
import { homedir } from "node:os";
|
|
57945
|
-
import { isAbsolute, join, resolve } from "node:path";
|
|
57946
|
-
var PLACEHOLDER_RE = /^\$\{[^}]*\}$/;
|
|
57947
|
-
function readEnvVar(key, opts = {}) {
|
|
57948
|
-
const env = opts.env ?? process.env;
|
|
57949
|
-
const raw = env[key];
|
|
57950
|
-
if (typeof raw === "string") {
|
|
57951
|
-
const trimmed = raw.trim();
|
|
57952
|
-
if (trimmed.length > 0 && trimmed !== "undefined" && trimmed !== "null" && !PLACEHOLDER_RE.test(trimmed)) {
|
|
57953
|
-
return trimmed;
|
|
57954
|
-
}
|
|
57955
|
-
}
|
|
57956
|
-
return opts.default;
|
|
57957
|
-
}
|
|
57958
|
-
function expandPath(p) {
|
|
57959
|
-
let expanded = p;
|
|
57960
|
-
if (p === "~") {
|
|
57961
|
-
expanded = homedir();
|
|
57962
|
-
} else if (p.startsWith("~/")) {
|
|
57963
|
-
expanded = join(homedir(), p.slice(2));
|
|
57964
|
-
}
|
|
57965
|
-
return isAbsolute(expanded) ? expanded : resolve(expanded);
|
|
57966
|
-
}
|
|
57967
|
-
async function loadDotenvSafely(opts = {}) {
|
|
57968
|
-
try {
|
|
57969
|
-
const mod = await import(
|
|
57970
|
-
/* @vite-ignore */
|
|
57971
|
-
"dotenv"
|
|
57972
|
-
);
|
|
57973
|
-
const result = mod.config({
|
|
57974
|
-
...opts.path !== void 0 ? { path: opts.path } : {},
|
|
57975
|
-
override: opts.override ?? false,
|
|
57976
|
-
quiet: true
|
|
57977
|
-
});
|
|
57978
|
-
return result.error === void 0;
|
|
57979
|
-
} catch {
|
|
57980
|
-
return false;
|
|
57981
|
-
}
|
|
57982
|
-
}
|
|
57983
|
-
|
|
57984
58418
|
// node_modules/@chrischall/mcp-utils/dist/fs/index.js
|
|
57985
58419
|
import { openAsBlob } from "node:fs";
|
|
57986
58420
|
import { open as open2 } from "node:fs/promises";
|
|
@@ -58126,7 +58560,7 @@ function toolAnnotations(opts = {}) {
|
|
|
58126
58560
|
}
|
|
58127
58561
|
|
|
58128
58562
|
// src/version.ts
|
|
58129
|
-
var VERSION = "2.1
|
|
58563
|
+
var VERSION = "2.2.1";
|
|
58130
58564
|
|
|
58131
58565
|
// src/client.ts
|
|
58132
58566
|
import { dirname as dirname2, join as join2 } from "path";
|
|
@@ -58944,6 +59378,22 @@ var CheckinStoreCore = class {
|
|
|
58944
59378
|
[...params, limit]
|
|
58945
59379
|
);
|
|
58946
59380
|
}
|
|
59381
|
+
/** Delete one user's cached rows (case-insensitive) in a single transaction. */
|
|
59382
|
+
forgetUser(username) {
|
|
59383
|
+
const key = username.toLowerCase();
|
|
59384
|
+
const count2 = (table) => Number(this.db.get(`SELECT COUNT(*) AS n FROM ${table} WHERE username = ?`, [key])?.n ?? 0);
|
|
59385
|
+
const removed = {
|
|
59386
|
+
checkins: count2("checkins"),
|
|
59387
|
+
distinct_beers: count2("distinct_beers"),
|
|
59388
|
+
sync_state: count2("sync_state")
|
|
59389
|
+
};
|
|
59390
|
+
this.db.transaction(() => {
|
|
59391
|
+
this.db.run("DELETE FROM checkins WHERE username = ?", [key]);
|
|
59392
|
+
this.db.run("DELETE FROM distinct_beers WHERE username = ?", [key]);
|
|
59393
|
+
this.db.run("DELETE FROM sync_state WHERE username = ?", [key]);
|
|
59394
|
+
});
|
|
59395
|
+
return removed;
|
|
59396
|
+
}
|
|
58947
59397
|
getBeerMeta(bids) {
|
|
58948
59398
|
if (bids.length === 0) return [];
|
|
58949
59399
|
const placeholders = bids.map(() => "?").join(", ");
|
|
@@ -59024,6 +59474,9 @@ var LocalCacheStore = class {
|
|
|
59024
59474
|
async upsertBeerMeta(rows) {
|
|
59025
59475
|
this.core.upsertBeerMeta(rows);
|
|
59026
59476
|
}
|
|
59477
|
+
async forgetUser(username) {
|
|
59478
|
+
return this.core.forgetUser(username);
|
|
59479
|
+
}
|
|
59027
59480
|
};
|
|
59028
59481
|
function escapeLike(s) {
|
|
59029
59482
|
return s.replace(/[%_]/g, " ");
|
|
@@ -59667,6 +60120,22 @@ function registerDiscoverTools(server, client2) {
|
|
|
59667
60120
|
);
|
|
59668
60121
|
}
|
|
59669
60122
|
|
|
60123
|
+
// src/tools/confirm.ts
|
|
60124
|
+
var CONFIRM_FLOW = "Asks the user to confirm first: a confirmation prompt where the client supports one; otherwise the first call returns a preview and a confirmToken, and only a repeat call with that token proceeds (see MCP_CONFIRM_MODE).";
|
|
60125
|
+
function confirmWrite(ctx, c) {
|
|
60126
|
+
return requireConfirmationWithFallback(
|
|
60127
|
+
ctx,
|
|
60128
|
+
confirmationFromEnv({
|
|
60129
|
+
action: c.action,
|
|
60130
|
+
message: c.message,
|
|
60131
|
+
details: c.preview,
|
|
60132
|
+
tool: c.tool,
|
|
60133
|
+
confirmToken: c.confirmToken,
|
|
60134
|
+
subject: () => ({ target: String(c.target), payload: c.payload, preview: c.preview })
|
|
60135
|
+
})
|
|
60136
|
+
);
|
|
60137
|
+
}
|
|
60138
|
+
|
|
59670
60139
|
// src/tools/friends.ts
|
|
59671
60140
|
var TargetUidSchema = external_exports.number().int().positive().describe("The target user id (uid) \u2014 from a user profile (response.user.uid) or a friends-list item");
|
|
59672
60141
|
var ACTIONS = [
|
|
@@ -59705,7 +60174,7 @@ function registerFriendActionTools(server, client2) {
|
|
|
59705
60174
|
action.tool,
|
|
59706
60175
|
{
|
|
59707
60176
|
title: action.title,
|
|
59708
|
-
description: `${action.detail} Acts on YOUR account and affects a real relationship with another person.
|
|
60177
|
+
description: `${action.detail} Acts on YOUR account and affects a real relationship with another person. ${CONFIRM_FLOW} Note: this endpoint path is taken from the Untappd app but is not otherwise independently verified.`,
|
|
59709
60178
|
// All four reach ANOTHER PERSON — the description says so itself: "affects
|
|
59710
60179
|
// a real relationship with another person". A friend request cannot be
|
|
59711
60180
|
// un-sent, and a rejection cannot be un-rejected, so none of them has an
|
|
@@ -59713,18 +60182,21 @@ function registerFriendActionTools(server, client2) {
|
|
|
59713
60182
|
annotations: toolAnnotations({ title: action.title, readOnly: false, idempotent: true, openWorld: true, destructive: true }),
|
|
59714
60183
|
inputSchema: external_exports.object({
|
|
59715
60184
|
target_uid: TargetUidSchema,
|
|
59716
|
-
|
|
60185
|
+
confirmToken: confirmTokenParam
|
|
59717
60186
|
})
|
|
59718
60187
|
},
|
|
59719
|
-
async ({ target_uid,
|
|
59720
|
-
|
|
59721
|
-
|
|
59722
|
-
|
|
59723
|
-
|
|
59724
|
-
|
|
59725
|
-
|
|
59726
|
-
|
|
59727
|
-
|
|
60188
|
+
async ({ target_uid, confirmToken }, ctx) => {
|
|
60189
|
+
const request = { method: "POST", path: `/friend/${action.path}/${target_uid}` };
|
|
60190
|
+
const gate = await confirmWrite(ctx, {
|
|
60191
|
+
tool: action.tool,
|
|
60192
|
+
action: `untappd.friend_${action.path}`,
|
|
60193
|
+
message: `Review and confirm: ${action.verb} Untappd user ${target_uid}.`,
|
|
60194
|
+
confirmToken,
|
|
60195
|
+
target: target_uid,
|
|
60196
|
+
payload: request,
|
|
60197
|
+
preview: { action: action.path, target_uid, ...request, note: `Will ${action.verb} user ${target_uid}.` }
|
|
60198
|
+
});
|
|
60199
|
+
if (gate) return gate;
|
|
59728
60200
|
const data = await client2.write("POST", `/friend/${action.path}/${target_uid}`);
|
|
59729
60201
|
return minifiedResult({ done: true, action: action.path, target_uid, result: data?.result });
|
|
59730
60202
|
}
|
|
@@ -59739,22 +60211,25 @@ function registerWishlistTools(server, client2) {
|
|
|
59739
60211
|
"untappd_wishlist_add",
|
|
59740
60212
|
{
|
|
59741
60213
|
title: "Add a beer to your wishlist",
|
|
59742
|
-
description:
|
|
60214
|
+
description: `Add a beer to YOUR Untappd wishlist by its bid. Writes to your account. ${CONFIRM_FLOW}`,
|
|
59743
60215
|
annotations: toolAnnotations({ title: "Add a beer to your wishlist", readOnly: false, idempotent: true, openWorld: true, destructive: false }),
|
|
59744
60216
|
inputSchema: external_exports.object({
|
|
59745
60217
|
bid: BidSchema2,
|
|
59746
|
-
|
|
60218
|
+
confirmToken: confirmTokenParam
|
|
59747
60219
|
})
|
|
59748
60220
|
},
|
|
59749
|
-
async ({ bid,
|
|
59750
|
-
|
|
59751
|
-
|
|
59752
|
-
|
|
59753
|
-
|
|
59754
|
-
|
|
59755
|
-
|
|
59756
|
-
|
|
59757
|
-
|
|
60221
|
+
async ({ bid, confirmToken }, ctx) => {
|
|
60222
|
+
const request = { method: "GET", path: "/user/wishlist/add", query: { bid } };
|
|
60223
|
+
const gate = await confirmWrite(ctx, {
|
|
60224
|
+
tool: "untappd_wishlist_add",
|
|
60225
|
+
action: "untappd.wishlist_add",
|
|
60226
|
+
message: "Review and confirm adding this beer to your Untappd wishlist:",
|
|
60227
|
+
confirmToken,
|
|
60228
|
+
target: bid,
|
|
60229
|
+
payload: request,
|
|
60230
|
+
preview: { action: "wishlist_add", bid, ...request, note: "Adds this beer to your Untappd wishlist." }
|
|
60231
|
+
});
|
|
60232
|
+
if (gate) return gate;
|
|
59758
60233
|
const data = await client2.write("GET", "/user/wishlist/add", { query: { bid } });
|
|
59759
60234
|
return minifiedResult({ added: true, bid, result: data?.result });
|
|
59760
60235
|
}
|
|
@@ -59763,22 +60238,25 @@ function registerWishlistTools(server, client2) {
|
|
|
59763
60238
|
"untappd_wishlist_remove",
|
|
59764
60239
|
{
|
|
59765
60240
|
title: "Remove a beer from your wishlist",
|
|
59766
|
-
description:
|
|
60241
|
+
description: `Remove a beer from YOUR Untappd wishlist by its bid. Writes to your account. ${CONFIRM_FLOW}`,
|
|
59767
60242
|
annotations: toolAnnotations({ title: "Remove a beer from your wishlist", readOnly: false, idempotent: true, openWorld: true, destructive: false }),
|
|
59768
60243
|
inputSchema: external_exports.object({
|
|
59769
60244
|
bid: BidSchema2,
|
|
59770
|
-
|
|
60245
|
+
confirmToken: confirmTokenParam
|
|
59771
60246
|
})
|
|
59772
60247
|
},
|
|
59773
|
-
async ({ bid,
|
|
59774
|
-
|
|
59775
|
-
|
|
59776
|
-
|
|
59777
|
-
|
|
59778
|
-
|
|
59779
|
-
|
|
59780
|
-
|
|
59781
|
-
|
|
60248
|
+
async ({ bid, confirmToken }, ctx) => {
|
|
60249
|
+
const request = { method: "GET", path: "/user/wishlist/delete", query: { bid } };
|
|
60250
|
+
const gate = await confirmWrite(ctx, {
|
|
60251
|
+
tool: "untappd_wishlist_remove",
|
|
60252
|
+
action: "untappd.wishlist_remove",
|
|
60253
|
+
message: "Review and confirm removing this beer from your Untappd wishlist:",
|
|
60254
|
+
confirmToken,
|
|
60255
|
+
target: bid,
|
|
60256
|
+
payload: request,
|
|
60257
|
+
preview: { action: "wishlist_remove", bid, ...request, note: "Removes this beer from your Untappd wishlist." }
|
|
60258
|
+
});
|
|
60259
|
+
if (gate) return gate;
|
|
59782
60260
|
const data = await client2.write("GET", "/user/wishlist/delete", { query: { bid } });
|
|
59783
60261
|
return minifiedResult({ removed: true, bid, result: data?.result });
|
|
59784
60262
|
}
|
|
@@ -59786,7 +60264,9 @@ function registerWishlistTools(server, client2) {
|
|
|
59786
60264
|
}
|
|
59787
60265
|
|
|
59788
60266
|
// src/tools/checkin.ts
|
|
59789
|
-
import { realpathSync as realpathSync2, statSync } from "node:fs";
|
|
60267
|
+
import { createReadStream, realpathSync as realpathSync2, statSync } from "node:fs";
|
|
60268
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
60269
|
+
import { pipeline } from "node:stream/promises";
|
|
59790
60270
|
import { delimiter, extname } from "node:path";
|
|
59791
60271
|
var CheckinIdSchema = external_exports.number().int().positive().describe("Untappd check-in id");
|
|
59792
60272
|
var PHOTO_CONTENT_TYPES = { jpg: "image/jpeg", png: "image/png" };
|
|
@@ -59832,7 +60312,13 @@ async function checkPhoto(photoPath) {
|
|
|
59832
60312
|
hint: "Attach a normal-sized JPEG or PNG photo."
|
|
59833
60313
|
});
|
|
59834
60314
|
}
|
|
59835
|
-
|
|
60315
|
+
let head;
|
|
60316
|
+
try {
|
|
60317
|
+
head = await readFileHead(real, 16, { allowedRoots: roots });
|
|
60318
|
+
} catch {
|
|
60319
|
+
throw new McpToolError("Photo file not found or not readable.");
|
|
60320
|
+
}
|
|
60321
|
+
const sniffed = sniffMimeBytes(head);
|
|
59836
60322
|
if (sniffed !== PHOTO_CONTENT_TYPES[ext]) {
|
|
59837
60323
|
throw createHelpfulError(
|
|
59838
60324
|
sniffed === "image/jpeg" || sniffed === "image/png" ? `Photo content (${sniffed}) does not match its .${ext} extension.` : "The file is not a JPEG or PNG image.",
|
|
@@ -59841,6 +60327,11 @@ async function checkPhoto(photoPath) {
|
|
|
59841
60327
|
}
|
|
59842
60328
|
return { path: real, size_bytes: size, ext, content_type: sniffed };
|
|
59843
60329
|
}
|
|
60330
|
+
async function photoSha256(path) {
|
|
60331
|
+
const hash2 = createHash4("sha256");
|
|
60332
|
+
await pipeline(createReadStream(path), hash2);
|
|
60333
|
+
return hash2.digest("hex");
|
|
60334
|
+
}
|
|
59844
60335
|
var RatingSchema = external_exports.number().min(0).max(5).refine((r) => Math.round(r * 4) === r * 4, { message: "rating must be a multiple of 0.25" });
|
|
59845
60336
|
function gmtOffsetHours(timeZone, at) {
|
|
59846
60337
|
const name = new Intl.DateTimeFormat("en-US", { timeZone, timeZoneName: "longOffset" }).formatToParts(at).find((p) => p.type === "timeZoneName")?.value;
|
|
@@ -59894,22 +60385,30 @@ function registerCheckinTools(server, client2) {
|
|
|
59894
60385
|
"untappd_toast",
|
|
59895
60386
|
{
|
|
59896
60387
|
title: "Toast an Untappd check-in",
|
|
59897
|
-
description:
|
|
60388
|
+
description: `Toast (like) a check-in on YOUR account. This endpoint is a TOGGLE: calling it on a check-in you have already toasted removes the toast. Writes to your Untappd account and is visible to others. ${CONFIRM_FLOW}`,
|
|
59898
60389
|
annotations: toolAnnotations({ title: "Toast an Untappd check-in", readOnly: false, idempotent: false, openWorld: true, destructive: false }),
|
|
59899
60390
|
inputSchema: external_exports.object({
|
|
59900
60391
|
checkin_id: CheckinIdSchema,
|
|
59901
|
-
|
|
60392
|
+
confirmToken: confirmTokenParam
|
|
59902
60393
|
})
|
|
59903
60394
|
},
|
|
59904
|
-
async ({ checkin_id,
|
|
59905
|
-
|
|
59906
|
-
|
|
59907
|
-
|
|
60395
|
+
async ({ checkin_id, confirmToken }, ctx) => {
|
|
60396
|
+
const request = { method: "POST", path: `/checkin/toast/${checkin_id}` };
|
|
60397
|
+
const gate = await confirmWrite(ctx, {
|
|
60398
|
+
tool: "untappd_toast",
|
|
60399
|
+
action: "untappd.toast",
|
|
60400
|
+
message: "Review and confirm toggling your toast on this Untappd check-in:",
|
|
60401
|
+
confirmToken,
|
|
60402
|
+
target: checkin_id,
|
|
60403
|
+
payload: request,
|
|
60404
|
+
preview: {
|
|
59908
60405
|
action: "toast",
|
|
59909
60406
|
checkin_id,
|
|
59910
|
-
|
|
59911
|
-
|
|
59912
|
-
|
|
60407
|
+
...request,
|
|
60408
|
+
note: "Toggles your toast on this check-in (removes it if you have already toasted)."
|
|
60409
|
+
}
|
|
60410
|
+
});
|
|
60411
|
+
if (gate) return gate;
|
|
59913
60412
|
const data = await nonIdempotentWrite(
|
|
59914
60413
|
() => client2.write("POST", `/checkin/toast/${checkin_id}`),
|
|
59915
60414
|
`Untappd did not answer the toast request for check-in ${checkin_id} in time, so it may or may not have been applied. Toast is a TOGGLE: retrying could remove a toast that did land. Check with untappd_checkin_info (checkin_id ${checkin_id}) before retrying.`
|
|
@@ -59921,24 +60420,32 @@ function registerCheckinTools(server, client2) {
|
|
|
59921
60420
|
"untappd_add_comment",
|
|
59922
60421
|
{
|
|
59923
60422
|
title: "Comment on an Untappd check-in",
|
|
59924
|
-
description: "Post a comment on a check-in from YOUR account.
|
|
60423
|
+
description: "Post a comment on a check-in from YOUR account. Writes to your Untappd account and is visible to others. " + CONFIRM_FLOW,
|
|
59925
60424
|
annotations: toolAnnotations({ title: "Comment on an Untappd check-in", readOnly: false, idempotent: false, openWorld: true, destructive: false }),
|
|
59926
60425
|
inputSchema: external_exports.object({
|
|
59927
60426
|
checkin_id: CheckinIdSchema,
|
|
59928
60427
|
comment: external_exports.string().min(1).max(2e3).describe("Comment text to post"),
|
|
59929
|
-
|
|
60428
|
+
confirmToken: confirmTokenParam
|
|
59930
60429
|
})
|
|
59931
60430
|
},
|
|
59932
|
-
async ({ checkin_id, comment,
|
|
59933
|
-
|
|
59934
|
-
|
|
59935
|
-
|
|
60431
|
+
async ({ checkin_id, comment, confirmToken }, ctx) => {
|
|
60432
|
+
const request = { method: "POST", path: `/checkin/addcomment/${checkin_id}`, form: { comment } };
|
|
60433
|
+
const gate = await confirmWrite(ctx, {
|
|
60434
|
+
tool: "untappd_add_comment",
|
|
60435
|
+
action: "untappd.add_comment",
|
|
60436
|
+
message: "Review and confirm posting this comment from your Untappd account:",
|
|
60437
|
+
confirmToken,
|
|
60438
|
+
target: checkin_id,
|
|
60439
|
+
payload: request,
|
|
60440
|
+
preview: {
|
|
59936
60441
|
action: "add_comment",
|
|
59937
60442
|
checkin_id,
|
|
59938
60443
|
comment,
|
|
59939
|
-
|
|
59940
|
-
|
|
59941
|
-
|
|
60444
|
+
...request,
|
|
60445
|
+
note: "Posts this comment to the check-in from your Untappd account, visible to others."
|
|
60446
|
+
}
|
|
60447
|
+
});
|
|
60448
|
+
if (gate) return gate;
|
|
59942
60449
|
const data = await nonIdempotentWrite(
|
|
59943
60450
|
() => client2.write("POST", `/checkin/addcomment/${checkin_id}`, { form: { comment } }),
|
|
59944
60451
|
`Untappd did not answer in time, so the comment may have been posted already. Check the comments with untappd_checkin_info (checkin_id ${checkin_id}) before retrying, or it may be posted twice.`
|
|
@@ -59950,22 +60457,25 @@ function registerCheckinTools(server, client2) {
|
|
|
59950
60457
|
"untappd_delete_comment",
|
|
59951
60458
|
{
|
|
59952
60459
|
title: "Delete a comment from an Untappd check-in",
|
|
59953
|
-
description:
|
|
60460
|
+
description: `Delete one of YOUR comments by its comment id (the id from a check-in's comments list). ${CONFIRM_FLOW}`,
|
|
59954
60461
|
annotations: toolAnnotations({ title: "Delete a comment from an Untappd check-in", readOnly: false, idempotent: true, openWorld: true, destructive: true }),
|
|
59955
60462
|
inputSchema: external_exports.object({
|
|
59956
60463
|
comment_id: external_exports.number().int().positive().describe("Untappd comment id (from a check-in's comments.items)"),
|
|
59957
|
-
|
|
60464
|
+
confirmToken: confirmTokenParam
|
|
59958
60465
|
})
|
|
59959
60466
|
},
|
|
59960
|
-
async ({ comment_id,
|
|
59961
|
-
|
|
59962
|
-
|
|
59963
|
-
|
|
59964
|
-
|
|
59965
|
-
|
|
59966
|
-
|
|
59967
|
-
|
|
59968
|
-
|
|
60467
|
+
async ({ comment_id, confirmToken }, ctx) => {
|
|
60468
|
+
const request = { method: "POST", path: `/checkin/deletecomment/${comment_id}` };
|
|
60469
|
+
const gate = await confirmWrite(ctx, {
|
|
60470
|
+
tool: "untappd_delete_comment",
|
|
60471
|
+
action: "untappd.delete_comment",
|
|
60472
|
+
message: "Review and confirm deleting this comment from your Untappd account:",
|
|
60473
|
+
confirmToken,
|
|
60474
|
+
target: comment_id,
|
|
60475
|
+
payload: request,
|
|
60476
|
+
preview: { action: "delete_comment", comment_id, ...request, note: "Deletes this comment from your Untappd account." }
|
|
60477
|
+
});
|
|
60478
|
+
if (gate) return gate;
|
|
59969
60479
|
const data = await client2.write("POST", `/checkin/deletecomment/${comment_id}`);
|
|
59970
60480
|
return minifiedResult({ deleted: true, comment_id, result: data?.result });
|
|
59971
60481
|
}
|
|
@@ -59974,22 +60484,30 @@ function registerCheckinTools(server, client2) {
|
|
|
59974
60484
|
"untappd_delete_checkin",
|
|
59975
60485
|
{
|
|
59976
60486
|
title: "Delete an Untappd check-in",
|
|
59977
|
-
description:
|
|
60487
|
+
description: `Permanently delete one of YOUR check-ins by its id. This is destructive and cannot be undone. ${CONFIRM_FLOW}`,
|
|
59978
60488
|
annotations: toolAnnotations({ title: "Delete an Untappd check-in", readOnly: false, idempotent: true, openWorld: true, destructive: true }),
|
|
59979
60489
|
inputSchema: external_exports.object({
|
|
59980
60490
|
checkin_id: CheckinIdSchema,
|
|
59981
|
-
|
|
60491
|
+
confirmToken: confirmTokenParam
|
|
59982
60492
|
})
|
|
59983
60493
|
},
|
|
59984
|
-
async ({ checkin_id,
|
|
59985
|
-
|
|
59986
|
-
|
|
59987
|
-
|
|
60494
|
+
async ({ checkin_id, confirmToken }, ctx) => {
|
|
60495
|
+
const request = { method: "POST", path: `/checkin/delete/${checkin_id}` };
|
|
60496
|
+
const gate = await confirmWrite(ctx, {
|
|
60497
|
+
tool: "untappd_delete_checkin",
|
|
60498
|
+
action: "untappd.delete_checkin",
|
|
60499
|
+
message: "Review and confirm PERMANENTLY deleting this Untappd check-in:",
|
|
60500
|
+
confirmToken,
|
|
60501
|
+
target: checkin_id,
|
|
60502
|
+
payload: request,
|
|
60503
|
+
preview: {
|
|
59988
60504
|
action: "delete_checkin",
|
|
59989
60505
|
checkin_id,
|
|
59990
|
-
|
|
59991
|
-
|
|
59992
|
-
|
|
60506
|
+
...request,
|
|
60507
|
+
note: "PERMANENTLY deletes this check-in. This cannot be undone."
|
|
60508
|
+
}
|
|
60509
|
+
});
|
|
60510
|
+
if (gate) return gate;
|
|
59993
60511
|
const data = await client2.write("POST", `/checkin/delete/${checkin_id}`);
|
|
59994
60512
|
return minifiedResult({ deleted: true, checkin_id, result: data?.result });
|
|
59995
60513
|
}
|
|
@@ -59998,7 +60516,7 @@ function registerCheckinTools(server, client2) {
|
|
|
59998
60516
|
"untappd_checkin",
|
|
59999
60517
|
{
|
|
60000
60518
|
title: "Check in a beer on Untappd",
|
|
60001
|
-
description:
|
|
60519
|
+
description: `Post a NEW beer check-in to YOUR Untappd account \u2014 this publishes to your public feed. Provide the beer id (bid) from untappd_search_beer; optionally a rating (0\u20135 in 0.25 steps), a shout (comment), a venue via foursquare_id, and a local photo via photo_path (JPEG/PNG). The preview shows the exact fields and photo that will be posted. ${CONFIRM_FLOW}`,
|
|
60002
60520
|
annotations: toolAnnotations({ title: "Check in a beer on Untappd", readOnly: false, idempotent: false, openWorld: true, destructive: false }),
|
|
60003
60521
|
inputSchema: external_exports.object({
|
|
60004
60522
|
bid: external_exports.number().int().positive().describe("Untappd beer id to check in (from untappd_search_beer)"),
|
|
@@ -60006,7 +60524,7 @@ function registerCheckinTools(server, client2) {
|
|
|
60006
60524
|
shout: external_exports.string().max(2e3).optional().describe("Optional shout / comment text for the check-in"),
|
|
60007
60525
|
foursquare_id: external_exports.string().optional().describe("Optional Foursquare venue id to tag the check-in location"),
|
|
60008
60526
|
photo_path: external_exports.string().optional().describe(
|
|
60009
|
-
"Optional path to a local JPEG/PNG photo (max 15 MB) to attach \u2014 it is published publicly. Only use a file the user explicitly chose; the
|
|
60527
|
+
"Optional path to a local JPEG/PNG photo (max 15 MB) to attach \u2014 it is published publicly. Only use a file the user explicitly chose; the preview shows the resolved path and size for them to confirm."
|
|
60010
60528
|
),
|
|
60011
60529
|
geolat: external_exports.number().optional().describe("Optional latitude of the check-in"),
|
|
60012
60530
|
geolng: external_exports.number().optional().describe("Optional longitude of the check-in"),
|
|
@@ -60014,12 +60532,13 @@ function registerCheckinTools(server, client2) {
|
|
|
60014
60532
|
timezone: external_exports.string().min(1).optional().describe(
|
|
60015
60533
|
"The drinker's IANA timezone (e.g. America/New_York), which sets the check-in's local time. Defaults to UNTAPPD_TIMEZONE, else the server's own zone \u2014 which on a hosted connector is usually UTC, so pass it when you know where the user is."
|
|
60016
60534
|
),
|
|
60017
|
-
|
|
60535
|
+
confirmToken: confirmTokenParam
|
|
60018
60536
|
})
|
|
60019
60537
|
},
|
|
60020
|
-
async ({ bid, rating, shout, foursquare_id, photo_path, geolat, geolng, container_id, timezone: requestedTz,
|
|
60538
|
+
async ({ bid, rating, shout, foursquare_id, photo_path, geolat, geolng, container_id, timezone: requestedTz, confirmToken }, ctx) => {
|
|
60021
60539
|
const { timezone, gmt_offset } = checkinTimezone(requestedTz);
|
|
60022
60540
|
const photo = photo_path !== void 0 ? await checkPhoto(photo_path) : void 0;
|
|
60541
|
+
const photo_sha256 = photo ? await photoSha256(photo.path) : void 0;
|
|
60023
60542
|
const form = {
|
|
60024
60543
|
bid,
|
|
60025
60544
|
rating: rating !== void 0 ? rating.toFixed(2) : void 0,
|
|
@@ -60034,15 +60553,22 @@ function registerCheckinTools(server, client2) {
|
|
|
60034
60553
|
photo_file_ext: photo?.ext,
|
|
60035
60554
|
platform: "ios"
|
|
60036
60555
|
};
|
|
60037
|
-
|
|
60038
|
-
|
|
60039
|
-
|
|
60556
|
+
const request = { method: "POST", path: "/checkin/add", form };
|
|
60557
|
+
const gate = await confirmWrite(ctx, {
|
|
60558
|
+
tool: "untappd_checkin",
|
|
60559
|
+
action: "untappd.checkin",
|
|
60560
|
+
message: "Review and confirm posting this check-in to your public Untappd feed:",
|
|
60561
|
+
confirmToken,
|
|
60562
|
+
target: bid,
|
|
60563
|
+
payload: { ...request, photo, photo_sha256 },
|
|
60564
|
+
preview: {
|
|
60040
60565
|
action: "checkin",
|
|
60041
|
-
|
|
60566
|
+
...request,
|
|
60042
60567
|
photo: photo ? { ...photo, note: "this exact file will be uploaded PUBLICLY after the check-in is created" } : void 0,
|
|
60043
|
-
note: "
|
|
60044
|
-
}
|
|
60045
|
-
}
|
|
60568
|
+
note: "POSTs this check-in to your public Untappd feed."
|
|
60569
|
+
}
|
|
60570
|
+
});
|
|
60571
|
+
if (gate) return gate;
|
|
60046
60572
|
let blob;
|
|
60047
60573
|
if (photo) {
|
|
60048
60574
|
blob = await fileBlob(photo.path, { maxBytes: MAX_PHOTO_BYTES, label: "Photo", allowedRoots: photoRoots() });
|
|
@@ -60787,11 +61313,48 @@ function registerCacheTools(server, client2, cacheProvider) {
|
|
|
60787
61313
|
});
|
|
60788
61314
|
}
|
|
60789
61315
|
);
|
|
61316
|
+
server.registerTool(
|
|
61317
|
+
"untappd_cache_forget",
|
|
61318
|
+
{
|
|
61319
|
+
title: "Forget a user's cached check-ins",
|
|
61320
|
+
description: `Delete everything the LOCAL cache holds for one user \u2014 their cached check-ins (beer, rating, comment, venue, date), distinct-beers list and sync state \u2014 e.g. after syncing a friend you no longer want a history of. Only the local cache is touched; nothing on Untappd changes, and a later sync can re-fetch it. Shared beer metadata is kept. The preview shows the username and exactly how many rows will be removed. Omit username for your own account. ${CONFIRM_FLOW}`,
|
|
61321
|
+
annotations: toolAnnotations({ title: "Forget a user's cached check-ins", readOnly: false, idempotent: true, openWorld: false, destructive: true }),
|
|
61322
|
+
inputSchema: external_exports.object({
|
|
61323
|
+
username: UsernameArg2,
|
|
61324
|
+
confirmToken: confirmTokenParam
|
|
61325
|
+
})
|
|
61326
|
+
},
|
|
61327
|
+
async ({ username, confirmToken }, ctx) => {
|
|
61328
|
+
const user = resolveUser2(username, client2.loginName);
|
|
61329
|
+
const cache = cacheProvider();
|
|
61330
|
+
const counts = {
|
|
61331
|
+
checkins: await cache.cachedCount(user),
|
|
61332
|
+
distinct_beers: await cache.distinctBeersCount(user)
|
|
61333
|
+
};
|
|
61334
|
+
const gate = await confirmWrite(ctx, {
|
|
61335
|
+
tool: "untappd_cache_forget",
|
|
61336
|
+
action: "untappd.cache_forget",
|
|
61337
|
+
message: `Review and confirm deleting ${user}'s data from the local Untappd cache:`,
|
|
61338
|
+
confirmToken,
|
|
61339
|
+
target: user.toLowerCase(),
|
|
61340
|
+
payload: { username: user.toLowerCase(), ...counts },
|
|
61341
|
+
preview: {
|
|
61342
|
+
action: "cache_forget",
|
|
61343
|
+
username: user,
|
|
61344
|
+
...counts,
|
|
61345
|
+
note: "Deletes these rows from the LOCAL cache only; Untappd itself is unchanged and a re-sync can restore them."
|
|
61346
|
+
}
|
|
61347
|
+
});
|
|
61348
|
+
if (gate) return gate;
|
|
61349
|
+
const removed = await cache.forgetUser(user);
|
|
61350
|
+
return minifiedResult({ forgotten: true, username: user, removed });
|
|
61351
|
+
}
|
|
61352
|
+
);
|
|
60790
61353
|
}
|
|
60791
61354
|
|
|
60792
61355
|
// src/cache/db.ts
|
|
60793
61356
|
import { DatabaseSync } from "node:sqlite";
|
|
60794
|
-
import { mkdirSync } from "node:fs";
|
|
61357
|
+
import { chmodSync, closeSync, existsSync as existsSync2, mkdirSync, openSync } from "node:fs";
|
|
60795
61358
|
import { dirname as dirname3, join as join3 } from "node:path";
|
|
60796
61359
|
import { homedir as homedir2 } from "node:os";
|
|
60797
61360
|
var NodeSqlDriver = class {
|
|
@@ -60828,15 +61391,39 @@ var CheckinCache = class _CheckinCache extends LocalCacheStore {
|
|
|
60828
61391
|
this.db = db;
|
|
60829
61392
|
}
|
|
60830
61393
|
db;
|
|
60831
|
-
/**
|
|
61394
|
+
/**
|
|
61395
|
+
* Open (creating parent dirs) a file-backed cache, or `:memory:` for tests.
|
|
61396
|
+
* The file holds other people's dated check-in and venue history, so it is
|
|
61397
|
+
* kept owner-only: dirs 0700, the db (and any -wal/-shm/-journal) 0600.
|
|
61398
|
+
*/
|
|
60832
61399
|
static open(path) {
|
|
60833
|
-
if (path !== ":memory:")
|
|
60834
|
-
|
|
61400
|
+
if (path !== ":memory:") preparePrivateFile(path);
|
|
61401
|
+
const cache = new _CheckinCache(new DatabaseSync(path));
|
|
61402
|
+
if (path !== ":memory:") tightenSidecars(path);
|
|
61403
|
+
return cache;
|
|
60835
61404
|
}
|
|
60836
61405
|
close() {
|
|
60837
61406
|
this.db.close();
|
|
60838
61407
|
}
|
|
60839
61408
|
};
|
|
61409
|
+
function preparePrivateFile(path) {
|
|
61410
|
+
const dir = dirname3(path);
|
|
61411
|
+
mkdirSync(dir, { recursive: true, mode: 448 });
|
|
61412
|
+
bestEffortChmod(dir, 448);
|
|
61413
|
+
if (!existsSync2(path)) closeSync(openSync(path, "a", 384));
|
|
61414
|
+
bestEffortChmod(path, 384);
|
|
61415
|
+
}
|
|
61416
|
+
function tightenSidecars(path) {
|
|
61417
|
+
for (const suffix of ["-wal", "-shm", "-journal"]) {
|
|
61418
|
+
if (existsSync2(path + suffix)) bestEffortChmod(path + suffix, 384);
|
|
61419
|
+
}
|
|
61420
|
+
}
|
|
61421
|
+
function bestEffortChmod(path, mode) {
|
|
61422
|
+
try {
|
|
61423
|
+
chmodSync(path, mode);
|
|
61424
|
+
} catch {
|
|
61425
|
+
}
|
|
61426
|
+
}
|
|
60840
61427
|
function defaultCachePath() {
|
|
60841
61428
|
return readEnvVar("UNTAPPD_CACHE_DB") ?? join3(homedir2(), ".untappd-mcp", "checkins.db");
|
|
60842
61429
|
}
|