@de-otio/epimethian-mcp 6.7.0 → 6.8.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/README.md +1 -1
- package/dist/cli/index.js +697 -141
- package/dist/cli/index.js.map +4 -4
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -35481,7 +35481,7 @@ async function getPage(pageId, includeBody) {
|
|
|
35481
35481
|
async function _rawCreatePage(spaceId, title, body, parentId, clientLabel) {
|
|
35482
35482
|
const cfg = await getConfig();
|
|
35483
35483
|
const pageBody = normalizeBodyForSubmit(body);
|
|
35484
|
-
const epimethianTag = `Epimethian v${"6.
|
|
35484
|
+
const epimethianTag = `Epimethian v${"6.8.0"}`;
|
|
35485
35485
|
const versionMsg = cfg.attribution && clientLabel ? `Created by ${clientLabel} (via ${epimethianTag})` : `Created by ${epimethianTag}`;
|
|
35486
35486
|
const payload = {
|
|
35487
35487
|
title,
|
|
@@ -35502,7 +35502,7 @@ async function _rawCreatePage(spaceId, title, body, parentId, clientLabel) {
|
|
|
35502
35502
|
async function _rawUpdatePage(pageId, opts) {
|
|
35503
35503
|
const cfg = await getConfig();
|
|
35504
35504
|
const newVersion = opts.version + 1;
|
|
35505
|
-
const epimethianTag = `Epimethian v${"6.
|
|
35505
|
+
const epimethianTag = `Epimethian v${"6.8.0"}`;
|
|
35506
35506
|
const effectiveClient = cfg.attribution ? opts.clientLabel : void 0;
|
|
35507
35507
|
let versionMessage;
|
|
35508
35508
|
if (opts.versionMessage && effectiveClient)
|
|
@@ -36879,6 +36879,295 @@ var init_confirmation_tokens = __esm({
|
|
|
36879
36879
|
}
|
|
36880
36880
|
});
|
|
36881
36881
|
|
|
36882
|
+
// src/server/batch-tokens.ts
|
|
36883
|
+
function clampTtlSeconds(seconds) {
|
|
36884
|
+
if (!Number.isFinite(seconds)) return DEFAULT_BATCH_TTL_SECONDS;
|
|
36885
|
+
if (seconds < TTL_MIN_SECONDS) return TTL_MIN_SECONDS;
|
|
36886
|
+
if (seconds > TTL_MAX_SECONDS) return TTL_MAX_SECONDS;
|
|
36887
|
+
return Math.floor(seconds);
|
|
36888
|
+
}
|
|
36889
|
+
function getMintLimit2() {
|
|
36890
|
+
const raw = process.env.EPIMETHIAN_BATCH_MINT_LIMIT;
|
|
36891
|
+
if (raw === void 0) return MAX_BATCH_MINTS_PER_15_MIN;
|
|
36892
|
+
const n = parseInt(raw, 10);
|
|
36893
|
+
if (!Number.isFinite(n) || n < 0) return MAX_BATCH_MINTS_PER_15_MIN;
|
|
36894
|
+
return n;
|
|
36895
|
+
}
|
|
36896
|
+
function emitMint2(meta) {
|
|
36897
|
+
for (const h of mintHandlers2) {
|
|
36898
|
+
try {
|
|
36899
|
+
h(meta);
|
|
36900
|
+
} catch {
|
|
36901
|
+
}
|
|
36902
|
+
}
|
|
36903
|
+
}
|
|
36904
|
+
function emitValidate2(meta) {
|
|
36905
|
+
for (const h of validateHandlers2) {
|
|
36906
|
+
try {
|
|
36907
|
+
h(meta);
|
|
36908
|
+
} catch {
|
|
36909
|
+
}
|
|
36910
|
+
}
|
|
36911
|
+
}
|
|
36912
|
+
function emitRefund(meta) {
|
|
36913
|
+
for (const h of refundHandlers) {
|
|
36914
|
+
try {
|
|
36915
|
+
h(meta);
|
|
36916
|
+
} catch {
|
|
36917
|
+
}
|
|
36918
|
+
}
|
|
36919
|
+
}
|
|
36920
|
+
function sleepUntil2(targetWallClockMs) {
|
|
36921
|
+
return new Promise((resolve2) => {
|
|
36922
|
+
const remaining = targetWallClockMs - Date.now();
|
|
36923
|
+
if (remaining <= 0) {
|
|
36924
|
+
resolve2();
|
|
36925
|
+
return;
|
|
36926
|
+
}
|
|
36927
|
+
setTimeout(resolve2, remaining);
|
|
36928
|
+
});
|
|
36929
|
+
}
|
|
36930
|
+
function pruneMintTimestamps2(now) {
|
|
36931
|
+
const cutoff = now - MINT_WINDOW_MS2;
|
|
36932
|
+
if (mintTimestamps2.length === 0) return;
|
|
36933
|
+
if (mintTimestamps2[0] >= cutoff) return;
|
|
36934
|
+
mintTimestamps2 = mintTimestamps2.filter((ts) => ts >= cutoff);
|
|
36935
|
+
}
|
|
36936
|
+
function evictOldest2() {
|
|
36937
|
+
let oldestKey;
|
|
36938
|
+
let oldestSeq = Infinity;
|
|
36939
|
+
for (const [k, v] of store2.entries()) {
|
|
36940
|
+
if (v.insertSeq < oldestSeq) {
|
|
36941
|
+
oldestSeq = v.insertSeq;
|
|
36942
|
+
oldestKey = k;
|
|
36943
|
+
}
|
|
36944
|
+
}
|
|
36945
|
+
if (oldestKey === void 0) return;
|
|
36946
|
+
const entry = store2.get(oldestKey);
|
|
36947
|
+
store2.delete(oldestKey);
|
|
36948
|
+
for (const reservationId of entry.reservations.keys()) {
|
|
36949
|
+
reservationIndex.delete(reservationId);
|
|
36950
|
+
}
|
|
36951
|
+
emitValidate2({
|
|
36952
|
+
auditId: entry.auditId,
|
|
36953
|
+
cloudId: entry.cloudId,
|
|
36954
|
+
pageId: entry.pageIdsList[0] ?? "",
|
|
36955
|
+
outcome: "evicted"
|
|
36956
|
+
});
|
|
36957
|
+
}
|
|
36958
|
+
function mintBatchToken(args) {
|
|
36959
|
+
if (typeof args.cloudId !== "string" || args.cloudId.length === 0) {
|
|
36960
|
+
throw new Error("mintBatchToken: cloudId is required and must be a non-empty string.");
|
|
36961
|
+
}
|
|
36962
|
+
if (!Array.isArray(args.pageIds) || args.pageIds.length === 0) {
|
|
36963
|
+
throw new Error("mintBatchToken: pageIds must be a non-empty array.");
|
|
36964
|
+
}
|
|
36965
|
+
const seen = /* @__PURE__ */ new Set();
|
|
36966
|
+
const dedupedPageIds = [];
|
|
36967
|
+
for (const id of args.pageIds) {
|
|
36968
|
+
if (typeof id !== "string" || id.length === 0) {
|
|
36969
|
+
throw new Error("mintBatchToken: every page_id must be a non-empty string.");
|
|
36970
|
+
}
|
|
36971
|
+
if (!seen.has(id)) {
|
|
36972
|
+
seen.add(id);
|
|
36973
|
+
dedupedPageIds.push(id);
|
|
36974
|
+
}
|
|
36975
|
+
}
|
|
36976
|
+
if (dedupedPageIds.length > MAX_PAGE_IDS_PER_BATCH) {
|
|
36977
|
+
throw new Error(
|
|
36978
|
+
`mintBatchToken: page_ids list exceeds MAX_PAGE_IDS_PER_BATCH=${MAX_PAGE_IDS_PER_BATCH}.`
|
|
36979
|
+
);
|
|
36980
|
+
}
|
|
36981
|
+
const ttlSeconds = clampTtlSeconds(args.ttlSeconds ?? DEFAULT_BATCH_TTL_SECONDS);
|
|
36982
|
+
const ttlMs = ttlSeconds * 1e3;
|
|
36983
|
+
const N = dedupedPageIds.length;
|
|
36984
|
+
const requestedMax = args.maxOperations === void 0 ? N : Math.floor(args.maxOperations);
|
|
36985
|
+
if (!Number.isFinite(requestedMax) || requestedMax < 1) {
|
|
36986
|
+
throw new Error("mintBatchToken: max_operations must be a positive integer.");
|
|
36987
|
+
}
|
|
36988
|
+
const maxOperations = Math.min(requestedMax, N * 2);
|
|
36989
|
+
const now = Date.now();
|
|
36990
|
+
pruneMintTimestamps2(now);
|
|
36991
|
+
const limit = getMintLimit2();
|
|
36992
|
+
if (limit > 0 && mintTimestamps2.length >= limit) {
|
|
36993
|
+
const oldest = mintTimestamps2[0];
|
|
36994
|
+
const waitMs = Math.max(0, oldest + MINT_WINDOW_MS2 - now);
|
|
36995
|
+
throw new BatchMintRateLimitedError(mintTimestamps2.length, limit, waitMs);
|
|
36996
|
+
}
|
|
36997
|
+
while (store2.size >= MAX_OUTSTANDING_BATCH_TOKENS) {
|
|
36998
|
+
evictOldest2();
|
|
36999
|
+
}
|
|
37000
|
+
const expiresAt = now + ttlMs;
|
|
37001
|
+
const auditId = (0, import_node_crypto5.randomUUID)();
|
|
37002
|
+
const tokenStr = `btk_${(0, import_node_crypto5.randomBytes)(32).toString("hex")}`;
|
|
37003
|
+
const entry = {
|
|
37004
|
+
auditId,
|
|
37005
|
+
cloudId: args.cloudId,
|
|
37006
|
+
pageIds: new Set(dedupedPageIds),
|
|
37007
|
+
pageIdsList: dedupedPageIds,
|
|
37008
|
+
expiresAt,
|
|
37009
|
+
maxOperations,
|
|
37010
|
+
remainingOperations: maxOperations,
|
|
37011
|
+
reservations: /* @__PURE__ */ new Map(),
|
|
37012
|
+
insertSeq: ++insertSeqCounter2
|
|
37013
|
+
};
|
|
37014
|
+
store2.set(tokenStr, entry);
|
|
37015
|
+
mintTimestamps2.push(now);
|
|
37016
|
+
emitMint2({
|
|
37017
|
+
auditId,
|
|
37018
|
+
cloudId: args.cloudId,
|
|
37019
|
+
pageIds: dedupedPageIds,
|
|
37020
|
+
ttlMs,
|
|
37021
|
+
maxOperations,
|
|
37022
|
+
expiresAt,
|
|
37023
|
+
outstanding: store2.size
|
|
37024
|
+
});
|
|
37025
|
+
return {
|
|
37026
|
+
token: tokenStr,
|
|
37027
|
+
auditId,
|
|
37028
|
+
expiresAt,
|
|
37029
|
+
authorisedPageIds: dedupedPageIds,
|
|
37030
|
+
remainingOperations: maxOperations
|
|
37031
|
+
};
|
|
37032
|
+
}
|
|
37033
|
+
async function validateBatchToken(args) {
|
|
37034
|
+
const floorTarget = Date.now() + MIN_VALIDATE_FLOOR_MS2;
|
|
37035
|
+
let outcome;
|
|
37036
|
+
let auditId;
|
|
37037
|
+
let result = { valid: false };
|
|
37038
|
+
let remainingAfter;
|
|
37039
|
+
const entry = store2.get(args.token);
|
|
37040
|
+
if (!entry) {
|
|
37041
|
+
outcome = "unknown";
|
|
37042
|
+
} else {
|
|
37043
|
+
auditId = entry.auditId;
|
|
37044
|
+
const now = Date.now();
|
|
37045
|
+
if (now >= entry.expiresAt) {
|
|
37046
|
+
store2.delete(args.token);
|
|
37047
|
+
for (const reservationId of entry.reservations.keys()) {
|
|
37048
|
+
reservationIndex.delete(reservationId);
|
|
37049
|
+
}
|
|
37050
|
+
outcome = "expired";
|
|
37051
|
+
} else if (entry.cloudId !== args.cloudId) {
|
|
37052
|
+
outcome = "cloudid_mismatch";
|
|
37053
|
+
} else if (!entry.pageIds.has(args.pageId)) {
|
|
37054
|
+
outcome = "page_id_not_authorised";
|
|
37055
|
+
} else if (entry.remainingOperations <= 0) {
|
|
37056
|
+
outcome = "exhausted";
|
|
37057
|
+
} else {
|
|
37058
|
+
entry.remainingOperations -= 1;
|
|
37059
|
+
remainingAfter = entry.remainingOperations;
|
|
37060
|
+
const reservationId = (0, import_node_crypto5.randomUUID)();
|
|
37061
|
+
entry.reservations.set(reservationId, {
|
|
37062
|
+
pageId: args.pageId,
|
|
37063
|
+
refunded: false
|
|
37064
|
+
});
|
|
37065
|
+
reservationIndex.set(reservationId, args.token);
|
|
37066
|
+
outcome = "ok";
|
|
37067
|
+
result = { valid: true, reservationId };
|
|
37068
|
+
}
|
|
37069
|
+
}
|
|
37070
|
+
emitValidate2({
|
|
37071
|
+
auditId,
|
|
37072
|
+
cloudId: args.cloudId,
|
|
37073
|
+
pageId: args.pageId,
|
|
37074
|
+
outcome,
|
|
37075
|
+
...remainingAfter !== void 0 ? { remainingAfter } : {}
|
|
37076
|
+
});
|
|
37077
|
+
await sleepUntil2(floorTarget);
|
|
37078
|
+
return result;
|
|
37079
|
+
}
|
|
37080
|
+
function refundReservation(reservationId) {
|
|
37081
|
+
const tokenStr = reservationIndex.get(reservationId);
|
|
37082
|
+
if (tokenStr === void 0) return false;
|
|
37083
|
+
const entry = store2.get(tokenStr);
|
|
37084
|
+
if (!entry) {
|
|
37085
|
+
reservationIndex.delete(reservationId);
|
|
37086
|
+
return false;
|
|
37087
|
+
}
|
|
37088
|
+
const reservation = entry.reservations.get(reservationId);
|
|
37089
|
+
if (!reservation) return false;
|
|
37090
|
+
if (reservation.refunded) {
|
|
37091
|
+
emitRefund({
|
|
37092
|
+
auditId: entry.auditId,
|
|
37093
|
+
cloudId: entry.cloudId,
|
|
37094
|
+
pageId: reservation.pageId,
|
|
37095
|
+
refunded: false,
|
|
37096
|
+
remainingAfter: entry.remainingOperations
|
|
37097
|
+
});
|
|
37098
|
+
return false;
|
|
37099
|
+
}
|
|
37100
|
+
reservation.refunded = true;
|
|
37101
|
+
entry.remainingOperations += 1;
|
|
37102
|
+
emitRefund({
|
|
37103
|
+
auditId: entry.auditId,
|
|
37104
|
+
cloudId: entry.cloudId,
|
|
37105
|
+
pageId: reservation.pageId,
|
|
37106
|
+
refunded: true,
|
|
37107
|
+
remainingAfter: entry.remainingOperations
|
|
37108
|
+
});
|
|
37109
|
+
return true;
|
|
37110
|
+
}
|
|
37111
|
+
function finaliseReservation(reservationId) {
|
|
37112
|
+
const tokenStr = reservationIndex.get(reservationId);
|
|
37113
|
+
reservationIndex.delete(reservationId);
|
|
37114
|
+
if (tokenStr === void 0) return;
|
|
37115
|
+
const entry = store2.get(tokenStr);
|
|
37116
|
+
if (!entry) return;
|
|
37117
|
+
entry.reservations.delete(reservationId);
|
|
37118
|
+
}
|
|
37119
|
+
function isBatchTokenShape(token) {
|
|
37120
|
+
if (typeof token !== "string") return false;
|
|
37121
|
+
if (!token.startsWith("btk_")) return false;
|
|
37122
|
+
if (token.length !== "btk_".length + 64) return false;
|
|
37123
|
+
for (let i = 4; i < token.length; i++) {
|
|
37124
|
+
const c = token.charCodeAt(i);
|
|
37125
|
+
const isHex = c >= 48 && c <= 57 || // 0-9
|
|
37126
|
+
c >= 97 && c <= 102 || // a-f
|
|
37127
|
+
c >= 65 && c <= 70;
|
|
37128
|
+
if (!isHex) return false;
|
|
37129
|
+
}
|
|
37130
|
+
return true;
|
|
37131
|
+
}
|
|
37132
|
+
var import_node_crypto5, DEFAULT_BATCH_TTL_SECONDS, TTL_MIN_SECONDS, TTL_MAX_SECONDS, MAX_PAGE_IDS_PER_BATCH, MAX_OUTSTANDING_BATCH_TOKENS, MAX_BATCH_MINTS_PER_15_MIN, MINT_WINDOW_MS2, MIN_VALIDATE_FLOOR_MS2, BATCH_MINT_RATE_LIMITED, BatchMintRateLimitedError, store2, reservationIndex, mintTimestamps2, insertSeqCounter2, mintHandlers2, validateHandlers2, refundHandlers;
|
|
37133
|
+
var init_batch_tokens = __esm({
|
|
37134
|
+
"src/server/batch-tokens.ts"() {
|
|
37135
|
+
"use strict";
|
|
37136
|
+
import_node_crypto5 = require("node:crypto");
|
|
37137
|
+
DEFAULT_BATCH_TTL_SECONDS = 15 * 60;
|
|
37138
|
+
TTL_MIN_SECONDS = 60;
|
|
37139
|
+
TTL_MAX_SECONDS = 60 * 60;
|
|
37140
|
+
MAX_PAGE_IDS_PER_BATCH = 50;
|
|
37141
|
+
MAX_OUTSTANDING_BATCH_TOKENS = 25;
|
|
37142
|
+
MAX_BATCH_MINTS_PER_15_MIN = 25;
|
|
37143
|
+
MINT_WINDOW_MS2 = 15 * 60 * 1e3;
|
|
37144
|
+
MIN_VALIDATE_FLOOR_MS2 = 5;
|
|
37145
|
+
BATCH_MINT_RATE_LIMITED = "BATCH_MINT_RATE_LIMITED";
|
|
37146
|
+
BatchMintRateLimitedError = class extends Error {
|
|
37147
|
+
code = BATCH_MINT_RATE_LIMITED;
|
|
37148
|
+
current;
|
|
37149
|
+
limit;
|
|
37150
|
+
waitMs;
|
|
37151
|
+
constructor(current, limit, waitMs) {
|
|
37152
|
+
super(
|
|
37153
|
+
`Batch authorisation mint cap exhausted: ${current} mints in the last 15 min, limit ${limit}. Window opens again in ~${Math.ceil(waitMs / 6e4)} min. Override via EPIMETHIAN_BATCH_MINT_LIMIT (set "0" to disable).`
|
|
37154
|
+
);
|
|
37155
|
+
this.name = "BatchMintRateLimitedError";
|
|
37156
|
+
this.current = current;
|
|
37157
|
+
this.limit = limit;
|
|
37158
|
+
this.waitMs = waitMs;
|
|
37159
|
+
}
|
|
37160
|
+
};
|
|
37161
|
+
store2 = /* @__PURE__ */ new Map();
|
|
37162
|
+
reservationIndex = /* @__PURE__ */ new Map();
|
|
37163
|
+
mintTimestamps2 = [];
|
|
37164
|
+
insertSeqCounter2 = 0;
|
|
37165
|
+
mintHandlers2 = [];
|
|
37166
|
+
validateHandlers2 = [];
|
|
37167
|
+
refundHandlers = [];
|
|
37168
|
+
}
|
|
37169
|
+
});
|
|
37170
|
+
|
|
36882
37171
|
// node_modules/mdurl/lib/decode.mjs
|
|
36883
37172
|
function getDecodeCache(exclude) {
|
|
36884
37173
|
let cache = decodeCache[exclude];
|
|
@@ -47901,6 +48190,21 @@ async function maybeConsumeConfirmToken(args) {
|
|
|
47901
48190
|
});
|
|
47902
48191
|
return outcome;
|
|
47903
48192
|
}
|
|
48193
|
+
async function tryBatchTokenForWrite(args) {
|
|
48194
|
+
const { batch_token, cloudId, pageId } = args;
|
|
48195
|
+
if (batch_token === void 0 || cloudId === void 0 || !isBatchTokenShape(batch_token)) {
|
|
48196
|
+
return { batchReservationId: void 0 };
|
|
48197
|
+
}
|
|
48198
|
+
const result = await validateBatchToken({
|
|
48199
|
+
token: batch_token,
|
|
48200
|
+
cloudId,
|
|
48201
|
+
pageId
|
|
48202
|
+
});
|
|
48203
|
+
if (result.valid) {
|
|
48204
|
+
return { batchReservationId: result.reservationId };
|
|
48205
|
+
}
|
|
48206
|
+
return { batchReservationId: void 0 };
|
|
48207
|
+
}
|
|
47904
48208
|
function formatSoftConfirmationResult(err, params) {
|
|
47905
48209
|
const last8 = err.token.slice(-8);
|
|
47906
48210
|
const isoExpires = new Date(err.expiresAt).toISOString();
|
|
@@ -47919,10 +48223,10 @@ Token tail: ...${last8} Expires: ${isoExpires} Audit ID: ${err.auditId}
|
|
|
47919
48223
|
The token is single-use, bound to this exact diff and page version,
|
|
47920
48224
|
and invalidated by any competing write to this page. If validation
|
|
47921
48225
|
fails, mint a new one by re-calling without \`confirm_token\`.`;
|
|
47922
|
-
const
|
|
47923
|
-
const finalText =
|
|
48226
|
+
const explicitlyHidden = process.env.EPIMETHIAN_HIDE_TOKEN_IN_TEXT === "true" || process.env.EPIMETHIAN_TOKEN_IN_TEXT === "false";
|
|
48227
|
+
const finalText = explicitlyHidden ? text2 : text2 + `
|
|
47924
48228
|
|
|
47925
|
-
[FALLBACK] Full token
|
|
48229
|
+
[FALLBACK] Full token: ${err.token}`;
|
|
47926
48230
|
const structuredContent = {
|
|
47927
48231
|
kind: "confirmation_required",
|
|
47928
48232
|
confirm_token: err.token,
|
|
@@ -48658,6 +48962,7 @@ var init_safe_write = __esm({
|
|
|
48658
48962
|
"use strict";
|
|
48659
48963
|
init_confluence_client();
|
|
48660
48964
|
init_confirmation_tokens();
|
|
48965
|
+
init_batch_tokens();
|
|
48661
48966
|
init_md_to_storage();
|
|
48662
48967
|
init_update_orchestrator();
|
|
48663
48968
|
init_content_safety_guards();
|
|
@@ -48785,7 +49090,7 @@ async function writeCheckState(state) {
|
|
|
48785
49090
|
const data = JSON.stringify(state, null, 2) + "\n";
|
|
48786
49091
|
const tmpFile = (0, import_node_path3.join)(
|
|
48787
49092
|
CONFIG_DIR2,
|
|
48788
|
-
`.update-check.${(0,
|
|
49093
|
+
`.update-check.${(0, import_node_crypto6.randomBytes)(4).toString("hex")}.tmp`
|
|
48789
49094
|
);
|
|
48790
49095
|
await (0, import_promises3.writeFile)(tmpFile, data, { mode: 384 });
|
|
48791
49096
|
await (0, import_promises3.rename)(tmpFile, UPDATE_CHECK_FILE);
|
|
@@ -48926,14 +49231,14 @@ async function checkForUpdates(currentVersion) {
|
|
|
48926
49231
|
return null;
|
|
48927
49232
|
}
|
|
48928
49233
|
}
|
|
48929
|
-
var import_promises3, import_node_path3, import_node_os2,
|
|
49234
|
+
var import_promises3, import_node_path3, import_node_os2, import_node_crypto6, import_node_child_process2, import_node_util, execFileAsync, CONFIG_DIR2, UPDATE_CHECK_FILE, ONE_DAY_MS, NPM_REGISTRY_URL, PACKAGE_NAME;
|
|
48930
49235
|
var init_update_check = __esm({
|
|
48931
49236
|
"src/shared/update-check.ts"() {
|
|
48932
49237
|
"use strict";
|
|
48933
49238
|
import_promises3 = require("node:fs/promises");
|
|
48934
49239
|
import_node_path3 = require("node:path");
|
|
48935
49240
|
import_node_os2 = require("node:os");
|
|
48936
|
-
|
|
49241
|
+
import_node_crypto6 = require("node:crypto");
|
|
48937
49242
|
import_node_child_process2 = require("node:child_process");
|
|
48938
49243
|
import_node_util = require("node:util");
|
|
48939
49244
|
init_safe_fs();
|
|
@@ -49022,7 +49327,7 @@ var init_client_configs = __esm({
|
|
|
49022
49327
|
null,
|
|
49023
49328
|
2
|
|
49024
49329
|
),
|
|
49025
|
-
warning: "VS Code extension \u2264 2.1.123 does not honour elicitation requests; if write tools fail with NO_USER_RESPONSE, set `EPIMETHIAN_BYPASS_ELICITATION=true`.\n\
|
|
49330
|
+
warning: "VS Code extension \u2264 2.1.123 does not honour elicitation requests; if write tools fail with NO_USER_RESPONSE, set `EPIMETHIAN_BYPASS_ELICITATION=true`.\n\nSoft-confirm tokens are surfaced in the text content by default (v6.7.2+) so Claude Code can read them even when it drops `structuredContent` (issue #15412). To opt out for a client that does forward `structuredContent` reliably, set `EPIMETHIAN_HIDE_TOKEN_IN_TEXT=true`."
|
|
49026
49331
|
},
|
|
49027
49332
|
{
|
|
49028
49333
|
id: "cursor",
|
|
@@ -49948,7 +50253,7 @@ Informational:
|
|
|
49948
50253
|
var install_agent_default;
|
|
49949
50254
|
var init_install_agent = __esm({
|
|
49950
50255
|
"install-agent.md"() {
|
|
49951
|
-
install_agent_default = '# Epimethian MCP - Agent Installation Guide\n\n> This document is for AI agents installing and configuring the Epimethian MCP server on behalf of a user.\n\n## Verification\n\nThe official package is **`@de-otio/epimethian-mcp`** (scoped under `@de-otio`).\n\nDo NOT install unscoped alternatives like `epimethian-mcp` \u2014 these are unofficial and potentially unsafe.\n\n## Prerequisites\n\n- Node.js 18 or later\n- npm on PATH\n\n## Step 1: Install\n\n```bash\nnpm install -g @de-otio/epimethian-mcp\n```\n\nVerify the installation:\n\n```bash\nepimethian-mcp --version 2>/dev/null || which epimethian-mcp\n```\n\n## Step 2: Resolve the absolute path\n\nIDE-hosted agents may not resolve PATH correctly. Always use the absolute path in MCP configuration:\n\n```bash\nwhich epimethian-mcp\n```\n\nUse the output as the `command` value in the MCP config below.\n\n## Step 3: Collect configuration\n\nAsk the user for:\n1. **Profile name** \u2014 a short identifier for this Confluence instance (e.g., `globex`, `acme-corp`). Lowercase alphanumeric and hyphens only.\n2. **Confluence Cloud URL** \u2014 e.g., `https://yoursite.atlassian.net`\n3. **Email address** \u2014 the email associated with their Atlassian account\n\n## Step 4: Write MCP configuration\n\nRun `epimethian-mcp setup --profile <name> --client <client-id>` after Step 5 (credential setup) \u2014 it prints the exact config snippet for your MCP host. Supported clients: `claude-code`, `claude-desktop`, `claude-code-vscode`, `cursor`, `windsurf`, `zed`, `opencode`. Keep the fallback hand-typed examples below for cases where the CLI is unavailable.\n\nAdd the server to the user\'s MCP client config. The exact file and shape depend on the client:\n\n**Claude Code, Claude Desktop, Cursor, Windsurf, Zed** \u2014 `.mcp.json` (or the\nequivalent client-specific config). The standard `mcpServers` shape:\n\n```json\n{\n "mcpServers": {\n "confluence": {\n "command": "<absolute path from Step 2>",\n "env": {\n "CONFLUENCE_PROFILE": "<profile name from Step 3>"\n }\n }\n }\n}\n```\n\n**OpenCode** \u2014 `opencode.json` at the project root or\n`~/.config/opencode/opencode.json`. Different shape (`mcp` block, `type:\n"local"`, `command` is an array, `environment` not `env`):\n\n```jsonc\n{\n "$schema": "https://opencode.ai/config.json",\n "mcp": {\n "confluence": {\n "type": "local",\n "command": ["<absolute path from Step 2>"],\n "enabled": true,\n "environment": {\n "CONFLUENCE_PROFILE": "<profile name from Step 3>",\n "EPIMETHIAN_ALLOW_UNGATED_WRITES": "true"\n }\n }\n }\n}\n```\n\nOpenCode does not support MCP elicitation (the in-protocol confirmation\nprompts), so write tools that fire the elicitation gate fail unless\n`EPIMETHIAN_ALLOW_UNGATED_WRITES=true` is set. See "MCP client\ncompatibility" below for the trade-off.\n\n**IMPORTANT:** The only required env var is `CONFLUENCE_PROFILE`. The URL,\nemail, and API token are stored securely in the OS keychain \u2014 they should\nNOT appear in config files.\n\n## Step 5: Credential setup\n\nTell the user to run this command in their terminal:\n\n```\nepimethian-mcp setup --profile <profile name from Step 3>\n```\n\nThis interactive command will:\n1. Prompt for the Confluence URL, email, and API token (masked input)\n2. Test the connection\n3. Store all credentials securely in the OS keychain under the named profile\n\nThe API token is generated at: https://id.atlassian.com/manage-profile/security/api-tokens\n\n**Do NOT ask the user for the API token yourself.** The token must go directly from the user into the interactive setup command to avoid appearing in conversation logs.\n\n## Step 6: User must restart the MCP client\n\n**IMPORTANT:** The user must restart their MCP client (e.g., restart Claude Code, reload VS Code, restart Claude Desktop) for the new server configuration to take effect. The MCP client reads `.mcp.json` at startup and does not detect changes while running.\n\nTell the user:\n> Please restart your MCP client now to activate the Confluence tools.\n\n## Step 7: Validation\n\nAfter the user restarts, verify the server is working by listing available Confluence tools or running a simple operation like listing spaces.\n\n## Adding Additional Tenants\n\nTo add a second Confluence instance (e.g., for a different customer):\n\n1. Run `epimethian-mcp setup --profile <new-profile-name>` with the new credentials\n2. In the project that uses the new tenant, update `.mcp.json` to set `CONFLUENCE_PROFILE` to the new profile name\n3. Restart the MCP client\n\nEach VS Code window / Claude Code session uses the profile specified in its `.mcp.json`. Profiles are fully isolated \u2014 different OS keychain entries, different Confluence instances.\n\n## Managing Profiles\n\n- List all profiles: `epimethian-mcp profiles`\n- Show details: `epimethian-mcp profiles --verbose`\n- Check connection: `CONFLUENCE_PROFILE=<name> epimethian-mcp status`\n- Set read-only: `epimethian-mcp profiles --set-read-only <name>`\n- Set read-write: `epimethian-mcp profiles --set-read-write <name>`\n\n### Read-Only Mode\n\nNew profiles default to **read-only**. When read-only, all write tools are blocked and return an error. To enable writes for a profile:\n\n```bash\nepimethian-mcp profiles --set-read-write <name>\n```\n\nOr during setup: `epimethian-mcp setup --profile <name> --read-write`\n\n**Important:** Restart any running MCP servers after changing the read-only flag.\n\n### Removing a Profile\n\nTo delete a profile and its credentials, run:\n\n```bash\nepimethian-mcp profiles --remove <name> --force\n```\n\n**Agents must pass `--force`** because the command normally prompts for interactive confirmation (`Remove profile "<name>" and delete its credentials? [y/N]`), which will fail in non-TTY environments like agent shell sessions. The `--force` flag skips the confirmation prompt when stdin is not a TTY.\n\nThis command:\n1. Deletes the credential entry (URL, email, API token) from the OS keychain\n2. Removes the profile from the registry at `~/.config/epimethian-mcp/profiles.json`\n3. Writes an entry to the audit log at `~/.config/epimethian-mcp/audit.log`\n\nAfter removing a profile, also remove or update any `.mcp.json` files that reference it \u2014 otherwise the MCP server will fail to start with a missing-profile error.\n\n**Errors:**\n- If the profile name is invalid (not matching lowercase alphanumeric/hyphens, 1\u201363 chars), the command exits with code 1\n- If the profile does not exist in the keychain, the keychain deletion is silently skipped \u2014 the registry entry is still removed\n\n## Accessing This Guide Post-Install\n\nOnce installed, this guide is available locally via:\n\n```bash\nepimethian-mcp agent-guide\n```\n\nThis prints the full agent guide to stdout \u2014 no web fetch required.\n\n## Uninstallation\n\nWhen a user asks to uninstall Epimethian MCP, follow these steps:\n\n### Step 1: Check for existing profiles\n\n```bash\nepimethian-mcp profiles\n```\n\n### Step 2: Ask the user about credential cleanup\n\nIf profiles exist, ask the user:\n\n> You have Epimethian profiles configured: [list the profile names]. Would you like to delete all stored credentials before uninstalling? (This removes API tokens from your OS keychain.)\n\n### Step 3: Delete credentials (if the user agrees)\n\nFor each profile the user wants removed:\n\n```bash\nepimethian-mcp profiles --remove <name> --force\n```\n\nOr to remove all profiles:\n\n```bash\nfor name in $(epimethian-mcp profiles | grep \'^ \'); do epimethian-mcp profiles --remove "$name" --force; done\n```\n\n### Step 4: Remove MCP configuration\n\nDelete the `confluence` entry (or the tenant-specific entry like `confluence-globex`) from the project\'s `.mcp.json`.\n\n### Step 5: Uninstall the package\n\n```bash\nnpm uninstall -g @de-otio/epimethian-mcp\n```\n\n### Step 6: Restart the MCP client\n\nTell the user to restart their MCP client so it stops trying to launch the removed server.\n\n## CI/CD (No Keychain)\n\nFor environments where the OS keychain is unavailable (Docker, CI), set all three env vars directly:\n\n```json\n{\n "mcpServers": {\n "confluence": {\n "command": "<absolute path>",\n "env": {\n "CONFLUENCE_URL": "<url>",\n "CONFLUENCE_EMAIL": "<email>",\n "CONFLUENCE_API_TOKEN": "<token>"\n }\n }\n }\n}\n```\n\n**Warning:** This exposes the API token in the process environment. Use profile-based auth whenever possible.\n\n## Troubleshooting\n\nIf **npm install fails**:\n- Verify Node.js 18+ is installed: `node --version`\n- Verify npm is on PATH: `npm --version`\n- If permission errors occur, the user may need to fix their npm prefix or use a Node version manager (nvm, fnm)\n\nIf **`epimethian-mcp setup` fails**:\n- "Connection failed": Verify the Confluence URL is correct and accessible\n- "Token is invalid or expired": The user needs to generate a new API token at https://id.atlassian.com/manage-profile/security/api-tokens\n- Keychain errors on Linux: The user may need to install `libsecret` / `gnome-keyring` (`apt install libsecret-tools` or equivalent)\n\nIf **the server doesn\'t appear after restart**:\n- Verify the `.mcp.json` path is correct for the user\'s MCP client\n- Verify the `command` value is an absolute path (run `which epimethian-mcp` to confirm)\n- Check that `.mcp.json` contains valid JSON (no trailing commas, correct quoting)\n\n## Write budget (safety cap on writes)\n\nepimethian-mcp enforces two write-rate caps per server process:\n\n- **Session cap** (default 250): total writes since the server started.\n- **Rolling cap** (default 75 per 15-minute window): catches bursts.\n\nThese are local safety nets, not Confluence limits. They exist because an\nautonomous agent in a retry loop or with a bad plan can issue hundreds of writes\nvery quickly, and most users would rather have a brief pause to confirm than\ndiscover the result an hour later.\n\n### What to do when you (the agent) hit `WRITE_BUDGET_EXCEEDED`\n\n1. **Stop and check.** Was the in-progress work user-requested and going as\n planned? If unsure, ask the user before continuing.\n2. **Explain to the user, in your own words:**\n - The safety budget has been hit (which scope, current vs. limit).\n - What the budget is for: a guard against runaway agents.\n - Whether the work-in-progress is legitimate (your judgement).\n - The two ways forward: wait for the rolling window to reopen, or raise the cap.\n3. **If the user wants to raise the cap**, give them this snippet to add to the\n `env` block of the epimethian-mcp entry in their MCP config (`.mcp.json` or\n equivalent \u2014 see Step 4 above for the layout):\n\n ```json\n "EPIMETHIAN_WRITE_BUDGET_ROLLING": "200",\n "EPIMETHIAN_WRITE_BUDGET_SESSION": "1000"\n ```\n\n Set either value to `"0"` to disable that scope. **Confirm with the user\n before recommending a raise** \u2014 the budget exists precisely to create a\n pause-and-check moment. The user must restart the MCP server (re-open the\n MCP client) for changes to take effect.\n4. **If the user gets a deprecation warning** about `EPIMETHIAN_WRITE_BUDGET_HOURLY`,\n tell them to rename it to `EPIMETHIAN_WRITE_BUDGET_ROLLING` in the same\n config file. The old name still works but will be removed in version 7.\n\n### Operator-side defaults\n\n- **`EPIMETHIAN_WRITE_BUDGET_SESSION`** \u2014 default 250; set to "0" to disable.\n- **`EPIMETHIAN_WRITE_BUDGET_ROLLING`** \u2014 default 75 per 15-minute window; set to "0" to disable.\n- **`EPIMETHIAN_WRITE_BUDGET_HOURLY`** \u2014 deprecated alias for `EPIMETHIAN_WRITE_BUDGET_ROLLING`; will be removed in version 7.\n\n## Soft confirmation (clients without working elicitation)\n\nSome MCP clients (OpenCode, the Claude Code VS Code extension, and\nothers) don\'t implement the in-protocol confirmation prompt \u2014 either\nthey don\'t advertise the capability, or they advertise it and never\nhonour the request (the SDK transport silently returns\n`{action: "decline"}` without showing the user a UI). Starting in\nv6.6.0, epimethian-mcp routes those confirmations through your\nagent\'s normal chat surface instead.\n\nThe implementation evolved across v6.6.0 \u2192 v6.6.3:\n\n- **v6.6.0** introduced soft elicitation for clients that don\'t\n *advertise* the capability \u2014 token-bound, single-use, diff-bound.\n- **v6.6.1** added **fast-decline auto-detection**: if the client\n advertises elicitation but the decline arrives in <50 ms (well below\n human reaction time), the session is flagged as fake and the call\n is re-routed through the soft-confirm path automatically. Threshold\n is overridable via `EPIMETHIAN_FAST_DECLINE_THRESHOLD_MS=<10..5000>`.\n No env-var configuration needed for the Claude Code VS Code\n extension\'s "fakes elicitation" bug \u2014 it Just Works.\n- **v6.6.2** declared `outputSchema` on every mutating tool so\n spec-compliant clients are obliged to forward `structuredContent`\n (where the token lives) to the agent. Added an opt-in\n `EPIMETHIAN_TOKEN_IN_TEXT=true` fallback that appends the full\n token to `content[0].text` for clients that drop `content` blocks\n on `isError: true` results (Claude Code issues #15412 / #9962 /\n #39976).\n- **v6.6.3** swapped the `outputSchema` from `z.discriminatedUnion`\n to `z.object` so the MCP SDK\'s `normalizeObjectSchema` (which only\n accepts schemas with `.shape`) can route the structured payload\n through `validateToolOutput` without throwing `_zod` undefined\n after the write commits. Hotfix; data-integrity-critical.\n\n### What you (the agent) see\n\nWhen a destructive write is requested against a client without working\nelicitation, the tool returns an error with a confirmation token:\n\n```\nisError: true\nstructuredContent:\n {\n "kind": "confirmation_required",\n "confirm_token": "<opaque token>",\n "audit_id": "<UUID for correlation>",\n "expires_at": "<ISO timestamp>",\n "page_id": "<pageId>",\n "human_summary": "<one-line description for the user>",\n "deletion_summary": { ... numeric counts only ... } // optional\n }\ncontent[0].text:\n \u26A0\uFE0F Confirmation required (SOFT_CONFIRMATION_REQUIRED)\n\n {human_summary}\n\n Please ask the user before retrying. If approved, re-call with the\n same parameters plus "confirm_token" from structuredContent.\n\n Token tail: ...<last 8 chars> Expires: <timestamp> Audit ID: <uuid>\n\n [FALLBACK] Full token (EPIMETHIAN_TOKEN_IN_TEXT=true): <full token>\n \u2190 only present when EPIMETHIAN_TOKEN_IN_TEXT=true is set\n```\n\nThe `kind` discriminator distinguishes this `"confirmation_required"`\narm from the success arm (`"written"` or `"deleted"`) on the same\ntool. Successful writes return:\n\n```\nstructuredContent:\n { "kind": "written", "page_id": "...", "new_version": 12, ... }\n```\n\n\u2026or `"kind": "deleted"` for `delete_page`.\n\n### What to do\n\n1. STOP. Don\'t retry blindly.\n2. Show the user, in their language, what\'s about to happen \u2014 use the\n `human_summary` field from `structuredContent`, or the\n human-readable text in `content[0].text` if your client doesn\'t\n forward `structuredContent`. **Never echo the token bytes to the\n user** \u2014 the token is meant to flow agent \u2192 server, not user \u2192 eye.\n3. Ask the user explicitly. Wait for their answer.\n4. If approved: re-call the tool with the SAME parameters plus\n `confirm_token`. Read the token from `structuredContent.confirm_token`\n when available; if your client doesn\'t surface that, the token\'s\n full bytes are in the `[FALLBACK] Full token: \u2026` line of\n `content[0].text` whenever `EPIMETHIAN_TOKEN_IN_TEXT=true` is set\n on the server. The 8-character "Token tail: \u2026" line in the prose\n is for human inspection only \u2014 it is **not** the token.\n5. If denied: tell the user the operation has been cancelled.\n\n### Token semantics\n\n- Single-use: a successful retry consumes the token. Replays fail.\n- 5-minute TTL by default.\n- Invalidated by any competing write to the same page (stale).\n- Bound to the specific diff and tenant: changing the body, page version, or\n tenant invalidates the token.\n\n### Operator opt-outs\n\nThese environment variables control soft confirmation behavior:\n\n- **`EPIMETHIAN_ALLOW_UNGATED_WRITES=true`** \u2014 bypasses soft confirmation\n entirely (no prompt; useful for headless / CI). Removes the\n human-in-the-loop gate; the harness\'s tool allow-list still applies.\n- **`EPIMETHIAN_DISABLE_SOFT_CONFIRM=true`** \u2014 keeps the legacy\n `ELICITATION_REQUIRED_BUT_UNAVAILABLE` failure mode for clients without\n elicitation support.\n- **`EPIMETHIAN_SOFT_CONFIRM_TTL_MS=300000`** \u2014 override the default 5-minute\n TTL (clamped to 60 seconds minimum, 15 minutes maximum).\n- **`EPIMETHIAN_SOFT_CONFIRM_MINT_LIMIT=100`** \u2014 override the per-15-minute\n mint cap (default 100; "0" disables the cap entirely).\n\n#### v6.6.1+ fast-decline auto-detection (Claude Code VS Code et al.)\n\n- **`EPIMETHIAN_TREAT_ELICITATION_AS_UNSUPPORTED=true`** \u2014 deterministic\n counterpart to fast-decline auto-detection. Use when your client is\n known to advertise elicitation but never honour it (e.g. the Claude\n Code VS Code extension \u2264 2.1.123) and you want to skip the timing\n probe on the first call. Distinct from `EPIMETHIAN_BYPASS_ELICITATION`:\n this routes through the soft-confirmation gate; bypass removes the\n gate entirely.\n- **`EPIMETHIAN_FAST_DECLINE_THRESHOLD_MS=<10..5000>`** \u2014 override the\n fast-decline threshold (default 50 ms). Raise this if a slow MCP\n transport is mis-classifying real declines as fake.\n- **`EPIMETHIAN_DISABLE_FAST_DECLINE_DETECTION=true`** \u2014 total\n off-switch for the auto-detection; restores exactly v6.6.0 behaviour.\n\n#### v6.6.2+ structured-content fallback\n\n- **`EPIMETHIAN_TOKEN_IN_TEXT=true`** \u2014 opt-in fallback for clients that\n drop `content` blocks on `isError: true` results, or that ignore the\n `outputSchema` declaration and never surface `structuredContent`\n to the agent. When set, the soft-confirm result text appends a\n `[FALLBACK] Full token (EPIMETHIAN_TOKEN_IN_TEXT=true): <token>`\n line so the agent can still extract the token. The structured\n payload is unchanged. Trade-off: the token is visible in the agent\n transcript (the security choice v6.6.0 explicitly avoided), so use\n only when needed. Today this is required for Claude Code (the VS\n Code extension and possibly the CLI) \u2014 see the per-client matrix\n below.\n\n### Multi-process deployments\n\nTokens are process-local in-memory. If you\'re running multiple MCP server\nprocesses for one tenant (e.g. a load-balanced fleet or separate processes\nper IDE window), a soft confirmation minted by process P1 will fail validation\nin process P2 (the load balancer routes the retry to a different process).\nThis is not a bug \u2014 it\'s the safe failure mode \u2014 but it means the user needs\nto mint a new token if the retry lands on a different process.\n\n**Recommendation:** Pin a single MCP server process per agent or IDE window.\nPre-seal profiles upgraded from versions before v5.5.0 must run `epimethian-mcp\nsetup` once to acquire a sealed cloudId before soft confirmation is available.\n\n## MCP client compatibility\n\nepimethian-mcp uses MCP **elicitation** (the in-protocol confirmation\nprompt added to MCP in 2025) as the human-in-the-loop gate for destructive\noperations. Different MCP clients support elicitation differently \u2014 some\nfully, some not at all, and some advertise the capability without honouring\nit. The compatibility matrix below tells you which env-var workaround to\nrecommend, if any.\n\n| Client | Elicitation? | What to do (v6.6.3+) |\n|---|---|---|\n| **Claude Code (CLI)** | Yes \u2014 full support | No special config needed. |\n| **Claude Desktop** | Yes \u2014 full support | No special config needed. |\n| **Claude Code VS Code extension** (all versions tested through 2.1.x) | Fakes it (advertises capability, never honours) | v6.6.1\'s fast-decline auto-detection routes the call through soft-confirm automatically. **Plus** set `EPIMETHIAN_TOKEN_IN_TEXT=true` in the server\'s env so the agent can read the full token from `content[0].text` \u2014 Claude Code does not currently surface `structuredContent` on `isError: true` responses (issue #15412). No `EPIMETHIAN_BYPASS_ELICITATION` needed; the gate works through the soft-confirm token flow. |\n| **OpenCode** | No \u2014 capability not advertised | v6.6.0+ soft-confirmation token flow is automatic when the client lacks elicitation. The Vercel AI SDK forwards `structuredContent` to the model when `outputSchema` is declared (which v6.6.2 added) \u2014 the agent should be able to read `confirm_token` directly. If your build of OpenCode/AI-SDK doesn\'t honour `outputSchema`, set `EPIMETHIAN_TOKEN_IN_TEXT=true` as a fallback. `EPIMETHIAN_ALLOW_UNGATED_WRITES=true` remains an escape hatch for headless / CI runs where no human is in the loop. |\n| **Cursor / Windsurf / Zed / others** | Varies | If write tools fail with `ELICITATION_REQUIRED_BUT_UNAVAILABLE`, the client doesn\'t advertise the capability \u2014 soft-confirm should kick in automatically; use `EPIMETHIAN_ALLOW_UNGATED_WRITES=true` only if no human is in the loop. If write tools succeed at the gate but the agent can\'t see the token in the response, set `EPIMETHIAN_TOKEN_IN_TEXT=true`. If the client *advertises* elicitation but always fails fast (decline arrives in <50 ms), v6.6.1\'s auto-detection re-routes through soft-confirm. Set `EPIMETHIAN_TREAT_ELICITATION_AS_UNSUPPORTED=true` to skip the timing probe on the first call when the client is known to fake it. |\n\n### Three configuration paths \u2014 pick the one that matches your client\n\nThese flags are **not interchangeable**. The newer (v6.6.x) flags\npreserve the human-in-the-loop gate; the older bypass flags remove it.\nDefault to preserving the gate.\n\n- **(Recommended) Soft-confirm token flow.** No env-var bypass needed.\n v6.6.0 + v6.6.1 + v6.6.3 give you a working soft-confirm round-trip\n for any client whose host can put a "do you approve?" prompt in front\n of the user (chat surface, tool-result UI, etc.). For clients with\n rendering quirks, layer on `EPIMETHIAN_TOKEN_IN_TEXT=true`\n (additive \u2014 keeps the gate, just exposes the token in\n `content[0].text` too).\n- **`EPIMETHIAN_ALLOW_UNGATED_WRITES=true`** \u2014 for clients that *don\'t\n advertise* elicitation AND have no other way to surface a\n confirmation prompt (e.g. headless / CI runs). Removes the gate\n entirely; only the harness allow-list and server-side guards remain.\n Pre-v6.6.0 escape hatch; rarely needed today.\n- **`EPIMETHIAN_BYPASS_ELICITATION=true`** \u2014 unconditional bypass,\n regardless of whether the client advertises elicitation. Original\n v6.4.1 escape hatch for Claude Code VS Code\'s "fakes elicitation"\n bug. **In v6.6.1+ this is no longer needed** for that bug \u2014 the\n fast-decline detector routes around it. Use only if you specifically\n want to remove the gate.\n\n### Trade-off: what you give up by setting `ALLOW_UNGATED_WRITES` or `BYPASS_ELICITATION`\n\nThese two flags **disable the human-in-the-loop confirmation gate\nentirely**. Writes still go through the harness\'s tool allow-list (so\nusers can still block the tool in their permission settings) and\nthrough every server-side guard (provenance, source-policy,\nwrite-budget, byte-equivalence) \u2014 but the user no longer gets a\nprompt before each destructive operation. Recommend this only when:\n\n1. The user is aware of and accepts the trade-off, AND\n2. The user\'s MCP client provides some other interaction model where they\n can intervene (e.g. they review tool calls before approval), OR\n3. The work is read-mostly and only occasional, additive writes happen.\n\n**Do NOT set either flag silently.** If you (the agent) need to recommend\none, explain to the user what the gate is for, why their client can\'t\nhonour it, and what alternative protections remain.\n\n`EPIMETHIAN_TREAT_ELICITATION_AS_UNSUPPORTED` and\n`EPIMETHIAN_TOKEN_IN_TEXT` (v6.6.1+ / v6.6.2+) are different \u2014 they\n**preserve** the gate by routing through the soft-confirm token flow.\nThey affect how the prompt reaches the user, not whether one happens.\nPrefer these to the bypass flags whenever the client supports any\nform of agent \u2194 user dialogue.\n\n## Other operator-side environment variables\n\nThese are off by default and only relevant in specific scenarios:\n\n- **`EPIMETHIAN_SUPPRESS_EQUIVALENT_DELETIONS`** \u2014 opt-in (default OFF).\n When set to `true`, suppresses the `confirm_deletions` gate for token\n deletion+creation pairs that canonicalise to byte-equivalent XML\n (e.g. re-rendering the same `<ac:link>` macros with different attribute\n order, or regenerating an `<ac:structured-macro>` whose parameters and\n CDATA body are identical after sort). Genuine semantic deletions still\n fire the gate. Every suppressed pair is recorded in the mutation log\n for postmortem. Useful for spaces with lots of cross-link rewrites\n where the gate fires repeatedly on no-op churn.\n- **`EPIMETHIAN_REQUIRE_SOURCE`** \u2014 opt-in (default OFF). When `true`,\n every write tool call must include a `source` parameter (one of\n `user_request` / `file_or_cli_input` / `chained_tool_output` /\n `elicitation_response`). Calls without an explicit source are rejected\n with `SOURCE_POLICY_BLOCKED`. Useful in audit-heavy environments where\n every write must declare provenance.\n- **`EPIMETHIAN_AUTO_UPGRADE`** \u2014 opt-in (default OFF). When `true`, the\n server checks for and applies updates on startup. Useful for managed\n fleets; usually you want explicit `epimethian-mcp upgrade` runs instead.\n- **`CONFLUENCE_READ_ONLY`** \u2014 opt-in (default OFF). When `true`, all\n write tools are disabled regardless of MCP client config. Useful for\n read-only profiles or sandbox environments.\n\n## Available Tools (35)\n\n| Tool | Description |\n|------|-------------|\n| `check_permissions` | Report the current profile\'s MCP access mode and the token\'s capabilities |\n| `create_page` | Create a new Confluence page |\n| `get_page` | Read a page by ID (use `headings_only` to preview structure first) |\n| `get_page_by_title` | Look up a page by title (use `headings_only` to preview structure first) |\n| `update_page` | Update an existing page |\n| `update_page_section` | Update a single section by heading name (supports `body` replacement OR `find_replace` literal substitutions) |\n| `update_page_sections` | Atomically update multiple sections in one version bump (all-or-nothing) |\n| `prepend_to_page` | Insert content at the beginning of an existing page (additive, safe) |\n| `append_to_page` | Insert content at the end of an existing page (additive, safe) |\n| `delete_page` | Delete a page |\n| `revert_page` | Revert a page to a previous version |\n| `list_pages` | List pages in a space |\n| `get_page_children` | Get child pages of a page |\n| `search_pages` | Search pages using CQL (Confluence Query Language) |\n| `get_spaces` | List available Confluence spaces |\n| `add_attachment` | Upload a file attachment to a page |\n| `get_attachments` | List attachments on a page |\n| `add_drawio_diagram` | Add a draw.io diagram to a page |\n| `get_labels` | Get all labels on a Confluence page |\n| `add_label` | Add one or more labels to a Confluence page |\n| `remove_label` | Remove a label from a Confluence page |\n| `get_page_status` | Get the content status badge on a page |\n| `set_page_status` | Set the content status badge on a page |\n| `remove_page_status` | Remove the content status badge from a page |\n| `get_comments` | Get footer and/or inline comments on a page |\n| `create_comment` | Create a footer or inline comment on a page |\n| `resolve_comment` | Resolve or reopen an inline comment |\n| `delete_comment` | Permanently delete a comment |\n| `get_page_versions` | List version history for a page |\n| `get_page_version` | Get page content at a specific historical version |\n| `diff_page_versions` | Compare two versions of a page |\n| `lookup_user` | Search for Atlassian users by name or email to resolve accountId for inline mentions |\n| `resolve_page_link` | Resolve a page title + space key to a stable contentId and URL for page links |\n| `get_version` | Return the epimethian-mcp server version and report available updates |\n| `upgrade` | Upgrade epimethian-mcp to the latest available version (restart required after) |\n';
|
|
50256
|
+
install_agent_default = '# Epimethian MCP - Agent Installation Guide\n\n> This document is for AI agents installing and configuring the Epimethian MCP server on behalf of a user.\n\n## Verification\n\nThe official package is **`@de-otio/epimethian-mcp`** (scoped under `@de-otio`).\n\nDo NOT install unscoped alternatives like `epimethian-mcp` \u2014 these are unofficial and potentially unsafe.\n\n## Prerequisites\n\n- Node.js 18 or later\n- npm on PATH\n\n## Step 1: Install\n\n```bash\nnpm install -g @de-otio/epimethian-mcp\n```\n\nVerify the installation:\n\n```bash\nepimethian-mcp --version 2>/dev/null || which epimethian-mcp\n```\n\n## Step 2: Resolve the absolute path\n\nIDE-hosted agents may not resolve PATH correctly. Always use the absolute path in MCP configuration:\n\n```bash\nwhich epimethian-mcp\n```\n\nUse the output as the `command` value in the MCP config below.\n\n## Step 3: Collect configuration\n\nAsk the user for:\n1. **Profile name** \u2014 a short identifier for this Confluence instance (e.g., `globex`, `acme-corp`). Lowercase alphanumeric and hyphens only.\n2. **Confluence Cloud URL** \u2014 e.g., `https://yoursite.atlassian.net`\n3. **Email address** \u2014 the email associated with their Atlassian account\n\n## Step 4: Write MCP configuration\n\nRun `epimethian-mcp setup --profile <name> --client <client-id>` after Step 5 (credential setup) \u2014 it prints the exact config snippet for your MCP host. Supported clients: `claude-code`, `claude-desktop`, `claude-code-vscode`, `cursor`, `windsurf`, `zed`, `opencode`. Keep the fallback hand-typed examples below for cases where the CLI is unavailable.\n\nAdd the server to the user\'s MCP client config. The exact file and shape depend on the client:\n\n**Claude Code, Claude Desktop, Cursor, Windsurf, Zed** \u2014 `.mcp.json` (or the\nequivalent client-specific config). The standard `mcpServers` shape:\n\n```json\n{\n "mcpServers": {\n "confluence": {\n "command": "<absolute path from Step 2>",\n "env": {\n "CONFLUENCE_PROFILE": "<profile name from Step 3>"\n }\n }\n }\n}\n```\n\n**OpenCode** \u2014 `opencode.json` at the project root or\n`~/.config/opencode/opencode.json`. Different shape (`mcp` block, `type:\n"local"`, `command` is an array, `environment` not `env`):\n\n```jsonc\n{\n "$schema": "https://opencode.ai/config.json",\n "mcp": {\n "confluence": {\n "type": "local",\n "command": ["<absolute path from Step 2>"],\n "enabled": true,\n "environment": {\n "CONFLUENCE_PROFILE": "<profile name from Step 3>",\n "EPIMETHIAN_ALLOW_UNGATED_WRITES": "true"\n }\n }\n }\n}\n```\n\nOpenCode does not support MCP elicitation (the in-protocol confirmation\nprompts), so write tools that fire the elicitation gate fail unless\n`EPIMETHIAN_ALLOW_UNGATED_WRITES=true` is set. See "MCP client\ncompatibility" below for the trade-off.\n\n**IMPORTANT:** The only required env var is `CONFLUENCE_PROFILE`. The URL,\nemail, and API token are stored securely in the OS keychain \u2014 they should\nNOT appear in config files.\n\n## Step 5: Credential setup\n\nTell the user to run this command in their terminal:\n\n```\nepimethian-mcp setup --profile <profile name from Step 3>\n```\n\nThis interactive command will:\n1. Prompt for the Confluence URL, email, and API token (masked input)\n2. Test the connection\n3. Store all credentials securely in the OS keychain under the named profile\n\nThe API token is generated at: https://id.atlassian.com/manage-profile/security/api-tokens\n\n**Do NOT ask the user for the API token yourself.** The token must go directly from the user into the interactive setup command to avoid appearing in conversation logs.\n\n## Step 6: User must restart the MCP client\n\n**IMPORTANT:** The user must restart their MCP client (e.g., restart Claude Code, reload VS Code, restart Claude Desktop) for the new server configuration to take effect. The MCP client reads `.mcp.json` at startup and does not detect changes while running.\n\nTell the user:\n> Please restart your MCP client now to activate the Confluence tools.\n\n## Step 7: Validation\n\nAfter the user restarts, verify the server is working by listing available Confluence tools or running a simple operation like listing spaces.\n\n## Adding Additional Tenants\n\nTo add a second Confluence instance (e.g., for a different customer):\n\n1. Run `epimethian-mcp setup --profile <new-profile-name>` with the new credentials\n2. In the project that uses the new tenant, update `.mcp.json` to set `CONFLUENCE_PROFILE` to the new profile name\n3. Restart the MCP client\n\nEach VS Code window / Claude Code session uses the profile specified in its `.mcp.json`. Profiles are fully isolated \u2014 different OS keychain entries, different Confluence instances.\n\n## Managing Profiles\n\n- List all profiles: `epimethian-mcp profiles`\n- Show details: `epimethian-mcp profiles --verbose`\n- Check connection: `CONFLUENCE_PROFILE=<name> epimethian-mcp status`\n- Set read-only: `epimethian-mcp profiles --set-read-only <name>`\n- Set read-write: `epimethian-mcp profiles --set-read-write <name>`\n\n### Read-Only Mode\n\nNew profiles default to **read-only**. When read-only, all write tools are blocked and return an error. To enable writes for a profile:\n\n```bash\nepimethian-mcp profiles --set-read-write <name>\n```\n\nOr during setup: `epimethian-mcp setup --profile <name> --read-write`\n\n**Important:** Restart any running MCP servers after changing the read-only flag.\n\n### Removing a Profile\n\nTo delete a profile and its credentials, run:\n\n```bash\nepimethian-mcp profiles --remove <name> --force\n```\n\n**Agents must pass `--force`** because the command normally prompts for interactive confirmation (`Remove profile "<name>" and delete its credentials? [y/N]`), which will fail in non-TTY environments like agent shell sessions. The `--force` flag skips the confirmation prompt when stdin is not a TTY.\n\nThis command:\n1. Deletes the credential entry (URL, email, API token) from the OS keychain\n2. Removes the profile from the registry at `~/.config/epimethian-mcp/profiles.json`\n3. Writes an entry to the audit log at `~/.config/epimethian-mcp/audit.log`\n\nAfter removing a profile, also remove or update any `.mcp.json` files that reference it \u2014 otherwise the MCP server will fail to start with a missing-profile error.\n\n**Errors:**\n- If the profile name is invalid (not matching lowercase alphanumeric/hyphens, 1\u201363 chars), the command exits with code 1\n- If the profile does not exist in the keychain, the keychain deletion is silently skipped \u2014 the registry entry is still removed\n\n## Accessing This Guide Post-Install\n\nOnce installed, this guide is available locally via:\n\n```bash\nepimethian-mcp agent-guide\n```\n\nThis prints the full agent guide to stdout \u2014 no web fetch required.\n\n## Uninstallation\n\nWhen a user asks to uninstall Epimethian MCP, follow these steps:\n\n### Step 1: Check for existing profiles\n\n```bash\nepimethian-mcp profiles\n```\n\n### Step 2: Ask the user about credential cleanup\n\nIf profiles exist, ask the user:\n\n> You have Epimethian profiles configured: [list the profile names]. Would you like to delete all stored credentials before uninstalling? (This removes API tokens from your OS keychain.)\n\n### Step 3: Delete credentials (if the user agrees)\n\nFor each profile the user wants removed:\n\n```bash\nepimethian-mcp profiles --remove <name> --force\n```\n\nOr to remove all profiles:\n\n```bash\nfor name in $(epimethian-mcp profiles | grep \'^ \'); do epimethian-mcp profiles --remove "$name" --force; done\n```\n\n### Step 4: Remove MCP configuration\n\nDelete the `confluence` entry (or the tenant-specific entry like `confluence-globex`) from the project\'s `.mcp.json`.\n\n### Step 5: Uninstall the package\n\n```bash\nnpm uninstall -g @de-otio/epimethian-mcp\n```\n\n### Step 6: Restart the MCP client\n\nTell the user to restart their MCP client so it stops trying to launch the removed server.\n\n## CI/CD (No Keychain)\n\nFor environments where the OS keychain is unavailable (Docker, CI), set all three env vars directly:\n\n```json\n{\n "mcpServers": {\n "confluence": {\n "command": "<absolute path>",\n "env": {\n "CONFLUENCE_URL": "<url>",\n "CONFLUENCE_EMAIL": "<email>",\n "CONFLUENCE_API_TOKEN": "<token>"\n }\n }\n }\n}\n```\n\n**Warning:** This exposes the API token in the process environment. Use profile-based auth whenever possible.\n\n## Troubleshooting\n\nIf **npm install fails**:\n- Verify Node.js 18+ is installed: `node --version`\n- Verify npm is on PATH: `npm --version`\n- If permission errors occur, the user may need to fix their npm prefix or use a Node version manager (nvm, fnm)\n\nIf **`epimethian-mcp setup` fails**:\n- "Connection failed": Verify the Confluence URL is correct and accessible\n- "Token is invalid or expired": The user needs to generate a new API token at https://id.atlassian.com/manage-profile/security/api-tokens\n- Keychain errors on Linux: The user may need to install `libsecret` / `gnome-keyring` (`apt install libsecret-tools` or equivalent)\n\nIf **the server doesn\'t appear after restart**:\n- Verify the `.mcp.json` path is correct for the user\'s MCP client\n- Verify the `command` value is an absolute path (run `which epimethian-mcp` to confirm)\n- Check that `.mcp.json` contains valid JSON (no trailing commas, correct quoting)\n\n## Write budget (safety cap on writes)\n\nepimethian-mcp enforces two write-rate caps per server process:\n\n- **Session cap** (default 250): total writes since the server started.\n- **Rolling cap** (default 75 per 15-minute window): catches bursts.\n\nThese are local safety nets, not Confluence limits. They exist because an\nautonomous agent in a retry loop or with a bad plan can issue hundreds of writes\nvery quickly, and most users would rather have a brief pause to confirm than\ndiscover the result an hour later.\n\n### What to do when you (the agent) hit `WRITE_BUDGET_EXCEEDED`\n\n1. **Stop and check.** Was the in-progress work user-requested and going as\n planned? If unsure, ask the user before continuing.\n2. **Explain to the user, in your own words:**\n - The safety budget has been hit (which scope, current vs. limit).\n - What the budget is for: a guard against runaway agents.\n - Whether the work-in-progress is legitimate (your judgement).\n - The two ways forward: wait for the rolling window to reopen, or raise the cap.\n3. **If the user wants to raise the cap**, give them this snippet to add to the\n `env` block of the epimethian-mcp entry in their MCP config (`.mcp.json` or\n equivalent \u2014 see Step 4 above for the layout):\n\n ```json\n "EPIMETHIAN_WRITE_BUDGET_ROLLING": "200",\n "EPIMETHIAN_WRITE_BUDGET_SESSION": "1000"\n ```\n\n Set either value to `"0"` to disable that scope. **Confirm with the user\n before recommending a raise** \u2014 the budget exists precisely to create a\n pause-and-check moment. The user must restart the MCP server (re-open the\n MCP client) for changes to take effect.\n4. **If the user gets a deprecation warning** about `EPIMETHIAN_WRITE_BUDGET_HOURLY`,\n tell them to rename it to `EPIMETHIAN_WRITE_BUDGET_ROLLING` in the same\n config file. The old name still works but will be removed in version 7.\n\n### Operator-side defaults\n\n- **`EPIMETHIAN_WRITE_BUDGET_SESSION`** \u2014 default 250; set to "0" to disable.\n- **`EPIMETHIAN_WRITE_BUDGET_ROLLING`** \u2014 default 75 per 15-minute window; set to "0" to disable.\n- **`EPIMETHIAN_WRITE_BUDGET_HOURLY`** \u2014 deprecated alias for `EPIMETHIAN_WRITE_BUDGET_ROLLING`; will be removed in version 7.\n\n## Soft confirmation (clients without working elicitation)\n\nSome MCP clients (OpenCode, the Claude Code VS Code extension, and\nothers) don\'t implement the in-protocol confirmation prompt \u2014 either\nthey don\'t advertise the capability, or they advertise it and never\nhonour the request (the SDK transport silently returns\n`{action: "decline"}` without showing the user a UI). Starting in\nv6.6.0, epimethian-mcp routes those confirmations through your\nagent\'s normal chat surface instead.\n\nThe implementation evolved across v6.6.0 \u2192 v6.6.3:\n\n- **v6.6.0** introduced soft elicitation for clients that don\'t\n *advertise* the capability \u2014 token-bound, single-use, diff-bound.\n- **v6.6.1** added **fast-decline auto-detection**: if the client\n advertises elicitation but the decline arrives in <50 ms (well below\n human reaction time), the session is flagged as fake and the call\n is re-routed through the soft-confirm path automatically. Threshold\n is overridable via `EPIMETHIAN_FAST_DECLINE_THRESHOLD_MS=<10..5000>`.\n No env-var configuration needed for the Claude Code VS Code\n extension\'s "fakes elicitation" bug \u2014 it Just Works.\n- **v6.6.2** declared `outputSchema` on every mutating tool so\n spec-compliant clients are obliged to forward `structuredContent`\n (where the token lives) to the agent. Added an opt-in\n `EPIMETHIAN_TOKEN_IN_TEXT=true` fallback that appends the full\n token to `content[0].text` for clients that drop `content` blocks\n on `isError: true` results (Claude Code issues #15412 / #9962 /\n #39976).\n- **v6.6.3** swapped the `outputSchema` from `z.discriminatedUnion`\n to `z.object` so the MCP SDK\'s `normalizeObjectSchema` (which only\n accepts schemas with `.shape`) can route the structured payload\n through `validateToolOutput` without throwing `_zod` undefined\n after the write commits. Hotfix; data-integrity-critical.\n- **v6.7.2** flipped the token-in-text fallback to default-on: the\n full token now appears in `content[0].text` without needing an env\n var, since most clients we care about (Claude Code in particular)\n drop `structuredContent` on `isError: true` results. Set\n `EPIMETHIAN_HIDE_TOKEN_IN_TEXT=true` to opt out for clients that\n reliably forward `structuredContent` (or to keep the token out of\n the agent transcript on principle). The legacy\n `EPIMETHIAN_TOKEN_IN_TEXT=false` spelling is also honoured for\n back-compat.\n\n### What you (the agent) see\n\nWhen a destructive write is requested against a client without working\nelicitation, the tool returns an error with a confirmation token:\n\n```\nisError: true\nstructuredContent:\n {\n "kind": "confirmation_required",\n "confirm_token": "<opaque token>",\n "audit_id": "<UUID for correlation>",\n "expires_at": "<ISO timestamp>",\n "page_id": "<pageId>",\n "human_summary": "<one-line description for the user>",\n "deletion_summary": { ... numeric counts only ... } // optional\n }\ncontent[0].text:\n \u26A0\uFE0F Confirmation required (SOFT_CONFIRMATION_REQUIRED)\n\n {human_summary}\n\n Please ask the user before retrying. If approved, re-call with the\n same parameters plus "confirm_token" from structuredContent.\n\n Token tail: ...<last 8 chars> Expires: <timestamp> Audit ID: <uuid>\n\n [FALLBACK] Full token: <full token>\n \u2190 present by default in v6.7.2+; suppressed when\n EPIMETHIAN_HIDE_TOKEN_IN_TEXT=true (or the legacy\n EPIMETHIAN_TOKEN_IN_TEXT=false) is set\n```\n\nThe `kind` discriminator distinguishes this `"confirmation_required"`\narm from the success arm (`"written"` or `"deleted"`) on the same\ntool. Successful writes return:\n\n```\nstructuredContent:\n { "kind": "written", "page_id": "...", "new_version": 12, ... }\n```\n\n\u2026or `"kind": "deleted"` for `delete_page`.\n\n### What to do\n\n1. STOP. Don\'t retry blindly.\n2. Show the user, in their language, what\'s about to happen \u2014 use the\n `human_summary` field from `structuredContent`, or the\n human-readable text in `content[0].text` if your client doesn\'t\n forward `structuredContent`. **Never echo the token bytes to the\n user** \u2014 the token is meant to flow agent \u2192 server, not user \u2192 eye.\n3. Ask the user explicitly. Wait for their answer.\n4. If approved: re-call the tool with the SAME parameters plus\n `confirm_token`. Read the token from `structuredContent.confirm_token`\n when available; otherwise the token\'s full bytes are in the\n `[FALLBACK] Full token: \u2026` line of `content[0].text` (present by\n default in v6.7.2+, suppressed by `EPIMETHIAN_HIDE_TOKEN_IN_TEXT=true`).\n The 8-character "Token tail: \u2026" line in the prose is for human\n inspection only \u2014 it is **not** the token.\n5. If denied: tell the user the operation has been cancelled.\n\n### Token semantics\n\n- Single-use: a successful retry consumes the token. Replays fail.\n- 5-minute TTL by default.\n- Invalidated by any competing write to the same page (stale).\n- Bound to the specific diff and tenant: changing the body, page version, or\n tenant invalidates the token.\n\n### Operator opt-outs\n\nThese environment variables control soft confirmation behavior:\n\n- **`EPIMETHIAN_ALLOW_UNGATED_WRITES=true`** \u2014 bypasses soft confirmation\n entirely (no prompt; useful for headless / CI). Removes the\n human-in-the-loop gate; the harness\'s tool allow-list still applies.\n- **`EPIMETHIAN_DISABLE_SOFT_CONFIRM=true`** \u2014 keeps the legacy\n `ELICITATION_REQUIRED_BUT_UNAVAILABLE` failure mode for clients without\n elicitation support.\n- **`EPIMETHIAN_SOFT_CONFIRM_TTL_MS=300000`** \u2014 override the default 5-minute\n TTL (clamped to 60 seconds minimum, 15 minutes maximum).\n- **`EPIMETHIAN_SOFT_CONFIRM_MINT_LIMIT=100`** \u2014 override the per-15-minute\n mint cap (default 100; "0" disables the cap entirely).\n\n#### v6.6.1+ fast-decline auto-detection (Claude Code VS Code et al.)\n\n- **`EPIMETHIAN_TREAT_ELICITATION_AS_UNSUPPORTED=true`** \u2014 deterministic\n counterpart to fast-decline auto-detection. Use when your client is\n known to advertise elicitation but never honour it (e.g. the Claude\n Code VS Code extension \u2264 2.1.123) and you want to skip the timing\n probe on the first call. Distinct from `EPIMETHIAN_BYPASS_ELICITATION`:\n this routes through the soft-confirmation gate; bypass removes the\n gate entirely.\n- **`EPIMETHIAN_FAST_DECLINE_THRESHOLD_MS=<10..5000>`** \u2014 override the\n fast-decline threshold (default 50 ms). Raise this if a slow MCP\n transport is mis-classifying real declines as fake.\n- **`EPIMETHIAN_DISABLE_FAST_DECLINE_DETECTION=true`** \u2014 total\n off-switch for the auto-detection; restores exactly v6.6.0 behaviour.\n\n#### v6.7.2+ token-in-text default\n\nThe soft-confirm result text appends a `[FALLBACK] Full token: <token>`\nline by default, so the token is visible to the agent even when the\nclient drops `structuredContent` on `isError: true` (Claude Code issues\n#15412 / #9962 / #39976). This matches the configuration most clients\nneed; making it the default closes the under-configured-install gap.\nThe structured payload is unchanged.\n\n- **`EPIMETHIAN_HIDE_TOKEN_IN_TEXT=true`** \u2014 opt out of the in-text\n fallback. Use when your client reliably forwards `structuredContent`\n to the agent and you\'d rather keep the token out of the transcript\n (the security choice v6.6.0 originally optimised for).\n- **`EPIMETHIAN_TOKEN_IN_TEXT=false`** \u2014 legacy spelling for the same\n opt-out, honoured for back-compat with v6.6.x configs that pinned\n the env var to a disabled value. Any other value (unset, "true",\n "1", typos) leaves the default-on behaviour in place.\n\n#### v6.8.0+ batch authorisation tokens\n\nWhen you need to fan out destructive writes across many pages \u2014 bulk\ndoc refreshes, post-migration cleanups, sub-agent fan-out \u2014 use\n`authorise_destructive_writes` once to get a `batch_token`, then pass\nit to every subsequent `update_page` / `update_page_section` /\n`update_page_sections` / `delete_page` call. ONE user prompt covers\nthe entire batch.\n\n```\n[parent agent]\n \u2192 AskUserQuestion("Rewrite these 13 pages?") \u2190 agent harness UX\n \u2192 user approves\n \u2192 mcp.authorise_destructive_writes({\n page_ids: [...13 IDs...],\n ttl_seconds: 3600,\n max_operations: 13,\n reason: "Refresh all runbook pages with v6.8.0 release notes"\n })\n \u2192 server fires soft-confirm OR live elicitation (ONE prompt)\n \u2192 batch_token returned to parent\n \u2193\n[parent dispatches 13 sub-agents, each given the batch_token]\n \u2193\n[sub-agent N]\n \u2192 reads sources, drafts page N\n \u2192 mcp.update_page({\n page_id, body, replace_body: true,\n source: "user_request",\n batch_token // \u2190 bypasses the per-call gate\n })\n \u2192 server validates batch_token; on match, decrements the operation\n counter and writes; on any failure (wrong page, expired,\n exhausted, cloud mismatch), falls through to the per-call\n confirmation gate transparently\n```\n\n**What the agent should know:**\n\n- The `batch_token` is page-id-scoped. A call to a page outside the\n approved list looks like a normal first-call destructive write \u2014\n the agent gets `SOFT_CONFIRMATION_REQUIRED`, NOT a different error\n class (this is the validation-failure invariant: granular reasons\n never leak). The fall-through is the right ergonomics; the agent\n can recover by handling the per-call confirm_token round-trip.\n- The token is NOT diff-bound. The user approved which pages may be\n written to and how many times \u2014 not the exact bytes of each write.\n This is a real reduction in defence vs. the per-call `confirm_token`\n flow: a poisoned page can make the agent draft adversarial content,\n but only for pages already in the allowlist (the injection cannot\n redirect the write target). Don\'t request a batch token for pages\n the agent discovered autonomously.\n- The token has a maximum TTL of 1 hour and a maximum operation cap\n of `page_ids.length \xD7 2` (network-retry headroom \u2014 not an\n application-level retry budget). Defaults are 15 min and `N \xD7 1`.\n- The token covers `update_page`, `update_page_section`,\n `update_page_sections`, and `delete_page`. It does NOT cover\n `create_page` (no target page_id), `prepend_to_page` /\n `append_to_page` (use the per-call gate; these are non-destructive\n by default), or `find_replace` mode of `update_page_section` (also\n non-destructive).\n- Tokens are process-local. Multi-process deployments hit the same\n caveat as `confirm_token` \u2014 pin one process per agent.\n\n**Operator opt-outs / strict mode:**\n\n- **`EPIMETHIAN_BATCH_REQUIRES_ELICITATION=true`** \u2014 refuse to mint a\n batch token via the soft-confirm fallback. Forces the user to\n approve via real (in-protocol) elicitation. Use when you want to\n preserve the strong per-call diff-binding and only allow batch\n authorisation through clients that can render a real confirmation\n UI.\n- **`EPIMETHIAN_BATCH_MINT_LIMIT=<n>`** \u2014 override the rolling\n 15-minute mint cap (default 25; "0" disables). Distinct from the\n `confirm_token` mint budget.\n\n### Multi-process deployments\n\nTokens are process-local in-memory. If you\'re running multiple MCP server\nprocesses for one tenant (e.g. a load-balanced fleet or separate processes\nper IDE window), a soft confirmation minted by process P1 will fail validation\nin process P2 (the load balancer routes the retry to a different process).\nThis is not a bug \u2014 it\'s the safe failure mode \u2014 but it means the user needs\nto mint a new token if the retry lands on a different process.\n\n**Recommendation:** Pin a single MCP server process per agent or IDE window.\nPre-seal profiles upgraded from versions before v5.5.0 must run `epimethian-mcp\nsetup` once to acquire a sealed cloudId before soft confirmation is available.\n\n## MCP client compatibility\n\nepimethian-mcp uses MCP **elicitation** (the in-protocol confirmation\nprompt added to MCP in 2025) as the human-in-the-loop gate for destructive\noperations. Different MCP clients support elicitation differently \u2014 some\nfully, some not at all, and some advertise the capability without honouring\nit. The compatibility matrix below tells you which env-var workaround to\nrecommend, if any.\n\n| Client | Elicitation? | What to do (v6.6.3+) |\n|---|---|---|\n| **Claude Code (CLI)** | Yes \u2014 full support | No special config needed. |\n| **Claude Desktop** | Yes \u2014 full support | No special config needed. |\n| **Claude Code VS Code extension** (all versions tested through 2.1.x) | Fakes it (advertises capability, never honours) | v6.6.1\'s fast-decline auto-detection routes the call through soft-confirm automatically. v6.7.2+ surfaces the full token in `content[0].text` by default (Claude Code does not forward `structuredContent` on `isError: true` \u2014 issue #15412), so no env-var configuration is needed. No `EPIMETHIAN_BYPASS_ELICITATION` either; the gate works through the soft-confirm token flow. |\n| **OpenCode** | No \u2014 capability not advertised | v6.6.0+ soft-confirmation token flow is automatic when the client lacks elicitation. The Vercel AI SDK forwards `structuredContent` when `outputSchema` is declared (v6.6.2+); v6.7.2+ also surfaces the token in `content[0].text` by default as a defence-in-depth fallback. Set `EPIMETHIAN_HIDE_TOKEN_IN_TEXT=true` if you\'d rather keep the token out of the transcript on a build that does forward `structuredContent` reliably. `EPIMETHIAN_ALLOW_UNGATED_WRITES=true` remains an escape hatch for headless / CI runs where no human is in the loop. |\n| **Cursor / Windsurf / Zed / others** | Varies | If write tools fail with `ELICITATION_REQUIRED_BUT_UNAVAILABLE`, the client doesn\'t advertise the capability \u2014 soft-confirm should kick in automatically; use `EPIMETHIAN_ALLOW_UNGATED_WRITES=true` only if no human is in the loop. v6.7.2+ ships the full token in `content[0].text` by default, so the agent should always be able to read it. If the client *advertises* elicitation but always fails fast (decline arrives in <50 ms), v6.6.1\'s auto-detection re-routes through soft-confirm. Set `EPIMETHIAN_TREAT_ELICITATION_AS_UNSUPPORTED=true` to skip the timing probe on the first call when the client is known to fake it. |\n\n### Three configuration paths \u2014 pick the one that matches your client\n\nThese flags are **not interchangeable**. The newer (v6.6.x) flags\npreserve the human-in-the-loop gate; the older bypass flags remove it.\nDefault to preserving the gate.\n\n- **(Recommended) Soft-confirm token flow.** No env-var bypass needed.\n v6.6.0 + v6.6.1 + v6.6.3 give you a working soft-confirm round-trip\n for any client whose host can put a "do you approve?" prompt in front\n of the user (chat surface, tool-result UI, etc.). For clients with\n rendering quirks, layer on `EPIMETHIAN_TOKEN_IN_TEXT=true`\n (additive \u2014 keeps the gate, just exposes the token in\n `content[0].text` too).\n- **`EPIMETHIAN_ALLOW_UNGATED_WRITES=true`** \u2014 for clients that *don\'t\n advertise* elicitation AND have no other way to surface a\n confirmation prompt (e.g. headless / CI runs). Removes the gate\n entirely; only the harness allow-list and server-side guards remain.\n Pre-v6.6.0 escape hatch; rarely needed today.\n- **`EPIMETHIAN_BYPASS_ELICITATION=true`** \u2014 unconditional bypass,\n regardless of whether the client advertises elicitation. Original\n v6.4.1 escape hatch for Claude Code VS Code\'s "fakes elicitation"\n bug. **In v6.6.1+ this is no longer needed** for that bug \u2014 the\n fast-decline detector routes around it. Use only if you specifically\n want to remove the gate.\n\n### Trade-off: what you give up by setting `ALLOW_UNGATED_WRITES` or `BYPASS_ELICITATION`\n\nThese two flags **disable the human-in-the-loop confirmation gate\nentirely**. Writes still go through the harness\'s tool allow-list (so\nusers can still block the tool in their permission settings) and\nthrough every server-side guard (provenance, source-policy,\nwrite-budget, byte-equivalence) \u2014 but the user no longer gets a\nprompt before each destructive operation. Recommend this only when:\n\n1. The user is aware of and accepts the trade-off, AND\n2. The user\'s MCP client provides some other interaction model where they\n can intervene (e.g. they review tool calls before approval), OR\n3. The work is read-mostly and only occasional, additive writes happen.\n\n**Do NOT set either flag silently.** If you (the agent) need to recommend\none, explain to the user what the gate is for, why their client can\'t\nhonour it, and what alternative protections remain.\n\n`EPIMETHIAN_TREAT_ELICITATION_AS_UNSUPPORTED` and\n`EPIMETHIAN_TOKEN_IN_TEXT` (v6.6.1+ / v6.6.2+) are different \u2014 they\n**preserve** the gate by routing through the soft-confirm token flow.\nThey affect how the prompt reaches the user, not whether one happens.\nPrefer these to the bypass flags whenever the client supports any\nform of agent \u2194 user dialogue.\n\n## Other operator-side environment variables\n\nThese are off by default and only relevant in specific scenarios:\n\n- **`EPIMETHIAN_SUPPRESS_EQUIVALENT_DELETIONS`** \u2014 opt-in (default OFF).\n When set to `true`, suppresses the `confirm_deletions` gate for token\n deletion+creation pairs that canonicalise to byte-equivalent XML\n (e.g. re-rendering the same `<ac:link>` macros with different attribute\n order, or regenerating an `<ac:structured-macro>` whose parameters and\n CDATA body are identical after sort). Genuine semantic deletions still\n fire the gate. Every suppressed pair is recorded in the mutation log\n for postmortem. Useful for spaces with lots of cross-link rewrites\n where the gate fires repeatedly on no-op churn.\n- **`EPIMETHIAN_REQUIRE_SOURCE`** \u2014 opt-in (default OFF). When `true`,\n every write tool call must include a `source` parameter (one of\n `user_request` / `file_or_cli_input` / `chained_tool_output` /\n `elicitation_response`). Calls without an explicit source are rejected\n with `SOURCE_POLICY_BLOCKED`. Useful in audit-heavy environments where\n every write must declare provenance.\n- **`EPIMETHIAN_AUTO_UPGRADE`** \u2014 opt-in (default OFF). When `true`, the\n server checks for and applies updates on startup. Useful for managed\n fleets; usually you want explicit `epimethian-mcp upgrade` runs instead.\n- **`CONFLUENCE_READ_ONLY`** \u2014 opt-in (default OFF). When `true`, all\n write tools are disabled regardless of MCP client config. Useful for\n read-only profiles or sandbox environments.\n\n## Available Tools (36)\n\n| Tool | Description |\n|------|-------------|\n| `check_permissions` | Report the current profile\'s MCP access mode and the token\'s capabilities |\n| `create_page` | Create a new Confluence page |\n| `get_page` | Read a page by ID (use `headings_only` to preview structure first) |\n| `get_page_by_title` | Look up a page by title (use `headings_only` to preview structure first) |\n| `update_page` | Update an existing page |\n| `update_page_section` | Update a single section by heading name (supports `body` replacement OR `find_replace` literal substitutions) |\n| `update_page_sections` | Atomically update multiple sections in one version bump (all-or-nothing) |\n| `prepend_to_page` | Insert content at the beginning of an existing page (additive, safe) |\n| `append_to_page` | Insert content at the end of an existing page (additive, safe) |\n| `delete_page` | Delete a page |\n| `authorise_destructive_writes` | Pre-authorise a batch of destructive writes (returns `batch_token` consumed by subsequent update / delete calls) |\n| `revert_page` | Revert a page to a previous version |\n| `list_pages` | List pages in a space |\n| `get_page_children` | Get child pages of a page |\n| `search_pages` | Search pages using CQL (Confluence Query Language) |\n| `get_spaces` | List available Confluence spaces |\n| `add_attachment` | Upload a file attachment to a page |\n| `get_attachments` | List attachments on a page |\n| `add_drawio_diagram` | Add a draw.io diagram to a page |\n| `get_labels` | Get all labels on a Confluence page |\n| `add_label` | Add one or more labels to a Confluence page |\n| `remove_label` | Remove a label from a Confluence page |\n| `get_page_status` | Get the content status badge on a page |\n| `set_page_status` | Set the content status badge on a page |\n| `remove_page_status` | Remove the content status badge from a page |\n| `get_comments` | Get footer and/or inline comments on a page |\n| `create_comment` | Create a footer or inline comment on a page |\n| `resolve_comment` | Resolve or reopen an inline comment |\n| `delete_comment` | Permanently delete a comment |\n| `get_page_versions` | List version history for a page |\n| `get_page_version` | Get page content at a specific historical version |\n| `diff_page_versions` | Compare two versions of a page |\n| `lookup_user` | Search for Atlassian users by name or email to resolve accountId for inline mentions |\n| `resolve_page_link` | Resolve a page title + space key to a stable contentId and URL for page links |\n| `get_version` | Return the epimethian-mcp server version and report available updates |\n| `upgrade` | Upgrade epimethian-mcp to the latest available version (restart required after) |\n';
|
|
49952
50257
|
}
|
|
49953
50258
|
});
|
|
49954
50259
|
|
|
@@ -49973,7 +50278,7 @@ __export(upgrade_exports, {
|
|
|
49973
50278
|
runUpgrade: () => runUpgrade
|
|
49974
50279
|
});
|
|
49975
50280
|
async function runUpgrade() {
|
|
49976
|
-
const currentVersion = "6.
|
|
50281
|
+
const currentVersion = "6.8.0";
|
|
49977
50282
|
console.log(`epimethian-mcp upgrade: current version v${currentVersion}`);
|
|
49978
50283
|
let pending = await getPendingUpdate();
|
|
49979
50284
|
if (!pending) {
|
|
@@ -60223,6 +60528,7 @@ init_zod();
|
|
|
60223
60528
|
var import_promises4 = require("node:fs/promises");
|
|
60224
60529
|
var import_node_os3 = require("node:os");
|
|
60225
60530
|
var import_node_path4 = require("node:path");
|
|
60531
|
+
var import_node_crypto7 = require("node:crypto");
|
|
60226
60532
|
init_confluence_client();
|
|
60227
60533
|
|
|
60228
60534
|
// node_modules/diff/libesm/diff/base.js
|
|
@@ -60866,6 +61172,7 @@ async function markPageUnverified(pageId, cfg) {
|
|
|
60866
61172
|
|
|
60867
61173
|
// src/server/index.ts
|
|
60868
61174
|
init_safe_write();
|
|
61175
|
+
init_batch_tokens();
|
|
60869
61176
|
|
|
60870
61177
|
// src/server/source-provenance.ts
|
|
60871
61178
|
init_zod();
|
|
@@ -61206,6 +61513,29 @@ var deleteOutputSchema = external_exports.object({
|
|
|
61206
61513
|
human_summary: external_exports.string().optional(),
|
|
61207
61514
|
deletion_summary: deletionSummarySchema.optional()
|
|
61208
61515
|
});
|
|
61516
|
+
var batchAuthorisedArm = external_exports.object({
|
|
61517
|
+
kind: external_exports.literal("batch_authorised"),
|
|
61518
|
+
batch_token: external_exports.string().min(1),
|
|
61519
|
+
audit_id: external_exports.string().min(1),
|
|
61520
|
+
expires_at: external_exports.string().min(1),
|
|
61521
|
+
authorised_page_ids: external_exports.array(external_exports.string().min(1)).min(1),
|
|
61522
|
+
remaining_operations: external_exports.number().int().nonnegative()
|
|
61523
|
+
});
|
|
61524
|
+
var batchAuthOutputSchema = external_exports.object({
|
|
61525
|
+
kind: external_exports.enum(["batch_authorised", "confirmation_required"]),
|
|
61526
|
+
// shared (audit_id and expires_at appear on both arms)
|
|
61527
|
+
audit_id: external_exports.string().min(1).optional(),
|
|
61528
|
+
expires_at: external_exports.string().min(1).optional(),
|
|
61529
|
+
// batch_authorised arm fields
|
|
61530
|
+
batch_token: external_exports.string().min(1).optional(),
|
|
61531
|
+
authorised_page_ids: external_exports.array(external_exports.string().min(1)).optional(),
|
|
61532
|
+
remaining_operations: external_exports.number().int().nonnegative().optional(),
|
|
61533
|
+
// confirmation_required arm fields
|
|
61534
|
+
confirm_token: external_exports.string().min(1).optional(),
|
|
61535
|
+
page_id: external_exports.string().min(1).optional(),
|
|
61536
|
+
human_summary: external_exports.string().optional(),
|
|
61537
|
+
deletion_summary: deletionSummarySchema.optional()
|
|
61538
|
+
});
|
|
61209
61539
|
|
|
61210
61540
|
// src/server/index.ts
|
|
61211
61541
|
init_update_orchestrator();
|
|
@@ -61943,6 +62273,148 @@ ${truncated}${truncationNote(origLen)}`
|
|
|
61943
62273
|
}
|
|
61944
62274
|
}
|
|
61945
62275
|
);
|
|
62276
|
+
server.registerTool(
|
|
62277
|
+
"authorise_destructive_writes",
|
|
62278
|
+
{
|
|
62279
|
+
description: describeWithLock(
|
|
62280
|
+
withDestructiveWarning(
|
|
62281
|
+
'Pre-authorise a batch of destructive Confluence write operations. Returns a `batch_token` that can be passed to subsequent `update_page` / `update_page_section` / `update_page_sections` / `delete_page` calls in place of `confirm_token`. A SINGLE user prompt covers the entire batch.\n\nUse this when fanning out destructive writes across multiple pages \u2014 sub-agent fan-outs, bulk doc refreshes, post-migration cleanups. The token is page-id-scoped: calls to pages outside the `page_ids` allowlist fall through to the per-call confirmation gate (no different error class than a normal destructive write would produce).\n\nTrade-off vs. per-call `confirm_token`: a batch_token is NOT diff-bound. The user approves which pages may be written to and how many times, but not the exact bytes of each write. This weakens the defence against page-content-driven prompt injection that targets the same page being viewed. Use only when the user has explicitly authorised the batch (e.g. "rewrite these 13 runbook pages") \u2014 never for content the agent discovered autonomously.\n\nIf your MCP client does not support in-protocol confirmation, this tool returns `SOFT_CONFIRMATION_REQUIRED` on the first call. STOP and ask the user. If approved, re-call with the same parameters plus `confirm_token`. The `batch_token` returned then covers all subsequent destructive writes within `page_ids` until `ttl_seconds` elapse or `max_operations` are exhausted.'
|
|
62282
|
+
),
|
|
62283
|
+
config3
|
|
62284
|
+
),
|
|
62285
|
+
inputSchema: {
|
|
62286
|
+
page_ids: external_exports.array(external_exports.string().min(1)).min(1).max(50).describe(
|
|
62287
|
+
"Explicit list of page IDs the batch_token will be valid for. 1..50 entries. Duplicates are silently deduplicated. Wildcards are NOT supported \u2014 by design, to prevent injection-redirected writes."
|
|
62288
|
+
),
|
|
62289
|
+
ttl_seconds: external_exports.number().int().min(60).max(3600).default(900).describe(
|
|
62290
|
+
"Token lifetime in seconds. Clamped to [60, 3600]. Default 900 (15 min)."
|
|
62291
|
+
),
|
|
62292
|
+
max_operations: external_exports.number().int().min(1).max(100).optional().describe(
|
|
62293
|
+
"Total operations the token authorises across all pages. Defaults to `page_ids.length` (one successful write per page). Maximum is `page_ids.length \xD7 2` \u2014 headroom for network-level retries the server cannot distinguish from real conflicts; do NOT rely on it for application-level retry budgets."
|
|
62294
|
+
),
|
|
62295
|
+
reason: external_exports.string().min(10).max(500).describe(
|
|
62296
|
+
'Human-readable purpose, shown to the user during the confirmation prompt (e.g. "Bulk doc refresh: rewrite N runbook pages with v6.8.0 release notes"). Required so the user can make an informed authorisation decision.'
|
|
62297
|
+
),
|
|
62298
|
+
source: sourceSchema,
|
|
62299
|
+
confirm_token: external_exports.string().optional().describe(
|
|
62300
|
+
"Soft-confirmation token from a prior SOFT_CONFIRMATION_REQUIRED response on this same tool. Single-use; bound to the exact {page_ids, ttl_seconds, max_operations, reason} tuple."
|
|
62301
|
+
)
|
|
62302
|
+
},
|
|
62303
|
+
// v6.8.0 — declared so spec-compliant clients forward
|
|
62304
|
+
// structuredContent (which carries the batch_token) to the agent.
|
|
62305
|
+
// Same rationale as v6.6.2 §3.1 for the other write tools.
|
|
62306
|
+
outputSchema: batchAuthOutputSchema,
|
|
62307
|
+
annotations: { destructiveHint: true, idempotentHint: false }
|
|
62308
|
+
},
|
|
62309
|
+
async ({ page_ids, ttl_seconds, max_operations, reason, source, confirm_token }) => {
|
|
62310
|
+
const blocked = writeGuard("authorise_destructive_writes", config3);
|
|
62311
|
+
if (blocked) return blocked;
|
|
62312
|
+
try {
|
|
62313
|
+
const effectiveSource = validateSource(source, ["authorise_destructive_writes"]);
|
|
62314
|
+
const requireLiveElicitation = process.env.EPIMETHIAN_BATCH_REQUIRES_ELICITATION === "true";
|
|
62315
|
+
const cfg = await getConfig();
|
|
62316
|
+
const cloudId = cfg.sealedCloudId;
|
|
62317
|
+
if (cloudId === void 0) {
|
|
62318
|
+
throw new Error(
|
|
62319
|
+
"authorise_destructive_writes requires a sealed cloudId. Run `epimethian-mcp setup` once to acquire one."
|
|
62320
|
+
);
|
|
62321
|
+
}
|
|
62322
|
+
for (const pageId of page_ids) {
|
|
62323
|
+
await checkSpaceAllowed({ pageId });
|
|
62324
|
+
}
|
|
62325
|
+
const dedupedPageIds = Array.from(new Set(page_ids));
|
|
62326
|
+
const resolvedTtl = Math.min(3600, Math.max(60, Math.floor(ttl_seconds)));
|
|
62327
|
+
const N = dedupedPageIds.length;
|
|
62328
|
+
const resolvedMax = Math.min(N * 2, max_operations ?? N);
|
|
62329
|
+
const requestDigestSrc = JSON.stringify({
|
|
62330
|
+
tool: "authorise_destructive_writes",
|
|
62331
|
+
cloudId,
|
|
62332
|
+
page_ids: [...dedupedPageIds].sort(),
|
|
62333
|
+
ttl_seconds: resolvedTtl,
|
|
62334
|
+
max_operations: resolvedMax,
|
|
62335
|
+
reason
|
|
62336
|
+
});
|
|
62337
|
+
const diffHash = (0, import_node_crypto7.createHash)("sha256").update(requestDigestSrc).digest("hex");
|
|
62338
|
+
const SYNTH_PAGE_ID = "__batch_authorisation__";
|
|
62339
|
+
const SYNTH_PAGE_VERSION = 1;
|
|
62340
|
+
const tokenResult = await maybeConsumeConfirmToken({
|
|
62341
|
+
confirm_token,
|
|
62342
|
+
tool: "authorise_destructive_writes",
|
|
62343
|
+
cloudId,
|
|
62344
|
+
pageId: SYNTH_PAGE_ID,
|
|
62345
|
+
pageVersion: SYNTH_PAGE_VERSION,
|
|
62346
|
+
diffHash
|
|
62347
|
+
});
|
|
62348
|
+
if (tokenResult === "invalid") {
|
|
62349
|
+
throw new ConverterError(
|
|
62350
|
+
"The confirmation token is no longer valid. Mint a new one by re-calling this tool without confirm_token, ask the user again, then retry with the new token.",
|
|
62351
|
+
"CONFIRMATION_TOKEN_INVALID"
|
|
62352
|
+
);
|
|
62353
|
+
} else if (tokenResult === "no_token") {
|
|
62354
|
+
const idsList = dedupedPageIds.join(", ");
|
|
62355
|
+
const summary = `Pre-authorise destructive writes to ${N} page${N === 1 ? "" : "s"}: ${idsList}. Up to ${resolvedMax} operation${resolvedMax === 1 ? "" : "s"} within ${resolvedTtl} seconds. Reason: ${reason}. Source: ${effectiveSource}.`;
|
|
62356
|
+
if (requireLiveElicitation && !effectiveSupportsElicitation(server)) {
|
|
62357
|
+
throw new Error(
|
|
62358
|
+
"EPIMETHIAN_BATCH_REQUIRES_ELICITATION=true: refusing to mint a batch token via the soft-confirm fallback. The connected client must support in-protocol elicitation. Either enable elicitation on the client, fall back to per-page confirm_token, or unset EPIMETHIAN_BATCH_REQUIRES_ELICITATION."
|
|
62359
|
+
);
|
|
62360
|
+
}
|
|
62361
|
+
await gateOperation(server, {
|
|
62362
|
+
tool: "authorise_destructive_writes",
|
|
62363
|
+
summary,
|
|
62364
|
+
details: {
|
|
62365
|
+
page_count: N,
|
|
62366
|
+
max_operations: resolvedMax,
|
|
62367
|
+
ttl_seconds: resolvedTtl,
|
|
62368
|
+
source: effectiveSource
|
|
62369
|
+
},
|
|
62370
|
+
cloudId,
|
|
62371
|
+
pageId: SYNTH_PAGE_ID,
|
|
62372
|
+
pageVersion: SYNTH_PAGE_VERSION,
|
|
62373
|
+
diffHash
|
|
62374
|
+
});
|
|
62375
|
+
}
|
|
62376
|
+
const batch = mintBatchToken({
|
|
62377
|
+
cloudId,
|
|
62378
|
+
pageIds: dedupedPageIds,
|
|
62379
|
+
ttlSeconds: resolvedTtl,
|
|
62380
|
+
maxOperations: resolvedMax
|
|
62381
|
+
});
|
|
62382
|
+
const expiresIso = new Date(batch.expiresAt).toISOString();
|
|
62383
|
+
const idsLine = dedupedPageIds.join(", ");
|
|
62384
|
+
const text2 = `Authorised batch destructive writes for ${N} page${N === 1 ? "" : "s"}.
|
|
62385
|
+
Pages: ${idsLine}
|
|
62386
|
+
Max operations: ${resolvedMax}
|
|
62387
|
+
Expires: ${expiresIso}
|
|
62388
|
+
Audit ID: ${batch.auditId}
|
|
62389
|
+
|
|
62390
|
+
Pass the batch_token below to subsequent update_page / update_page_section / update_page_sections / delete_page calls. Validation failures (wrong page, expired, exhausted, etc.) fall through to the per-call confirmation flow.
|
|
62391
|
+
|
|
62392
|
+
batch_token: ${batch.token}` + echo;
|
|
62393
|
+
return {
|
|
62394
|
+
content: [{ type: "text", text: text2 }],
|
|
62395
|
+
structuredContent: {
|
|
62396
|
+
kind: "batch_authorised",
|
|
62397
|
+
batch_token: batch.token,
|
|
62398
|
+
audit_id: batch.auditId,
|
|
62399
|
+
expires_at: expiresIso,
|
|
62400
|
+
authorised_page_ids: batch.authorisedPageIds,
|
|
62401
|
+
remaining_operations: batch.remainingOperations
|
|
62402
|
+
}
|
|
62403
|
+
};
|
|
62404
|
+
} catch (err) {
|
|
62405
|
+
if (err instanceof SoftConfirmationRequiredError) {
|
|
62406
|
+
return formatSoftConfirmationResult(err, { pageId: err.pageId });
|
|
62407
|
+
}
|
|
62408
|
+
if (err instanceof BatchMintRateLimitedError) {
|
|
62409
|
+
return toolError(err);
|
|
62410
|
+
}
|
|
62411
|
+
return toolErrorWithContext(err, {
|
|
62412
|
+
operation: "authorise_destructive_writes",
|
|
62413
|
+
profile: config3.profile
|
|
62414
|
+
});
|
|
62415
|
+
}
|
|
62416
|
+
}
|
|
62417
|
+
);
|
|
61946
62418
|
server.registerTool(
|
|
61947
62419
|
"update_page",
|
|
61948
62420
|
{
|
|
@@ -61971,7 +62443,10 @@ ${truncated}${truncationNote(origLen)}`
|
|
|
61971
62443
|
allow_raw_html: external_exports.boolean().default(false).describe("Allow raw HTML passthrough inside markdown bodies (disabled by default)."),
|
|
61972
62444
|
confluence_base_url: external_exports.string().url().optional().describe("Override the Confluence base URL used by the link rewriter. Defaults to the configured Confluence URL."),
|
|
61973
62445
|
source: sourceSchema,
|
|
61974
|
-
confirm_token: external_exports.string().optional().describe("Soft-confirmation token from a prior SOFT_CONFIRMATION_REQUIRED response. Single-use; bound to this exact diff and page version.")
|
|
62446
|
+
confirm_token: external_exports.string().optional().describe("Soft-confirmation token from a prior SOFT_CONFIRMATION_REQUIRED response. Single-use; bound to this exact diff and page version."),
|
|
62447
|
+
batch_token: external_exports.string().optional().describe(
|
|
62448
|
+
"Batch authorisation token from a prior authorise_destructive_writes call. Bypasses the per-call confirmation gate when valid for this page_id. Validation failures fall through to the per-call confirm_token / soft-confirm flow."
|
|
62449
|
+
)
|
|
61975
62450
|
},
|
|
61976
62451
|
// v6.6.2 §3.1 — declared so spec-compliant clients forward our
|
|
61977
62452
|
// structuredContent payload to the agent (the soft-confirmation
|
|
@@ -61980,9 +62455,11 @@ ${truncated}${truncationNote(origLen)}`
|
|
|
61980
62455
|
outputSchema: writeOutputSchema,
|
|
61981
62456
|
annotations: { destructiveHint: false, idempotentHint: false }
|
|
61982
62457
|
},
|
|
61983
|
-
async ({ page_id, title, version: version2, body, version_message, confirm_deletions, replace_body, confirm_shrinkage, confirm_structure_loss, allow_raw_html, confluence_base_url, source, confirm_token }) => {
|
|
62458
|
+
async ({ page_id, title, version: version2, body, version_message, confirm_deletions, replace_body, confirm_shrinkage, confirm_structure_loss, allow_raw_html, confluence_base_url, source, confirm_token, batch_token }) => {
|
|
61984
62459
|
const blocked = writeGuard("update_page", config3);
|
|
61985
62460
|
if (blocked) return blocked;
|
|
62461
|
+
let batchReservationId;
|
|
62462
|
+
let dispatched = false;
|
|
61986
62463
|
try {
|
|
61987
62464
|
await checkSpaceAllowed({ pageId: page_id });
|
|
61988
62465
|
const flagsSet = listDestructiveFlagsSet({
|
|
@@ -61999,36 +62476,44 @@ ${truncated}${truncationNote(origLen)}`
|
|
|
61999
62476
|
const pageVersion = currentPage.version?.number ?? 0;
|
|
62000
62477
|
if (flagsSet.length > 0) {
|
|
62001
62478
|
const deletionSummary = confirm_deletions && body ? tryForecastDeletions(currentStorage, body, confluence_base_url ?? cfg.url) : null;
|
|
62002
|
-
const
|
|
62003
|
-
|
|
62004
|
-
confirm_token,
|
|
62005
|
-
tool: "update_page",
|
|
62479
|
+
const batchAttempt = await tryBatchTokenForWrite({
|
|
62480
|
+
batch_token,
|
|
62006
62481
|
cloudId,
|
|
62007
|
-
pageId: page_id
|
|
62008
|
-
pageVersion,
|
|
62009
|
-
diffHash
|
|
62482
|
+
pageId: page_id
|
|
62010
62483
|
});
|
|
62011
|
-
|
|
62012
|
-
|
|
62013
|
-
|
|
62014
|
-
|
|
62015
|
-
|
|
62016
|
-
} else if (tokenResult === "no_token") {
|
|
62017
|
-
await gateOperation(server, {
|
|
62484
|
+
batchReservationId = batchAttempt.batchReservationId;
|
|
62485
|
+
if (batchReservationId === void 0) {
|
|
62486
|
+
const diffHash = cloudId && pageVersion > 0 ? computeDiffHash(body ?? currentStorage, pageVersion) : void 0;
|
|
62487
|
+
const tokenResult = await maybeConsumeConfirmToken({
|
|
62488
|
+
confirm_token,
|
|
62018
62489
|
tool: "update_page",
|
|
62019
|
-
summary: `Update page ${page_id} with destructive flags?`,
|
|
62020
|
-
details: {
|
|
62021
|
-
page_id,
|
|
62022
|
-
flags: flagsSet.join(","),
|
|
62023
|
-
source: effectiveSource,
|
|
62024
|
-
version: version2,
|
|
62025
|
-
...deletionSummary ? { deletionSummary } : {}
|
|
62026
|
-
},
|
|
62027
62490
|
cloudId,
|
|
62028
62491
|
pageId: page_id,
|
|
62029
62492
|
pageVersion,
|
|
62030
62493
|
diffHash
|
|
62031
62494
|
});
|
|
62495
|
+
if (tokenResult === "invalid") {
|
|
62496
|
+
throw new ConverterError(
|
|
62497
|
+
"The confirmation token is no longer valid. Mint a new one by re-calling this tool without confirm_token, ask the user again, then retry with the new token.",
|
|
62498
|
+
"CONFIRMATION_TOKEN_INVALID"
|
|
62499
|
+
);
|
|
62500
|
+
} else if (tokenResult === "no_token") {
|
|
62501
|
+
await gateOperation(server, {
|
|
62502
|
+
tool: "update_page",
|
|
62503
|
+
summary: `Update page ${page_id} with destructive flags?`,
|
|
62504
|
+
details: {
|
|
62505
|
+
page_id,
|
|
62506
|
+
flags: flagsSet.join(","),
|
|
62507
|
+
source: effectiveSource,
|
|
62508
|
+
version: version2,
|
|
62509
|
+
...deletionSummary ? { deletionSummary } : {}
|
|
62510
|
+
},
|
|
62511
|
+
cloudId,
|
|
62512
|
+
pageId: page_id,
|
|
62513
|
+
pageVersion,
|
|
62514
|
+
diffHash
|
|
62515
|
+
});
|
|
62516
|
+
}
|
|
62032
62517
|
}
|
|
62033
62518
|
}
|
|
62034
62519
|
const resolvedVersion = version2 === "current" ? currentPage.version?.number ?? 0 : version2;
|
|
@@ -62048,6 +62533,7 @@ ${truncated}${truncationNote(origLen)}`
|
|
|
62048
62533
|
confluenceBaseUrl: confluence_base_url ?? cfg.url
|
|
62049
62534
|
});
|
|
62050
62535
|
const mergedVersionMessage = prepared.versionMessage && version_message ? `${version_message}; ${prepared.versionMessage}` : prepared.versionMessage || version_message || "";
|
|
62536
|
+
dispatched = true;
|
|
62051
62537
|
const submitted = await safeSubmitPage({
|
|
62052
62538
|
pageId: page_id,
|
|
62053
62539
|
title,
|
|
@@ -62074,6 +62560,9 @@ ${truncated}${truncationNote(origLen)}`
|
|
|
62074
62560
|
const badgeResult = await markPageUnverified(submitted.page.id, cfg);
|
|
62075
62561
|
if (badgeResult.warning) warnings.push(badgeResult.warning);
|
|
62076
62562
|
if (isTitleOnly) {
|
|
62563
|
+
if (batchReservationId !== void 0) {
|
|
62564
|
+
finaliseReservation(batchReservationId);
|
|
62565
|
+
}
|
|
62077
62566
|
const titleOnlyResult = toolResult(
|
|
62078
62567
|
appendWarnings(`Updated: ${submitted.page.title} (ID: ${submitted.page.id}, version: ${submitted.newVersion}, title only, body unchanged)`, warnings) + echo
|
|
62079
62568
|
);
|
|
@@ -62088,6 +62577,9 @@ ${truncated}${truncationNote(origLen)}`
|
|
|
62088
62577
|
};
|
|
62089
62578
|
}
|
|
62090
62579
|
const removalNote = submitted.deletedTokens.length > 0 ? `; removed ${submitted.deletedTokens.length} preserved macro${submitted.deletedTokens.length === 1 ? "" : "s"}: ${submitted.deletedTokens.map((t) => t.fingerprint).join(", ")}` : "";
|
|
62580
|
+
if (batchReservationId !== void 0) {
|
|
62581
|
+
finaliseReservation(batchReservationId);
|
|
62582
|
+
}
|
|
62091
62583
|
const bodyUpdateResult = toolResult(
|
|
62092
62584
|
appendWarnings(`Updated: ${submitted.page.title} (ID: ${submitted.page.id}, version: ${submitted.newVersion}, body: ${submitted.oldLen}\u2192${submitted.newLen} chars${removalNote})`, warnings) + echo
|
|
62093
62585
|
);
|
|
@@ -62103,6 +62595,9 @@ ${truncated}${truncationNote(origLen)}`
|
|
|
62103
62595
|
}
|
|
62104
62596
|
};
|
|
62105
62597
|
} catch (err) {
|
|
62598
|
+
if (batchReservationId !== void 0 && !dispatched) {
|
|
62599
|
+
refundReservation(batchReservationId);
|
|
62600
|
+
}
|
|
62106
62601
|
if (err instanceof SoftConfirmationRequiredError) {
|
|
62107
62602
|
return formatSoftConfirmationResult(err, { pageId: page_id });
|
|
62108
62603
|
}
|
|
@@ -62125,7 +62620,10 @@ ${truncated}${truncationNote(origLen)}`
|
|
|
62125
62620
|
"The page version number from your most recent get_page call. Required unless EPIMETHIAN_LEGACY_DELETE_WITHOUT_VERSION=true is set; omitting it under the legacy flag emits a stderr warning."
|
|
62126
62621
|
),
|
|
62127
62622
|
source: sourceSchema,
|
|
62128
|
-
confirm_token: external_exports.string().optional().describe("Soft-confirmation token from a prior SOFT_CONFIRMATION_REQUIRED response. Single-use; bound to this exact page version.")
|
|
62623
|
+
confirm_token: external_exports.string().optional().describe("Soft-confirmation token from a prior SOFT_CONFIRMATION_REQUIRED response. Single-use; bound to this exact page version."),
|
|
62624
|
+
batch_token: external_exports.string().optional().describe(
|
|
62625
|
+
"Batch authorisation token from a prior authorise_destructive_writes call. Bypasses the per-call confirmation gate when valid for this page_id. Validation failures fall through to the per-call confirm_token / soft-confirm flow."
|
|
62626
|
+
)
|
|
62129
62627
|
},
|
|
62130
62628
|
// v6.6.2 §3.1 — declared so spec-compliant clients forward our
|
|
62131
62629
|
// structuredContent payload (especially the soft-confirm token)
|
|
@@ -62133,9 +62631,11 @@ ${truncated}${truncationNote(origLen)}`
|
|
|
62133
62631
|
outputSchema: deleteOutputSchema,
|
|
62134
62632
|
annotations: { destructiveHint: true, idempotentHint: true }
|
|
62135
62633
|
},
|
|
62136
|
-
async ({ page_id, version: version2, source, confirm_token }) => {
|
|
62634
|
+
async ({ page_id, version: version2, source, confirm_token, batch_token }) => {
|
|
62137
62635
|
const blocked = writeGuard("delete_page", config3);
|
|
62138
62636
|
if (blocked) return blocked;
|
|
62637
|
+
let batchReservationId;
|
|
62638
|
+
let dispatched = false;
|
|
62139
62639
|
try {
|
|
62140
62640
|
await checkSpaceAllowed({ pageId: page_id });
|
|
62141
62641
|
const effectiveSource = validateSource(source, ["delete_page"]);
|
|
@@ -62156,35 +62656,44 @@ ${truncated}${truncationNote(origLen)}`
|
|
|
62156
62656
|
const cloudId = cfg.sealedCloudId;
|
|
62157
62657
|
const pageVersion = version2 ?? 0;
|
|
62158
62658
|
const diffHash = cloudId && pageVersion > 0 ? computeDiffHash("", pageVersion) : void 0;
|
|
62159
|
-
const
|
|
62160
|
-
|
|
62161
|
-
tool: "delete_page",
|
|
62659
|
+
const batchAttempt = await tryBatchTokenForWrite({
|
|
62660
|
+
batch_token,
|
|
62162
62661
|
cloudId,
|
|
62163
|
-
pageId: page_id
|
|
62164
|
-
pageVersion,
|
|
62165
|
-
diffHash
|
|
62662
|
+
pageId: page_id
|
|
62166
62663
|
});
|
|
62167
|
-
|
|
62168
|
-
|
|
62169
|
-
|
|
62170
|
-
|
|
62171
|
-
);
|
|
62172
|
-
} else if (tokenResult === "no_token") {
|
|
62173
|
-
await gateOperation(server, {
|
|
62664
|
+
batchReservationId = batchAttempt.batchReservationId;
|
|
62665
|
+
if (batchReservationId === void 0) {
|
|
62666
|
+
const tokenResult = await maybeConsumeConfirmToken({
|
|
62667
|
+
confirm_token,
|
|
62174
62668
|
tool: "delete_page",
|
|
62175
|
-
summary: `Delete page ${page_id}?`,
|
|
62176
|
-
details: {
|
|
62177
|
-
page_id,
|
|
62178
|
-
version: version2 ?? "(legacy: unversioned)",
|
|
62179
|
-
source: effectiveSource
|
|
62180
|
-
},
|
|
62181
62669
|
cloudId,
|
|
62182
62670
|
pageId: page_id,
|
|
62183
62671
|
pageVersion,
|
|
62184
62672
|
diffHash
|
|
62185
62673
|
});
|
|
62674
|
+
if (tokenResult === "invalid") {
|
|
62675
|
+
throw new ConverterError(
|
|
62676
|
+
"The confirmation token is no longer valid. Mint a new one by re-calling this tool without confirm_token, ask the user again, then retry with the new token.",
|
|
62677
|
+
"CONFIRMATION_TOKEN_INVALID"
|
|
62678
|
+
);
|
|
62679
|
+
} else if (tokenResult === "no_token") {
|
|
62680
|
+
await gateOperation(server, {
|
|
62681
|
+
tool: "delete_page",
|
|
62682
|
+
summary: `Delete page ${page_id}?`,
|
|
62683
|
+
details: {
|
|
62684
|
+
page_id,
|
|
62685
|
+
version: version2 ?? "(legacy: unversioned)",
|
|
62686
|
+
source: effectiveSource
|
|
62687
|
+
},
|
|
62688
|
+
cloudId,
|
|
62689
|
+
pageId: page_id,
|
|
62690
|
+
pageVersion,
|
|
62691
|
+
diffHash
|
|
62692
|
+
});
|
|
62693
|
+
}
|
|
62186
62694
|
}
|
|
62187
62695
|
writeBudget.consume();
|
|
62696
|
+
dispatched = true;
|
|
62188
62697
|
await deletePage(page_id, version2);
|
|
62189
62698
|
if (cloudId !== void 0) {
|
|
62190
62699
|
invalidateForPage(cloudId, page_id);
|
|
@@ -62196,6 +62705,9 @@ ${truncated}${truncationNote(origLen)}`
|
|
|
62196
62705
|
...version2 !== void 0 ? { oldVersion: version2 } : {},
|
|
62197
62706
|
source: effectiveSource
|
|
62198
62707
|
});
|
|
62708
|
+
if (batchReservationId !== void 0) {
|
|
62709
|
+
finaliseReservation(batchReservationId);
|
|
62710
|
+
}
|
|
62199
62711
|
const deletedResult = toolResult(`Deleted page ${page_id}` + echo);
|
|
62200
62712
|
return {
|
|
62201
62713
|
...deletedResult,
|
|
@@ -62206,6 +62718,9 @@ ${truncated}${truncationNote(origLen)}`
|
|
|
62206
62718
|
}
|
|
62207
62719
|
};
|
|
62208
62720
|
} catch (err) {
|
|
62721
|
+
if (batchReservationId !== void 0 && !dispatched) {
|
|
62722
|
+
refundReservation(batchReservationId);
|
|
62723
|
+
}
|
|
62209
62724
|
if (err instanceof SoftConfirmationRequiredError) {
|
|
62210
62725
|
return formatSoftConfirmationResult(err, { pageId: page_id });
|
|
62211
62726
|
}
|
|
@@ -62246,16 +62761,21 @@ ${truncated}${truncationNote(origLen)}`
|
|
|
62246
62761
|
),
|
|
62247
62762
|
version_message: external_exports.string().optional().describe("Optional version comment"),
|
|
62248
62763
|
confirm_deletions: external_exports.boolean().default(false).describe("Set to true to acknowledge that your markdown removes preserved macros, emoticons, or rich elements from this section. Required when any preserved element would be deleted."),
|
|
62249
|
-
confirm_token: external_exports.string().optional().describe("Soft-confirmation token from a prior SOFT_CONFIRMATION_REQUIRED response. Single-use; bound to this exact diff and page version.")
|
|
62764
|
+
confirm_token: external_exports.string().optional().describe("Soft-confirmation token from a prior SOFT_CONFIRMATION_REQUIRED response. Single-use; bound to this exact diff and page version."),
|
|
62765
|
+
batch_token: external_exports.string().optional().describe(
|
|
62766
|
+
"Batch authorisation token from a prior authorise_destructive_writes call. Bypasses the per-call confirmation gate when valid for this page_id. Validation failures fall through to the per-call confirm_token / soft-confirm flow."
|
|
62767
|
+
)
|
|
62250
62768
|
},
|
|
62251
62769
|
// v6.6.2 §3.1 — declared so spec-compliant clients forward our
|
|
62252
62770
|
// structuredContent payload to the agent.
|
|
62253
62771
|
outputSchema: writeOutputSchema,
|
|
62254
62772
|
annotations: { destructiveHint: false, idempotentHint: false }
|
|
62255
62773
|
},
|
|
62256
|
-
async ({ page_id, section, body, find_replace, version: version2, version_message, confirm_deletions, confirm_token }) => {
|
|
62774
|
+
async ({ page_id, section, body, find_replace, version: version2, version_message, confirm_deletions, confirm_token, batch_token }) => {
|
|
62257
62775
|
const blocked = writeGuard("update_page_section", config3);
|
|
62258
62776
|
if (blocked) return blocked;
|
|
62777
|
+
let batchReservationId;
|
|
62778
|
+
let dispatched = false;
|
|
62259
62779
|
try {
|
|
62260
62780
|
const hasBody = body !== void 0;
|
|
62261
62781
|
const hasFindReplace = find_replace !== void 0 && find_replace.length > 0;
|
|
@@ -62306,6 +62826,7 @@ ${truncated}${truncationNote(origLen)}`
|
|
|
62306
62826
|
)
|
|
62307
62827
|
);
|
|
62308
62828
|
}
|
|
62829
|
+
dispatched = true;
|
|
62309
62830
|
const submitted2 = await safeSubmitPage({
|
|
62310
62831
|
pageId: page_id,
|
|
62311
62832
|
title: page.title,
|
|
@@ -62343,36 +62864,44 @@ ${truncated}${truncationNote(origLen)}`
|
|
|
62343
62864
|
};
|
|
62344
62865
|
}
|
|
62345
62866
|
if (confirm_deletions) {
|
|
62346
|
-
const
|
|
62347
|
-
|
|
62348
|
-
const tokenResult = await maybeConsumeConfirmToken({
|
|
62349
|
-
confirm_token,
|
|
62350
|
-
tool: "update_page_section",
|
|
62867
|
+
const batchAttempt = await tryBatchTokenForWrite({
|
|
62868
|
+
batch_token,
|
|
62351
62869
|
cloudId,
|
|
62352
|
-
pageId: page_id
|
|
62353
|
-
pageVersion,
|
|
62354
|
-
diffHash
|
|
62870
|
+
pageId: page_id
|
|
62355
62871
|
});
|
|
62356
|
-
|
|
62357
|
-
|
|
62358
|
-
|
|
62359
|
-
|
|
62360
|
-
|
|
62361
|
-
|
|
62362
|
-
await gateOperation(server, {
|
|
62872
|
+
batchReservationId = batchAttempt.batchReservationId;
|
|
62873
|
+
if (batchReservationId === void 0) {
|
|
62874
|
+
const deletionSummary = tryForecastDeletions(currentSectionBody, body, cfg.url);
|
|
62875
|
+
const diffHash = cloudId && pageVersion > 0 ? computeDiffHash(body ?? currentSectionBody, pageVersion) : void 0;
|
|
62876
|
+
const tokenResult = await maybeConsumeConfirmToken({
|
|
62877
|
+
confirm_token,
|
|
62363
62878
|
tool: "update_page_section",
|
|
62364
|
-
summary: `Update section "${section}" in page ${page_id} with confirm_deletions?`,
|
|
62365
|
-
details: {
|
|
62366
|
-
page_id,
|
|
62367
|
-
section,
|
|
62368
|
-
source: "confirm_deletions",
|
|
62369
|
-
...deletionSummary ? { deletionSummary } : {}
|
|
62370
|
-
},
|
|
62371
62879
|
cloudId,
|
|
62372
62880
|
pageId: page_id,
|
|
62373
62881
|
pageVersion,
|
|
62374
62882
|
diffHash
|
|
62375
62883
|
});
|
|
62884
|
+
if (tokenResult === "invalid") {
|
|
62885
|
+
throw new ConverterError(
|
|
62886
|
+
"The confirmation token is no longer valid. Mint a new one by re-calling this tool without confirm_token, ask the user again, then retry with the new token.",
|
|
62887
|
+
"CONFIRMATION_TOKEN_INVALID"
|
|
62888
|
+
);
|
|
62889
|
+
} else if (tokenResult === "no_token") {
|
|
62890
|
+
await gateOperation(server, {
|
|
62891
|
+
tool: "update_page_section",
|
|
62892
|
+
summary: `Update section "${section}" in page ${page_id} with confirm_deletions?`,
|
|
62893
|
+
details: {
|
|
62894
|
+
page_id,
|
|
62895
|
+
section,
|
|
62896
|
+
source: "confirm_deletions",
|
|
62897
|
+
...deletionSummary ? { deletionSummary } : {}
|
|
62898
|
+
},
|
|
62899
|
+
cloudId,
|
|
62900
|
+
pageId: page_id,
|
|
62901
|
+
pageVersion,
|
|
62902
|
+
diffHash
|
|
62903
|
+
});
|
|
62904
|
+
}
|
|
62376
62905
|
}
|
|
62377
62906
|
}
|
|
62378
62907
|
const prepared = await safePrepareBody({
|
|
@@ -62391,6 +62920,7 @@ ${truncated}${truncationNote(origLen)}`
|
|
|
62391
62920
|
);
|
|
62392
62921
|
}
|
|
62393
62922
|
const mergedVersionMessage = prepared.versionMessage && version_message ? `${version_message}; ${prepared.versionMessage}` : prepared.versionMessage || version_message || "";
|
|
62923
|
+
dispatched = true;
|
|
62394
62924
|
const submitted = await safeSubmitPage({
|
|
62395
62925
|
pageId: page_id,
|
|
62396
62926
|
title: page.title,
|
|
@@ -62409,6 +62939,9 @@ ${truncated}${truncationNote(origLen)}`
|
|
|
62409
62939
|
const badgeResult = await markPageUnverified(submitted.page.id, cfg);
|
|
62410
62940
|
if (badgeResult.warning) warnings.push(badgeResult.warning);
|
|
62411
62941
|
const removalNote = submitted.deletedTokens.length > 0 ? `; removed ${submitted.deletedTokens.length} preserved macro${submitted.deletedTokens.length === 1 ? "" : "s"}: ${submitted.deletedTokens.map((t) => t.fingerprint).join(", ")}` : "";
|
|
62942
|
+
if (batchReservationId !== void 0) {
|
|
62943
|
+
finaliseReservation(batchReservationId);
|
|
62944
|
+
}
|
|
62412
62945
|
const sectionBodyResult = toolResult(
|
|
62413
62946
|
appendWarnings(`Updated section "${section}" in: ${submitted.page.title} (ID: ${submitted.page.id}, version: ${submitted.newVersion}${removalNote})`, warnings) + echo
|
|
62414
62947
|
);
|
|
@@ -62424,6 +62957,9 @@ ${truncated}${truncationNote(origLen)}`
|
|
|
62424
62957
|
}
|
|
62425
62958
|
};
|
|
62426
62959
|
} catch (err) {
|
|
62960
|
+
if (batchReservationId !== void 0 && !dispatched) {
|
|
62961
|
+
refundReservation(batchReservationId);
|
|
62962
|
+
}
|
|
62427
62963
|
if (err instanceof SoftConfirmationRequiredError) {
|
|
62428
62964
|
return formatSoftConfirmationResult(err, { pageId: page_id });
|
|
62429
62965
|
}
|
|
@@ -62450,6 +62986,9 @@ ${truncated}${truncationNote(origLen)}`
|
|
|
62450
62986
|
"Set to true to acknowledge that the aggregated set of sections removes preserved macros, emoticons, or rich elements. Required when ANY section would delete a preserved element. The deletion-summary gate fires once on the AGGREGATE \u2014 a caller cannot bypass the gate by spreading deletions across sections."
|
|
62451
62987
|
),
|
|
62452
62988
|
confirm_token: external_exports.string().optional().describe("Soft-confirmation token from a prior SOFT_CONFIRMATION_REQUIRED response. Single-use; bound to this exact diff and page version."),
|
|
62989
|
+
batch_token: external_exports.string().optional().describe(
|
|
62990
|
+
"Batch authorisation token from a prior authorise_destructive_writes call. Bypasses the per-call confirmation gate when valid for this page_id. Validation failures fall through to the per-call confirm_token / soft-confirm flow."
|
|
62991
|
+
),
|
|
62453
62992
|
sections: external_exports.array(
|
|
62454
62993
|
external_exports.object({
|
|
62455
62994
|
section: external_exports.string().describe("Heading text identifying the section to replace"),
|
|
@@ -62463,9 +63002,11 @@ ${truncated}${truncationNote(origLen)}`
|
|
|
62463
63002
|
},
|
|
62464
63003
|
annotations: { destructiveHint: false, idempotentHint: false }
|
|
62465
63004
|
},
|
|
62466
|
-
async ({ page_id, version: version2, version_message, confirm_deletions, sections, confirm_token }) => {
|
|
63005
|
+
async ({ page_id, version: version2, version_message, confirm_deletions, sections, confirm_token, batch_token }) => {
|
|
62467
63006
|
const blocked = writeGuard("update_page_sections", config3);
|
|
62468
63007
|
if (blocked) return blocked;
|
|
63008
|
+
let batchReservationId;
|
|
63009
|
+
let dispatched = false;
|
|
62469
63010
|
try {
|
|
62470
63011
|
await checkSpaceAllowed({ pageId: page_id });
|
|
62471
63012
|
const cfg = await getConfig();
|
|
@@ -62480,68 +63021,76 @@ ${truncated}${truncationNote(origLen)}`
|
|
|
62480
63021
|
);
|
|
62481
63022
|
}
|
|
62482
63023
|
if (confirm_deletions) {
|
|
62483
|
-
const
|
|
62484
|
-
|
|
62485
|
-
links: 0,
|
|
62486
|
-
structuredMacros: 0,
|
|
62487
|
-
codeMacros: 0,
|
|
62488
|
-
plainElements: 0,
|
|
62489
|
-
other: 0
|
|
62490
|
-
};
|
|
62491
|
-
let any = false;
|
|
62492
|
-
for (const s of sections) {
|
|
62493
|
-
let currentSectionBody = null;
|
|
62494
|
-
try {
|
|
62495
|
-
currentSectionBody = extractSectionBody(fullBody, s.section);
|
|
62496
|
-
} catch {
|
|
62497
|
-
currentSectionBody = null;
|
|
62498
|
-
}
|
|
62499
|
-
if (currentSectionBody === null) continue;
|
|
62500
|
-
const summary = tryForecastDeletions(
|
|
62501
|
-
currentSectionBody,
|
|
62502
|
-
s.body,
|
|
62503
|
-
cfg.url
|
|
62504
|
-
);
|
|
62505
|
-
if (summary !== null) {
|
|
62506
|
-
summed.tocs += summary.tocs;
|
|
62507
|
-
summed.links += summary.links;
|
|
62508
|
-
summed.structuredMacros += summary.structuredMacros;
|
|
62509
|
-
summed.codeMacros += summary.codeMacros;
|
|
62510
|
-
summed.plainElements += summary.plainElements;
|
|
62511
|
-
summed.other += summary.other;
|
|
62512
|
-
any = true;
|
|
62513
|
-
}
|
|
62514
|
-
}
|
|
62515
|
-
const aggregateBody = sections.map((s) => s.body).join("\n");
|
|
62516
|
-
const diffHash = cloudId && pageVersion > 0 ? computeDiffHash(aggregateBody, pageVersion) : void 0;
|
|
62517
|
-
const tokenResult = await maybeConsumeConfirmToken({
|
|
62518
|
-
confirm_token,
|
|
62519
|
-
tool: "update_page_sections",
|
|
63024
|
+
const batchAttempt = await tryBatchTokenForWrite({
|
|
63025
|
+
batch_token,
|
|
62520
63026
|
cloudId,
|
|
62521
|
-
pageId: page_id
|
|
62522
|
-
pageVersion,
|
|
62523
|
-
diffHash
|
|
63027
|
+
pageId: page_id
|
|
62524
63028
|
});
|
|
62525
|
-
|
|
62526
|
-
|
|
62527
|
-
|
|
62528
|
-
|
|
62529
|
-
|
|
62530
|
-
|
|
62531
|
-
|
|
63029
|
+
batchReservationId = batchAttempt.batchReservationId;
|
|
63030
|
+
if (batchReservationId === void 0) {
|
|
63031
|
+
const summed = {
|
|
63032
|
+
tocs: 0,
|
|
63033
|
+
links: 0,
|
|
63034
|
+
structuredMacros: 0,
|
|
63035
|
+
codeMacros: 0,
|
|
63036
|
+
plainElements: 0,
|
|
63037
|
+
other: 0
|
|
63038
|
+
};
|
|
63039
|
+
let any = false;
|
|
63040
|
+
for (const s of sections) {
|
|
63041
|
+
let currentSectionBody = null;
|
|
63042
|
+
try {
|
|
63043
|
+
currentSectionBody = extractSectionBody(fullBody, s.section);
|
|
63044
|
+
} catch {
|
|
63045
|
+
currentSectionBody = null;
|
|
63046
|
+
}
|
|
63047
|
+
if (currentSectionBody === null) continue;
|
|
63048
|
+
const summary = tryForecastDeletions(
|
|
63049
|
+
currentSectionBody,
|
|
63050
|
+
s.body,
|
|
63051
|
+
cfg.url
|
|
63052
|
+
);
|
|
63053
|
+
if (summary !== null) {
|
|
63054
|
+
summed.tocs += summary.tocs;
|
|
63055
|
+
summed.links += summary.links;
|
|
63056
|
+
summed.structuredMacros += summary.structuredMacros;
|
|
63057
|
+
summed.codeMacros += summary.codeMacros;
|
|
63058
|
+
summed.plainElements += summary.plainElements;
|
|
63059
|
+
summed.other += summary.other;
|
|
63060
|
+
any = true;
|
|
63061
|
+
}
|
|
63062
|
+
}
|
|
63063
|
+
const aggregateBody = sections.map((s) => s.body).join("\n");
|
|
63064
|
+
const diffHash = cloudId && pageVersion > 0 ? computeDiffHash(aggregateBody, pageVersion) : void 0;
|
|
63065
|
+
const tokenResult = await maybeConsumeConfirmToken({
|
|
63066
|
+
confirm_token,
|
|
62532
63067
|
tool: "update_page_sections",
|
|
62533
|
-
summary: `Update ${sections.length} section${sections.length === 1 ? "" : "s"} in page ${page_id} with confirm_deletions?`,
|
|
62534
|
-
details: {
|
|
62535
|
-
page_id,
|
|
62536
|
-
section_count: sections.length,
|
|
62537
|
-
source: "confirm_deletions",
|
|
62538
|
-
...any ? { deletionSummary: summed } : {}
|
|
62539
|
-
},
|
|
62540
63068
|
cloudId,
|
|
62541
63069
|
pageId: page_id,
|
|
62542
63070
|
pageVersion,
|
|
62543
63071
|
diffHash
|
|
62544
63072
|
});
|
|
63073
|
+
if (tokenResult === "invalid") {
|
|
63074
|
+
throw new ConverterError(
|
|
63075
|
+
"The confirmation token is no longer valid. Mint a new one by re-calling this tool without confirm_token, ask the user again, then retry with the new token.",
|
|
63076
|
+
"CONFIRMATION_TOKEN_INVALID"
|
|
63077
|
+
);
|
|
63078
|
+
} else if (tokenResult === "no_token") {
|
|
63079
|
+
await gateOperation(server, {
|
|
63080
|
+
tool: "update_page_sections",
|
|
63081
|
+
summary: `Update ${sections.length} section${sections.length === 1 ? "" : "s"} in page ${page_id} with confirm_deletions?`,
|
|
63082
|
+
details: {
|
|
63083
|
+
page_id,
|
|
63084
|
+
section_count: sections.length,
|
|
63085
|
+
source: "confirm_deletions",
|
|
63086
|
+
...any ? { deletionSummary: summed } : {}
|
|
63087
|
+
},
|
|
63088
|
+
cloudId,
|
|
63089
|
+
pageId: page_id,
|
|
63090
|
+
pageVersion,
|
|
63091
|
+
diffHash
|
|
63092
|
+
});
|
|
63093
|
+
}
|
|
62545
63094
|
}
|
|
62546
63095
|
}
|
|
62547
63096
|
const prepared = await safePrepareMultiSectionBody({
|
|
@@ -62551,6 +63100,7 @@ ${truncated}${truncationNote(origLen)}`
|
|
|
62551
63100
|
confluenceBaseUrl: cfg.url
|
|
62552
63101
|
});
|
|
62553
63102
|
const mergedVersionMessage = prepared.versionMessage && version_message ? `${version_message}; ${prepared.versionMessage}` : prepared.versionMessage || version_message || "";
|
|
63103
|
+
dispatched = true;
|
|
62554
63104
|
const submitted = await safeSubmitPage({
|
|
62555
63105
|
pageId: page_id,
|
|
62556
63106
|
title: page.title,
|
|
@@ -62572,6 +63122,9 @@ ${truncated}${truncationNote(origLen)}`
|
|
|
62572
63122
|
if (badgeResult.warning) warnings.push(badgeResult.warning);
|
|
62573
63123
|
const removalNote = submitted.deletedTokens.length > 0 ? `; removed ${submitted.deletedTokens.length} preserved macro${submitted.deletedTokens.length === 1 ? "" : "s"}: ${submitted.deletedTokens.map((t) => t.fingerprint).join(", ")}` : "";
|
|
62574
63124
|
const sectionList = prepared.perSectionResults.map((r) => `"${r.section}"`).join(", ");
|
|
63125
|
+
if (batchReservationId !== void 0) {
|
|
63126
|
+
finaliseReservation(batchReservationId);
|
|
63127
|
+
}
|
|
62575
63128
|
return toolResult(
|
|
62576
63129
|
appendWarnings(
|
|
62577
63130
|
`Updated ${prepared.perSectionResults.length} section${prepared.perSectionResults.length === 1 ? "" : "s"} (${sectionList}) in: ${submitted.page.title} (ID: ${submitted.page.id}, version: ${submitted.newVersion}${removalNote})`,
|
|
@@ -62579,6 +63132,9 @@ ${truncated}${truncationNote(origLen)}`
|
|
|
62579
63132
|
) + echo
|
|
62580
63133
|
);
|
|
62581
63134
|
} catch (err) {
|
|
63135
|
+
if (batchReservationId !== void 0 && !dispatched) {
|
|
63136
|
+
refundReservation(batchReservationId);
|
|
63137
|
+
}
|
|
62582
63138
|
if (err instanceof SoftConfirmationRequiredError) {
|
|
62583
63139
|
return formatSoftConfirmationResult(err, { pageId: page_id });
|
|
62584
63140
|
}
|
|
@@ -63867,7 +64423,7 @@ ${titleFenced}${echo2}`
|
|
|
63867
64423
|
inputSchema: {}
|
|
63868
64424
|
},
|
|
63869
64425
|
async () => {
|
|
63870
|
-
let text2 = `epimethian-mcp v${"6.
|
|
64426
|
+
let text2 = `epimethian-mcp v${"6.8.0"}`;
|
|
63871
64427
|
try {
|
|
63872
64428
|
const pending = await getPendingUpdate();
|
|
63873
64429
|
if (pending) {
|
|
@@ -63898,7 +64454,7 @@ ${label} update available: v${pending.current} \u2192 v${pending.latest}. Run \`
|
|
|
63898
64454
|
const pending = await getPendingUpdate();
|
|
63899
64455
|
if (!pending) {
|
|
63900
64456
|
return toolResult(
|
|
63901
|
-
`epimethian-mcp v${"6.
|
|
64457
|
+
`epimethian-mcp v${"6.8.0"} is already up to date.`
|
|
63902
64458
|
);
|
|
63903
64459
|
}
|
|
63904
64460
|
const output = await performUpgrade(pending.latest);
|
|
@@ -63920,7 +64476,7 @@ async function startRecoveryServer(profile) {
|
|
|
63920
64476
|
const server = new McpServer(
|
|
63921
64477
|
{
|
|
63922
64478
|
name: `confluence-${profile}-setup-needed`,
|
|
63923
|
-
version: "6.
|
|
64479
|
+
version: "6.8.0"
|
|
63924
64480
|
},
|
|
63925
64481
|
{
|
|
63926
64482
|
instructions: `The Confluence profile "${profile}" referenced by CONFLUENCE_PROFILE has no keychain entry, so no Confluence tools are available. Call the setup_profile tool for instructions to create it.`
|
|
@@ -63971,21 +64527,21 @@ async function main() {
|
|
|
63971
64527
|
const serverName = config3.profile ? `confluence-${config3.profile}` : "confluence";
|
|
63972
64528
|
const server = new McpServer({
|
|
63973
64529
|
name: serverName,
|
|
63974
|
-
version: "6.
|
|
64530
|
+
version: "6.8.0"
|
|
63975
64531
|
});
|
|
63976
64532
|
await registerTools(server, config3);
|
|
63977
64533
|
const transport = new StdioServerTransport();
|
|
63978
64534
|
await server.connect(transport);
|
|
63979
64535
|
try {
|
|
63980
64536
|
const pending = await getPendingUpdate();
|
|
63981
|
-
if (pending && pending.current === "6.
|
|
64537
|
+
if (pending && pending.current === "6.8.0") {
|
|
63982
64538
|
console.error(
|
|
63983
64539
|
`epimethian-mcp: update available: v${pending.current} \u2192 v${pending.latest} (${pending.type}). Run \`epimethian-mcp upgrade\` to install.`
|
|
63984
64540
|
);
|
|
63985
64541
|
}
|
|
63986
64542
|
} catch {
|
|
63987
64543
|
}
|
|
63988
|
-
checkForUpdates("6.
|
|
64544
|
+
checkForUpdates("6.8.0").catch(() => {
|
|
63989
64545
|
});
|
|
63990
64546
|
}
|
|
63991
64547
|
|