@de-otio/epimethian-mcp 6.7.1 → 6.9.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 +789 -153
- 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.9.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.9.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];
|
|
@@ -47426,7 +47715,7 @@ function enforceContentSafetyGuards(input) {
|
|
|
47426
47715
|
if (oldLen > SHRINKAGE_GUARD_MIN_OLD_LEN && newLen < oldLen * SHRINKAGE_GUARD_MAX_RATIO && !confirmShrinkage) {
|
|
47427
47716
|
const pct = Math.round((1 - newLen / oldLen) * 100);
|
|
47428
47717
|
throw new ConverterError(
|
|
47429
|
-
`Body would shrink from ${oldLen} to ${newLen} characters (${pct}% reduction). This may indicate accidental content loss. Re-submit with confirm_shrinkage: true to proceed
|
|
47718
|
+
`Body would shrink from ${oldLen} to ${newLen} characters (${pct}% reduction). This may indicate accidental content loss. Re-submit with confirm_shrinkage: true to proceed (accepted by update_page, update_page_section, and update_page_sections).`,
|
|
47430
47719
|
SHRINKAGE_NOT_CONFIRMED
|
|
47431
47720
|
);
|
|
47432
47721
|
}
|
|
@@ -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,
|
|
@@ -48124,6 +48428,7 @@ async function safePrepareBody(input) {
|
|
|
48124
48428
|
confirmDeletions,
|
|
48125
48429
|
confirmShrinkage,
|
|
48126
48430
|
confirmStructureLoss,
|
|
48431
|
+
fullPageBody,
|
|
48127
48432
|
replaceBody,
|
|
48128
48433
|
allowRawHtml,
|
|
48129
48434
|
confluenceBaseUrl
|
|
@@ -48252,9 +48557,15 @@ Pick one path:
|
|
|
48252
48557
|
assertDeletionAckMatches(confirmDeletions, deletedTokens);
|
|
48253
48558
|
}
|
|
48254
48559
|
if (scope !== "additive" && currentBody !== void 0) {
|
|
48560
|
+
let guardOld = currentBody;
|
|
48561
|
+
let guardNew = finalStorage;
|
|
48562
|
+
if (scope === "section" && fullPageBody !== void 0 && fullPageBody.includes(currentBody)) {
|
|
48563
|
+
guardOld = fullPageBody;
|
|
48564
|
+
guardNew = fullPageBody.replace(currentBody, () => finalStorage);
|
|
48565
|
+
}
|
|
48255
48566
|
enforceContentSafetyGuards({
|
|
48256
|
-
oldStorage:
|
|
48257
|
-
newStorage:
|
|
48567
|
+
oldStorage: guardOld,
|
|
48568
|
+
newStorage: guardNew,
|
|
48258
48569
|
confirmShrinkage,
|
|
48259
48570
|
confirmStructureLoss: confirmStructureLoss || replaceBody === true,
|
|
48260
48571
|
// confirmDeletions (any truthy form, incl. a non-empty string[]) OR
|
|
@@ -48518,6 +48829,8 @@ async function safePrepareMultiSectionBody(input) {
|
|
|
48518
48829
|
currentStorage,
|
|
48519
48830
|
sections,
|
|
48520
48831
|
confirmDeletions,
|
|
48832
|
+
confirmShrinkage,
|
|
48833
|
+
confirmStructureLoss,
|
|
48521
48834
|
allowRawHtml,
|
|
48522
48835
|
confluenceBaseUrl
|
|
48523
48836
|
} = input;
|
|
@@ -48600,6 +48913,10 @@ async function safePrepareMultiSectionBody(input) {
|
|
|
48600
48913
|
currentBody: loc.body,
|
|
48601
48914
|
scope: "section",
|
|
48602
48915
|
confirmDeletions: confirmDeletions ? true : void 0,
|
|
48916
|
+
confirmShrinkage,
|
|
48917
|
+
confirmStructureLoss,
|
|
48918
|
+
// Measure each section's guards page-relative (see safePrepareBody).
|
|
48919
|
+
fullPageBody: currentStorage,
|
|
48603
48920
|
...allowRawHtml !== void 0 ? { allowRawHtml } : {},
|
|
48604
48921
|
...confluenceBaseUrl !== void 0 ? { confluenceBaseUrl } : {}
|
|
48605
48922
|
});
|
|
@@ -48658,6 +48975,7 @@ var init_safe_write = __esm({
|
|
|
48658
48975
|
"use strict";
|
|
48659
48976
|
init_confluence_client();
|
|
48660
48977
|
init_confirmation_tokens();
|
|
48978
|
+
init_batch_tokens();
|
|
48661
48979
|
init_md_to_storage();
|
|
48662
48980
|
init_update_orchestrator();
|
|
48663
48981
|
init_content_safety_guards();
|
|
@@ -48785,7 +49103,7 @@ async function writeCheckState(state) {
|
|
|
48785
49103
|
const data = JSON.stringify(state, null, 2) + "\n";
|
|
48786
49104
|
const tmpFile = (0, import_node_path3.join)(
|
|
48787
49105
|
CONFIG_DIR2,
|
|
48788
|
-
`.update-check.${(0,
|
|
49106
|
+
`.update-check.${(0, import_node_crypto6.randomBytes)(4).toString("hex")}.tmp`
|
|
48789
49107
|
);
|
|
48790
49108
|
await (0, import_promises3.writeFile)(tmpFile, data, { mode: 384 });
|
|
48791
49109
|
await (0, import_promises3.rename)(tmpFile, UPDATE_CHECK_FILE);
|
|
@@ -48926,14 +49244,14 @@ async function checkForUpdates(currentVersion) {
|
|
|
48926
49244
|
return null;
|
|
48927
49245
|
}
|
|
48928
49246
|
}
|
|
48929
|
-
var import_promises3, import_node_path3, import_node_os2,
|
|
49247
|
+
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
49248
|
var init_update_check = __esm({
|
|
48931
49249
|
"src/shared/update-check.ts"() {
|
|
48932
49250
|
"use strict";
|
|
48933
49251
|
import_promises3 = require("node:fs/promises");
|
|
48934
49252
|
import_node_path3 = require("node:path");
|
|
48935
49253
|
import_node_os2 = require("node:os");
|
|
48936
|
-
|
|
49254
|
+
import_node_crypto6 = require("node:crypto");
|
|
48937
49255
|
import_node_child_process2 = require("node:child_process");
|
|
48938
49256
|
import_node_util = require("node:util");
|
|
48939
49257
|
init_safe_fs();
|
|
@@ -49022,7 +49340,7 @@ var init_client_configs = __esm({
|
|
|
49022
49340
|
null,
|
|
49023
49341
|
2
|
|
49024
49342
|
),
|
|
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\
|
|
49343
|
+
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
49344
|
},
|
|
49027
49345
|
{
|
|
49028
49346
|
id: "cursor",
|
|
@@ -49948,7 +50266,7 @@ Informational:
|
|
|
49948
50266
|
var install_agent_default;
|
|
49949
50267
|
var init_install_agent = __esm({
|
|
49950
50268
|
"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';
|
|
50269
|
+
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
50270
|
}
|
|
49953
50271
|
});
|
|
49954
50272
|
|
|
@@ -49973,7 +50291,7 @@ __export(upgrade_exports, {
|
|
|
49973
50291
|
runUpgrade: () => runUpgrade
|
|
49974
50292
|
});
|
|
49975
50293
|
async function runUpgrade() {
|
|
49976
|
-
const currentVersion = "6.
|
|
50294
|
+
const currentVersion = "6.9.0";
|
|
49977
50295
|
console.log(`epimethian-mcp upgrade: current version v${currentVersion}`);
|
|
49978
50296
|
let pending = await getPendingUpdate();
|
|
49979
50297
|
if (!pending) {
|
|
@@ -60223,6 +60541,7 @@ init_zod();
|
|
|
60223
60541
|
var import_promises4 = require("node:fs/promises");
|
|
60224
60542
|
var import_node_os3 = require("node:os");
|
|
60225
60543
|
var import_node_path4 = require("node:path");
|
|
60544
|
+
var import_node_crypto7 = require("node:crypto");
|
|
60226
60545
|
init_confluence_client();
|
|
60227
60546
|
|
|
60228
60547
|
// node_modules/diff/libesm/diff/base.js
|
|
@@ -60866,6 +61185,7 @@ async function markPageUnverified(pageId, cfg) {
|
|
|
60866
61185
|
|
|
60867
61186
|
// src/server/index.ts
|
|
60868
61187
|
init_safe_write();
|
|
61188
|
+
init_batch_tokens();
|
|
60869
61189
|
|
|
60870
61190
|
// src/server/source-provenance.ts
|
|
60871
61191
|
init_zod();
|
|
@@ -61206,6 +61526,29 @@ var deleteOutputSchema = external_exports.object({
|
|
|
61206
61526
|
human_summary: external_exports.string().optional(),
|
|
61207
61527
|
deletion_summary: deletionSummarySchema.optional()
|
|
61208
61528
|
});
|
|
61529
|
+
var batchAuthorisedArm = external_exports.object({
|
|
61530
|
+
kind: external_exports.literal("batch_authorised"),
|
|
61531
|
+
batch_token: external_exports.string().min(1),
|
|
61532
|
+
audit_id: external_exports.string().min(1),
|
|
61533
|
+
expires_at: external_exports.string().min(1),
|
|
61534
|
+
authorised_page_ids: external_exports.array(external_exports.string().min(1)).min(1),
|
|
61535
|
+
remaining_operations: external_exports.number().int().nonnegative()
|
|
61536
|
+
});
|
|
61537
|
+
var batchAuthOutputSchema = external_exports.object({
|
|
61538
|
+
kind: external_exports.enum(["batch_authorised", "confirmation_required"]),
|
|
61539
|
+
// shared (audit_id and expires_at appear on both arms)
|
|
61540
|
+
audit_id: external_exports.string().min(1).optional(),
|
|
61541
|
+
expires_at: external_exports.string().min(1).optional(),
|
|
61542
|
+
// batch_authorised arm fields
|
|
61543
|
+
batch_token: external_exports.string().min(1).optional(),
|
|
61544
|
+
authorised_page_ids: external_exports.array(external_exports.string().min(1)).optional(),
|
|
61545
|
+
remaining_operations: external_exports.number().int().nonnegative().optional(),
|
|
61546
|
+
// confirmation_required arm fields
|
|
61547
|
+
confirm_token: external_exports.string().min(1).optional(),
|
|
61548
|
+
page_id: external_exports.string().min(1).optional(),
|
|
61549
|
+
human_summary: external_exports.string().optional(),
|
|
61550
|
+
deletion_summary: deletionSummarySchema.optional()
|
|
61551
|
+
});
|
|
61209
61552
|
|
|
61210
61553
|
// src/server/index.ts
|
|
61211
61554
|
init_update_orchestrator();
|
|
@@ -61943,6 +62286,148 @@ ${truncated}${truncationNote(origLen)}`
|
|
|
61943
62286
|
}
|
|
61944
62287
|
}
|
|
61945
62288
|
);
|
|
62289
|
+
server.registerTool(
|
|
62290
|
+
"authorise_destructive_writes",
|
|
62291
|
+
{
|
|
62292
|
+
description: describeWithLock(
|
|
62293
|
+
withDestructiveWarning(
|
|
62294
|
+
'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.'
|
|
62295
|
+
),
|
|
62296
|
+
config3
|
|
62297
|
+
),
|
|
62298
|
+
inputSchema: {
|
|
62299
|
+
page_ids: external_exports.array(external_exports.string().min(1)).min(1).max(50).describe(
|
|
62300
|
+
"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."
|
|
62301
|
+
),
|
|
62302
|
+
ttl_seconds: external_exports.number().int().min(60).max(3600).default(900).describe(
|
|
62303
|
+
"Token lifetime in seconds. Clamped to [60, 3600]. Default 900 (15 min)."
|
|
62304
|
+
),
|
|
62305
|
+
max_operations: external_exports.number().int().min(1).max(100).optional().describe(
|
|
62306
|
+
"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."
|
|
62307
|
+
),
|
|
62308
|
+
reason: external_exports.string().min(10).max(500).describe(
|
|
62309
|
+
'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.'
|
|
62310
|
+
),
|
|
62311
|
+
source: sourceSchema,
|
|
62312
|
+
confirm_token: external_exports.string().optional().describe(
|
|
62313
|
+
"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."
|
|
62314
|
+
)
|
|
62315
|
+
},
|
|
62316
|
+
// v6.8.0 — declared so spec-compliant clients forward
|
|
62317
|
+
// structuredContent (which carries the batch_token) to the agent.
|
|
62318
|
+
// Same rationale as v6.6.2 §3.1 for the other write tools.
|
|
62319
|
+
outputSchema: batchAuthOutputSchema,
|
|
62320
|
+
annotations: { destructiveHint: true, idempotentHint: false }
|
|
62321
|
+
},
|
|
62322
|
+
async ({ page_ids, ttl_seconds, max_operations, reason, source, confirm_token }) => {
|
|
62323
|
+
const blocked = writeGuard("authorise_destructive_writes", config3);
|
|
62324
|
+
if (blocked) return blocked;
|
|
62325
|
+
try {
|
|
62326
|
+
const effectiveSource = validateSource(source, ["authorise_destructive_writes"]);
|
|
62327
|
+
const requireLiveElicitation = process.env.EPIMETHIAN_BATCH_REQUIRES_ELICITATION === "true";
|
|
62328
|
+
const cfg = await getConfig();
|
|
62329
|
+
const cloudId = cfg.sealedCloudId;
|
|
62330
|
+
if (cloudId === void 0) {
|
|
62331
|
+
throw new Error(
|
|
62332
|
+
"authorise_destructive_writes requires a sealed cloudId. Run `epimethian-mcp setup` once to acquire one."
|
|
62333
|
+
);
|
|
62334
|
+
}
|
|
62335
|
+
for (const pageId of page_ids) {
|
|
62336
|
+
await checkSpaceAllowed({ pageId });
|
|
62337
|
+
}
|
|
62338
|
+
const dedupedPageIds = Array.from(new Set(page_ids));
|
|
62339
|
+
const resolvedTtl = Math.min(3600, Math.max(60, Math.floor(ttl_seconds)));
|
|
62340
|
+
const N = dedupedPageIds.length;
|
|
62341
|
+
const resolvedMax = Math.min(N * 2, max_operations ?? N);
|
|
62342
|
+
const requestDigestSrc = JSON.stringify({
|
|
62343
|
+
tool: "authorise_destructive_writes",
|
|
62344
|
+
cloudId,
|
|
62345
|
+
page_ids: [...dedupedPageIds].sort(),
|
|
62346
|
+
ttl_seconds: resolvedTtl,
|
|
62347
|
+
max_operations: resolvedMax,
|
|
62348
|
+
reason
|
|
62349
|
+
});
|
|
62350
|
+
const diffHash = (0, import_node_crypto7.createHash)("sha256").update(requestDigestSrc).digest("hex");
|
|
62351
|
+
const SYNTH_PAGE_ID = "__batch_authorisation__";
|
|
62352
|
+
const SYNTH_PAGE_VERSION = 1;
|
|
62353
|
+
const tokenResult = await maybeConsumeConfirmToken({
|
|
62354
|
+
confirm_token,
|
|
62355
|
+
tool: "authorise_destructive_writes",
|
|
62356
|
+
cloudId,
|
|
62357
|
+
pageId: SYNTH_PAGE_ID,
|
|
62358
|
+
pageVersion: SYNTH_PAGE_VERSION,
|
|
62359
|
+
diffHash
|
|
62360
|
+
});
|
|
62361
|
+
if (tokenResult === "invalid") {
|
|
62362
|
+
throw new ConverterError(
|
|
62363
|
+
"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.",
|
|
62364
|
+
"CONFIRMATION_TOKEN_INVALID"
|
|
62365
|
+
);
|
|
62366
|
+
} else if (tokenResult === "no_token") {
|
|
62367
|
+
const idsList = dedupedPageIds.join(", ");
|
|
62368
|
+
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}.`;
|
|
62369
|
+
if (requireLiveElicitation && !effectiveSupportsElicitation(server)) {
|
|
62370
|
+
throw new Error(
|
|
62371
|
+
"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."
|
|
62372
|
+
);
|
|
62373
|
+
}
|
|
62374
|
+
await gateOperation(server, {
|
|
62375
|
+
tool: "authorise_destructive_writes",
|
|
62376
|
+
summary,
|
|
62377
|
+
details: {
|
|
62378
|
+
page_count: N,
|
|
62379
|
+
max_operations: resolvedMax,
|
|
62380
|
+
ttl_seconds: resolvedTtl,
|
|
62381
|
+
source: effectiveSource
|
|
62382
|
+
},
|
|
62383
|
+
cloudId,
|
|
62384
|
+
pageId: SYNTH_PAGE_ID,
|
|
62385
|
+
pageVersion: SYNTH_PAGE_VERSION,
|
|
62386
|
+
diffHash
|
|
62387
|
+
});
|
|
62388
|
+
}
|
|
62389
|
+
const batch = mintBatchToken({
|
|
62390
|
+
cloudId,
|
|
62391
|
+
pageIds: dedupedPageIds,
|
|
62392
|
+
ttlSeconds: resolvedTtl,
|
|
62393
|
+
maxOperations: resolvedMax
|
|
62394
|
+
});
|
|
62395
|
+
const expiresIso = new Date(batch.expiresAt).toISOString();
|
|
62396
|
+
const idsLine = dedupedPageIds.join(", ");
|
|
62397
|
+
const text2 = `Authorised batch destructive writes for ${N} page${N === 1 ? "" : "s"}.
|
|
62398
|
+
Pages: ${idsLine}
|
|
62399
|
+
Max operations: ${resolvedMax}
|
|
62400
|
+
Expires: ${expiresIso}
|
|
62401
|
+
Audit ID: ${batch.auditId}
|
|
62402
|
+
|
|
62403
|
+
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.
|
|
62404
|
+
|
|
62405
|
+
batch_token: ${batch.token}` + echo;
|
|
62406
|
+
return {
|
|
62407
|
+
content: [{ type: "text", text: text2 }],
|
|
62408
|
+
structuredContent: {
|
|
62409
|
+
kind: "batch_authorised",
|
|
62410
|
+
batch_token: batch.token,
|
|
62411
|
+
audit_id: batch.auditId,
|
|
62412
|
+
expires_at: expiresIso,
|
|
62413
|
+
authorised_page_ids: batch.authorisedPageIds,
|
|
62414
|
+
remaining_operations: batch.remainingOperations
|
|
62415
|
+
}
|
|
62416
|
+
};
|
|
62417
|
+
} catch (err) {
|
|
62418
|
+
if (err instanceof SoftConfirmationRequiredError) {
|
|
62419
|
+
return formatSoftConfirmationResult(err, { pageId: err.pageId });
|
|
62420
|
+
}
|
|
62421
|
+
if (err instanceof BatchMintRateLimitedError) {
|
|
62422
|
+
return toolError(err);
|
|
62423
|
+
}
|
|
62424
|
+
return toolErrorWithContext(err, {
|
|
62425
|
+
operation: "authorise_destructive_writes",
|
|
62426
|
+
profile: config3.profile
|
|
62427
|
+
});
|
|
62428
|
+
}
|
|
62429
|
+
}
|
|
62430
|
+
);
|
|
61946
62431
|
server.registerTool(
|
|
61947
62432
|
"update_page",
|
|
61948
62433
|
{
|
|
@@ -61971,7 +62456,10 @@ ${truncated}${truncationNote(origLen)}`
|
|
|
61971
62456
|
allow_raw_html: external_exports.boolean().default(false).describe("Allow raw HTML passthrough inside markdown bodies (disabled by default)."),
|
|
61972
62457
|
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
62458
|
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.")
|
|
62459
|
+
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."),
|
|
62460
|
+
batch_token: external_exports.string().optional().describe(
|
|
62461
|
+
"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."
|
|
62462
|
+
)
|
|
61975
62463
|
},
|
|
61976
62464
|
// v6.6.2 §3.1 — declared so spec-compliant clients forward our
|
|
61977
62465
|
// structuredContent payload to the agent (the soft-confirmation
|
|
@@ -61980,9 +62468,11 @@ ${truncated}${truncationNote(origLen)}`
|
|
|
61980
62468
|
outputSchema: writeOutputSchema,
|
|
61981
62469
|
annotations: { destructiveHint: false, idempotentHint: false }
|
|
61982
62470
|
},
|
|
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 }) => {
|
|
62471
|
+
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
62472
|
const blocked = writeGuard("update_page", config3);
|
|
61985
62473
|
if (blocked) return blocked;
|
|
62474
|
+
let batchReservationId;
|
|
62475
|
+
let dispatched = false;
|
|
61986
62476
|
try {
|
|
61987
62477
|
await checkSpaceAllowed({ pageId: page_id });
|
|
61988
62478
|
const flagsSet = listDestructiveFlagsSet({
|
|
@@ -61999,36 +62489,44 @@ ${truncated}${truncationNote(origLen)}`
|
|
|
61999
62489
|
const pageVersion = currentPage.version?.number ?? 0;
|
|
62000
62490
|
if (flagsSet.length > 0) {
|
|
62001
62491
|
const deletionSummary = confirm_deletions && body ? tryForecastDeletions(currentStorage, body, confluence_base_url ?? cfg.url) : null;
|
|
62002
|
-
const
|
|
62003
|
-
|
|
62004
|
-
confirm_token,
|
|
62005
|
-
tool: "update_page",
|
|
62492
|
+
const batchAttempt = await tryBatchTokenForWrite({
|
|
62493
|
+
batch_token,
|
|
62006
62494
|
cloudId,
|
|
62007
|
-
pageId: page_id
|
|
62008
|
-
pageVersion,
|
|
62009
|
-
diffHash
|
|
62495
|
+
pageId: page_id
|
|
62010
62496
|
});
|
|
62011
|
-
|
|
62012
|
-
|
|
62013
|
-
|
|
62014
|
-
|
|
62015
|
-
|
|
62016
|
-
} else if (tokenResult === "no_token") {
|
|
62017
|
-
await gateOperation(server, {
|
|
62497
|
+
batchReservationId = batchAttempt.batchReservationId;
|
|
62498
|
+
if (batchReservationId === void 0) {
|
|
62499
|
+
const diffHash = cloudId && pageVersion > 0 ? computeDiffHash(body ?? currentStorage, pageVersion) : void 0;
|
|
62500
|
+
const tokenResult = await maybeConsumeConfirmToken({
|
|
62501
|
+
confirm_token,
|
|
62018
62502
|
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
62503
|
cloudId,
|
|
62028
62504
|
pageId: page_id,
|
|
62029
62505
|
pageVersion,
|
|
62030
62506
|
diffHash
|
|
62031
62507
|
});
|
|
62508
|
+
if (tokenResult === "invalid") {
|
|
62509
|
+
throw new ConverterError(
|
|
62510
|
+
"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.",
|
|
62511
|
+
"CONFIRMATION_TOKEN_INVALID"
|
|
62512
|
+
);
|
|
62513
|
+
} else if (tokenResult === "no_token") {
|
|
62514
|
+
await gateOperation(server, {
|
|
62515
|
+
tool: "update_page",
|
|
62516
|
+
summary: `Update page ${page_id} with destructive flags?`,
|
|
62517
|
+
details: {
|
|
62518
|
+
page_id,
|
|
62519
|
+
flags: flagsSet.join(","),
|
|
62520
|
+
source: effectiveSource,
|
|
62521
|
+
version: version2,
|
|
62522
|
+
...deletionSummary ? { deletionSummary } : {}
|
|
62523
|
+
},
|
|
62524
|
+
cloudId,
|
|
62525
|
+
pageId: page_id,
|
|
62526
|
+
pageVersion,
|
|
62527
|
+
diffHash
|
|
62528
|
+
});
|
|
62529
|
+
}
|
|
62032
62530
|
}
|
|
62033
62531
|
}
|
|
62034
62532
|
const resolvedVersion = version2 === "current" ? currentPage.version?.number ?? 0 : version2;
|
|
@@ -62048,6 +62546,7 @@ ${truncated}${truncationNote(origLen)}`
|
|
|
62048
62546
|
confluenceBaseUrl: confluence_base_url ?? cfg.url
|
|
62049
62547
|
});
|
|
62050
62548
|
const mergedVersionMessage = prepared.versionMessage && version_message ? `${version_message}; ${prepared.versionMessage}` : prepared.versionMessage || version_message || "";
|
|
62549
|
+
dispatched = true;
|
|
62051
62550
|
const submitted = await safeSubmitPage({
|
|
62052
62551
|
pageId: page_id,
|
|
62053
62552
|
title,
|
|
@@ -62074,6 +62573,9 @@ ${truncated}${truncationNote(origLen)}`
|
|
|
62074
62573
|
const badgeResult = await markPageUnverified(submitted.page.id, cfg);
|
|
62075
62574
|
if (badgeResult.warning) warnings.push(badgeResult.warning);
|
|
62076
62575
|
if (isTitleOnly) {
|
|
62576
|
+
if (batchReservationId !== void 0) {
|
|
62577
|
+
finaliseReservation(batchReservationId);
|
|
62578
|
+
}
|
|
62077
62579
|
const titleOnlyResult = toolResult(
|
|
62078
62580
|
appendWarnings(`Updated: ${submitted.page.title} (ID: ${submitted.page.id}, version: ${submitted.newVersion}, title only, body unchanged)`, warnings) + echo
|
|
62079
62581
|
);
|
|
@@ -62088,6 +62590,9 @@ ${truncated}${truncationNote(origLen)}`
|
|
|
62088
62590
|
};
|
|
62089
62591
|
}
|
|
62090
62592
|
const removalNote = submitted.deletedTokens.length > 0 ? `; removed ${submitted.deletedTokens.length} preserved macro${submitted.deletedTokens.length === 1 ? "" : "s"}: ${submitted.deletedTokens.map((t) => t.fingerprint).join(", ")}` : "";
|
|
62593
|
+
if (batchReservationId !== void 0) {
|
|
62594
|
+
finaliseReservation(batchReservationId);
|
|
62595
|
+
}
|
|
62091
62596
|
const bodyUpdateResult = toolResult(
|
|
62092
62597
|
appendWarnings(`Updated: ${submitted.page.title} (ID: ${submitted.page.id}, version: ${submitted.newVersion}, body: ${submitted.oldLen}\u2192${submitted.newLen} chars${removalNote})`, warnings) + echo
|
|
62093
62598
|
);
|
|
@@ -62103,6 +62608,9 @@ ${truncated}${truncationNote(origLen)}`
|
|
|
62103
62608
|
}
|
|
62104
62609
|
};
|
|
62105
62610
|
} catch (err) {
|
|
62611
|
+
if (batchReservationId !== void 0 && !dispatched) {
|
|
62612
|
+
refundReservation(batchReservationId);
|
|
62613
|
+
}
|
|
62106
62614
|
if (err instanceof SoftConfirmationRequiredError) {
|
|
62107
62615
|
return formatSoftConfirmationResult(err, { pageId: page_id });
|
|
62108
62616
|
}
|
|
@@ -62125,7 +62633,10 @@ ${truncated}${truncationNote(origLen)}`
|
|
|
62125
62633
|
"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
62634
|
),
|
|
62127
62635
|
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.")
|
|
62636
|
+
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."),
|
|
62637
|
+
batch_token: external_exports.string().optional().describe(
|
|
62638
|
+
"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."
|
|
62639
|
+
)
|
|
62129
62640
|
},
|
|
62130
62641
|
// v6.6.2 §3.1 — declared so spec-compliant clients forward our
|
|
62131
62642
|
// structuredContent payload (especially the soft-confirm token)
|
|
@@ -62133,9 +62644,11 @@ ${truncated}${truncationNote(origLen)}`
|
|
|
62133
62644
|
outputSchema: deleteOutputSchema,
|
|
62134
62645
|
annotations: { destructiveHint: true, idempotentHint: true }
|
|
62135
62646
|
},
|
|
62136
|
-
async ({ page_id, version: version2, source, confirm_token }) => {
|
|
62647
|
+
async ({ page_id, version: version2, source, confirm_token, batch_token }) => {
|
|
62137
62648
|
const blocked = writeGuard("delete_page", config3);
|
|
62138
62649
|
if (blocked) return blocked;
|
|
62650
|
+
let batchReservationId;
|
|
62651
|
+
let dispatched = false;
|
|
62139
62652
|
try {
|
|
62140
62653
|
await checkSpaceAllowed({ pageId: page_id });
|
|
62141
62654
|
const effectiveSource = validateSource(source, ["delete_page"]);
|
|
@@ -62156,35 +62669,44 @@ ${truncated}${truncationNote(origLen)}`
|
|
|
62156
62669
|
const cloudId = cfg.sealedCloudId;
|
|
62157
62670
|
const pageVersion = version2 ?? 0;
|
|
62158
62671
|
const diffHash = cloudId && pageVersion > 0 ? computeDiffHash("", pageVersion) : void 0;
|
|
62159
|
-
const
|
|
62160
|
-
|
|
62161
|
-
tool: "delete_page",
|
|
62672
|
+
const batchAttempt = await tryBatchTokenForWrite({
|
|
62673
|
+
batch_token,
|
|
62162
62674
|
cloudId,
|
|
62163
|
-
pageId: page_id
|
|
62164
|
-
pageVersion,
|
|
62165
|
-
diffHash
|
|
62675
|
+
pageId: page_id
|
|
62166
62676
|
});
|
|
62167
|
-
|
|
62168
|
-
|
|
62169
|
-
|
|
62170
|
-
|
|
62171
|
-
);
|
|
62172
|
-
} else if (tokenResult === "no_token") {
|
|
62173
|
-
await gateOperation(server, {
|
|
62677
|
+
batchReservationId = batchAttempt.batchReservationId;
|
|
62678
|
+
if (batchReservationId === void 0) {
|
|
62679
|
+
const tokenResult = await maybeConsumeConfirmToken({
|
|
62680
|
+
confirm_token,
|
|
62174
62681
|
tool: "delete_page",
|
|
62175
|
-
summary: `Delete page ${page_id}?`,
|
|
62176
|
-
details: {
|
|
62177
|
-
page_id,
|
|
62178
|
-
version: version2 ?? "(legacy: unversioned)",
|
|
62179
|
-
source: effectiveSource
|
|
62180
|
-
},
|
|
62181
62682
|
cloudId,
|
|
62182
62683
|
pageId: page_id,
|
|
62183
62684
|
pageVersion,
|
|
62184
62685
|
diffHash
|
|
62185
62686
|
});
|
|
62687
|
+
if (tokenResult === "invalid") {
|
|
62688
|
+
throw new ConverterError(
|
|
62689
|
+
"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.",
|
|
62690
|
+
"CONFIRMATION_TOKEN_INVALID"
|
|
62691
|
+
);
|
|
62692
|
+
} else if (tokenResult === "no_token") {
|
|
62693
|
+
await gateOperation(server, {
|
|
62694
|
+
tool: "delete_page",
|
|
62695
|
+
summary: `Delete page ${page_id}?`,
|
|
62696
|
+
details: {
|
|
62697
|
+
page_id,
|
|
62698
|
+
version: version2 ?? "(legacy: unversioned)",
|
|
62699
|
+
source: effectiveSource
|
|
62700
|
+
},
|
|
62701
|
+
cloudId,
|
|
62702
|
+
pageId: page_id,
|
|
62703
|
+
pageVersion,
|
|
62704
|
+
diffHash
|
|
62705
|
+
});
|
|
62706
|
+
}
|
|
62186
62707
|
}
|
|
62187
62708
|
writeBudget.consume();
|
|
62709
|
+
dispatched = true;
|
|
62188
62710
|
await deletePage(page_id, version2);
|
|
62189
62711
|
if (cloudId !== void 0) {
|
|
62190
62712
|
invalidateForPage(cloudId, page_id);
|
|
@@ -62196,6 +62718,9 @@ ${truncated}${truncationNote(origLen)}`
|
|
|
62196
62718
|
...version2 !== void 0 ? { oldVersion: version2 } : {},
|
|
62197
62719
|
source: effectiveSource
|
|
62198
62720
|
});
|
|
62721
|
+
if (batchReservationId !== void 0) {
|
|
62722
|
+
finaliseReservation(batchReservationId);
|
|
62723
|
+
}
|
|
62199
62724
|
const deletedResult = toolResult(`Deleted page ${page_id}` + echo);
|
|
62200
62725
|
return {
|
|
62201
62726
|
...deletedResult,
|
|
@@ -62206,6 +62731,9 @@ ${truncated}${truncationNote(origLen)}`
|
|
|
62206
62731
|
}
|
|
62207
62732
|
};
|
|
62208
62733
|
} catch (err) {
|
|
62734
|
+
if (batchReservationId !== void 0 && !dispatched) {
|
|
62735
|
+
refundReservation(batchReservationId);
|
|
62736
|
+
}
|
|
62209
62737
|
if (err instanceof SoftConfirmationRequiredError) {
|
|
62210
62738
|
return formatSoftConfirmationResult(err, { pageId: page_id });
|
|
62211
62739
|
}
|
|
@@ -62246,16 +62774,23 @@ ${truncated}${truncationNote(origLen)}`
|
|
|
62246
62774
|
),
|
|
62247
62775
|
version_message: external_exports.string().optional().describe("Optional version comment"),
|
|
62248
62776
|
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
|
-
|
|
62777
|
+
confirm_shrinkage: external_exports.boolean().default(false).describe("Set to true to acknowledge a large body reduction. The shrinkage guard is measured against the WHOLE page (not the isolated section), so a small edit to a short section will not trip it; this flag is only needed for a genuinely large page-level reduction."),
|
|
62778
|
+
confirm_structure_loss: external_exports.boolean().default(false).describe("Set to true to acknowledge a large drop in heading count, measured against the whole page."),
|
|
62779
|
+
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."),
|
|
62780
|
+
batch_token: external_exports.string().optional().describe(
|
|
62781
|
+
"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."
|
|
62782
|
+
)
|
|
62250
62783
|
},
|
|
62251
62784
|
// v6.6.2 §3.1 — declared so spec-compliant clients forward our
|
|
62252
62785
|
// structuredContent payload to the agent.
|
|
62253
62786
|
outputSchema: writeOutputSchema,
|
|
62254
62787
|
annotations: { destructiveHint: false, idempotentHint: false }
|
|
62255
62788
|
},
|
|
62256
|
-
async ({ page_id, section, body, find_replace, version: version2, version_message, confirm_deletions, confirm_token }) => {
|
|
62789
|
+
async ({ page_id, section, body, find_replace, version: version2, version_message, confirm_deletions, confirm_shrinkage, confirm_structure_loss, confirm_token, batch_token }) => {
|
|
62257
62790
|
const blocked = writeGuard("update_page_section", config3);
|
|
62258
62791
|
if (blocked) return blocked;
|
|
62792
|
+
let batchReservationId;
|
|
62793
|
+
let dispatched = false;
|
|
62259
62794
|
try {
|
|
62260
62795
|
const hasBody = body !== void 0;
|
|
62261
62796
|
const hasFindReplace = find_replace !== void 0 && find_replace.length > 0;
|
|
@@ -62306,6 +62841,7 @@ ${truncated}${truncationNote(origLen)}`
|
|
|
62306
62841
|
)
|
|
62307
62842
|
);
|
|
62308
62843
|
}
|
|
62844
|
+
dispatched = true;
|
|
62309
62845
|
const submitted2 = await safeSubmitPage({
|
|
62310
62846
|
pageId: page_id,
|
|
62311
62847
|
title: page.title,
|
|
@@ -62342,37 +62878,50 @@ ${truncated}${truncationNote(origLen)}`
|
|
|
62342
62878
|
}
|
|
62343
62879
|
};
|
|
62344
62880
|
}
|
|
62345
|
-
|
|
62346
|
-
|
|
62347
|
-
|
|
62348
|
-
|
|
62349
|
-
|
|
62350
|
-
|
|
62881
|
+
const sectionFlagsSet = listDestructiveFlagsSet({
|
|
62882
|
+
confirmShrinkage: confirm_shrinkage,
|
|
62883
|
+
confirmStructureLoss: confirm_structure_loss,
|
|
62884
|
+
confirmDeletions: confirm_deletions
|
|
62885
|
+
});
|
|
62886
|
+
if (sectionFlagsSet.length > 0) {
|
|
62887
|
+
const batchAttempt = await tryBatchTokenForWrite({
|
|
62888
|
+
batch_token,
|
|
62351
62889
|
cloudId,
|
|
62352
|
-
pageId: page_id
|
|
62353
|
-
pageVersion,
|
|
62354
|
-
diffHash
|
|
62890
|
+
pageId: page_id
|
|
62355
62891
|
});
|
|
62356
|
-
|
|
62357
|
-
|
|
62358
|
-
|
|
62359
|
-
|
|
62360
|
-
|
|
62361
|
-
|
|
62362
|
-
await gateOperation(server, {
|
|
62892
|
+
batchReservationId = batchAttempt.batchReservationId;
|
|
62893
|
+
if (batchReservationId === void 0) {
|
|
62894
|
+
const deletionSummary = confirm_deletions && body ? tryForecastDeletions(currentSectionBody, body, cfg.url) : null;
|
|
62895
|
+
const diffHash = cloudId && pageVersion > 0 ? computeDiffHash(body ?? currentSectionBody, pageVersion) : void 0;
|
|
62896
|
+
const tokenResult = await maybeConsumeConfirmToken({
|
|
62897
|
+
confirm_token,
|
|
62363
62898
|
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
62899
|
cloudId,
|
|
62372
62900
|
pageId: page_id,
|
|
62373
62901
|
pageVersion,
|
|
62374
62902
|
diffHash
|
|
62375
62903
|
});
|
|
62904
|
+
if (tokenResult === "invalid") {
|
|
62905
|
+
throw new ConverterError(
|
|
62906
|
+
"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.",
|
|
62907
|
+
"CONFIRMATION_TOKEN_INVALID"
|
|
62908
|
+
);
|
|
62909
|
+
} else if (tokenResult === "no_token") {
|
|
62910
|
+
await gateOperation(server, {
|
|
62911
|
+
tool: "update_page_section",
|
|
62912
|
+
summary: `Update section "${section}" in page ${page_id} with ${sectionFlagsSet.join(", ")}?`,
|
|
62913
|
+
details: {
|
|
62914
|
+
page_id,
|
|
62915
|
+
section,
|
|
62916
|
+
flags: sectionFlagsSet.join(","),
|
|
62917
|
+
...deletionSummary ? { deletionSummary } : {}
|
|
62918
|
+
},
|
|
62919
|
+
cloudId,
|
|
62920
|
+
pageId: page_id,
|
|
62921
|
+
pageVersion,
|
|
62922
|
+
diffHash
|
|
62923
|
+
});
|
|
62924
|
+
}
|
|
62376
62925
|
}
|
|
62377
62926
|
}
|
|
62378
62927
|
const prepared = await safePrepareBody({
|
|
@@ -62380,6 +62929,10 @@ ${truncated}${truncationNote(origLen)}`
|
|
|
62380
62929
|
currentBody: currentSectionBody,
|
|
62381
62930
|
scope: "section",
|
|
62382
62931
|
confirmDeletions: confirm_deletions || void 0,
|
|
62932
|
+
confirmShrinkage: confirm_shrinkage,
|
|
62933
|
+
confirmStructureLoss: confirm_structure_loss,
|
|
62934
|
+
// Measure shrink/structure/floor guards against the whole page.
|
|
62935
|
+
fullPageBody: fullBody,
|
|
62383
62936
|
confluenceBaseUrl: cfg.url
|
|
62384
62937
|
});
|
|
62385
62938
|
const newFullBody = replaceSection(fullBody, section, prepared.finalStorage);
|
|
@@ -62391,6 +62944,7 @@ ${truncated}${truncationNote(origLen)}`
|
|
|
62391
62944
|
);
|
|
62392
62945
|
}
|
|
62393
62946
|
const mergedVersionMessage = prepared.versionMessage && version_message ? `${version_message}; ${prepared.versionMessage}` : prepared.versionMessage || version_message || "";
|
|
62947
|
+
dispatched = true;
|
|
62394
62948
|
const submitted = await safeSubmitPage({
|
|
62395
62949
|
pageId: page_id,
|
|
62396
62950
|
title: page.title,
|
|
@@ -62401,6 +62955,11 @@ ${truncated}${truncationNote(origLen)}`
|
|
|
62401
62955
|
deletedTokens: prepared.deletedTokens,
|
|
62402
62956
|
operation: "update_page_section",
|
|
62403
62957
|
clientLabel: getClientLabel(server),
|
|
62958
|
+
// Recorded for the destructive-flag audit / version-message suffix;
|
|
62959
|
+
// guards already ran in safePrepareBody (page-relative).
|
|
62960
|
+
confirmShrinkage: confirm_shrinkage,
|
|
62961
|
+
confirmStructureLoss: confirm_structure_loss,
|
|
62962
|
+
confirmDeletions: confirm_deletions || void 0,
|
|
62404
62963
|
cloudId
|
|
62405
62964
|
});
|
|
62406
62965
|
const warnings = [];
|
|
@@ -62409,6 +62968,9 @@ ${truncated}${truncationNote(origLen)}`
|
|
|
62409
62968
|
const badgeResult = await markPageUnverified(submitted.page.id, cfg);
|
|
62410
62969
|
if (badgeResult.warning) warnings.push(badgeResult.warning);
|
|
62411
62970
|
const removalNote = submitted.deletedTokens.length > 0 ? `; removed ${submitted.deletedTokens.length} preserved macro${submitted.deletedTokens.length === 1 ? "" : "s"}: ${submitted.deletedTokens.map((t) => t.fingerprint).join(", ")}` : "";
|
|
62971
|
+
if (batchReservationId !== void 0) {
|
|
62972
|
+
finaliseReservation(batchReservationId);
|
|
62973
|
+
}
|
|
62412
62974
|
const sectionBodyResult = toolResult(
|
|
62413
62975
|
appendWarnings(`Updated section "${section}" in: ${submitted.page.title} (ID: ${submitted.page.id}, version: ${submitted.newVersion}${removalNote})`, warnings) + echo
|
|
62414
62976
|
);
|
|
@@ -62424,6 +62986,9 @@ ${truncated}${truncationNote(origLen)}`
|
|
|
62424
62986
|
}
|
|
62425
62987
|
};
|
|
62426
62988
|
} catch (err) {
|
|
62989
|
+
if (batchReservationId !== void 0 && !dispatched) {
|
|
62990
|
+
refundReservation(batchReservationId);
|
|
62991
|
+
}
|
|
62427
62992
|
if (err instanceof SoftConfirmationRequiredError) {
|
|
62428
62993
|
return formatSoftConfirmationResult(err, { pageId: page_id });
|
|
62429
62994
|
}
|
|
@@ -62449,7 +63014,12 @@ ${truncated}${truncationNote(origLen)}`
|
|
|
62449
63014
|
confirm_deletions: external_exports.boolean().default(false).describe(
|
|
62450
63015
|
"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
63016
|
),
|
|
63017
|
+
confirm_shrinkage: external_exports.boolean().default(false).describe("Set to true to acknowledge a large body reduction. Each section's shrinkage is measured against the WHOLE page, so small edits to short sections will not trip it; needed only for a genuinely large page-level reduction."),
|
|
63018
|
+
confirm_structure_loss: external_exports.boolean().default(false).describe("Set to true to acknowledge a large drop in heading count, measured against the whole page."),
|
|
62452
63019
|
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."),
|
|
63020
|
+
batch_token: external_exports.string().optional().describe(
|
|
63021
|
+
"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."
|
|
63022
|
+
),
|
|
62453
63023
|
sections: external_exports.array(
|
|
62454
63024
|
external_exports.object({
|
|
62455
63025
|
section: external_exports.string().describe("Heading text identifying the section to replace"),
|
|
@@ -62463,9 +63033,11 @@ ${truncated}${truncationNote(origLen)}`
|
|
|
62463
63033
|
},
|
|
62464
63034
|
annotations: { destructiveHint: false, idempotentHint: false }
|
|
62465
63035
|
},
|
|
62466
|
-
async ({ page_id, version: version2, version_message, confirm_deletions, sections, confirm_token }) => {
|
|
63036
|
+
async ({ page_id, version: version2, version_message, confirm_deletions, confirm_shrinkage, confirm_structure_loss, sections, confirm_token, batch_token }) => {
|
|
62467
63037
|
const blocked = writeGuard("update_page_sections", config3);
|
|
62468
63038
|
if (blocked) return blocked;
|
|
63039
|
+
let batchReservationId;
|
|
63040
|
+
let dispatched = false;
|
|
62469
63041
|
try {
|
|
62470
63042
|
await checkSpaceAllowed({ pageId: page_id });
|
|
62471
63043
|
const cfg = await getConfig();
|
|
@@ -62479,78 +63051,96 @@ ${truncated}${truncationNote(origLen)}`
|
|
|
62479
63051
|
`Could not resolve current version for page ${page_id} (server returned no version metadata)`
|
|
62480
63052
|
);
|
|
62481
63053
|
}
|
|
62482
|
-
|
|
62483
|
-
|
|
62484
|
-
|
|
62485
|
-
|
|
62486
|
-
|
|
62487
|
-
|
|
62488
|
-
|
|
62489
|
-
|
|
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",
|
|
63054
|
+
const sectionsFlagsSet = listDestructiveFlagsSet({
|
|
63055
|
+
confirmShrinkage: confirm_shrinkage,
|
|
63056
|
+
confirmStructureLoss: confirm_structure_loss,
|
|
63057
|
+
confirmDeletions: confirm_deletions
|
|
63058
|
+
});
|
|
63059
|
+
if (sectionsFlagsSet.length > 0) {
|
|
63060
|
+
const batchAttempt = await tryBatchTokenForWrite({
|
|
63061
|
+
batch_token,
|
|
62520
63062
|
cloudId,
|
|
62521
|
-
pageId: page_id
|
|
62522
|
-
pageVersion,
|
|
62523
|
-
diffHash
|
|
63063
|
+
pageId: page_id
|
|
62524
63064
|
});
|
|
62525
|
-
|
|
62526
|
-
|
|
62527
|
-
|
|
62528
|
-
|
|
62529
|
-
|
|
62530
|
-
|
|
62531
|
-
|
|
63065
|
+
batchReservationId = batchAttempt.batchReservationId;
|
|
63066
|
+
if (batchReservationId === void 0) {
|
|
63067
|
+
const summed = {
|
|
63068
|
+
tocs: 0,
|
|
63069
|
+
links: 0,
|
|
63070
|
+
structuredMacros: 0,
|
|
63071
|
+
codeMacros: 0,
|
|
63072
|
+
plainElements: 0,
|
|
63073
|
+
other: 0
|
|
63074
|
+
};
|
|
63075
|
+
let any = false;
|
|
63076
|
+
if (confirm_deletions) {
|
|
63077
|
+
for (const s of sections) {
|
|
63078
|
+
let currentSectionBody = null;
|
|
63079
|
+
try {
|
|
63080
|
+
currentSectionBody = extractSectionBody(fullBody, s.section);
|
|
63081
|
+
} catch {
|
|
63082
|
+
currentSectionBody = null;
|
|
63083
|
+
}
|
|
63084
|
+
if (currentSectionBody === null) continue;
|
|
63085
|
+
const summary = tryForecastDeletions(
|
|
63086
|
+
currentSectionBody,
|
|
63087
|
+
s.body,
|
|
63088
|
+
cfg.url
|
|
63089
|
+
);
|
|
63090
|
+
if (summary !== null) {
|
|
63091
|
+
summed.tocs += summary.tocs;
|
|
63092
|
+
summed.links += summary.links;
|
|
63093
|
+
summed.structuredMacros += summary.structuredMacros;
|
|
63094
|
+
summed.codeMacros += summary.codeMacros;
|
|
63095
|
+
summed.plainElements += summary.plainElements;
|
|
63096
|
+
summed.other += summary.other;
|
|
63097
|
+
any = true;
|
|
63098
|
+
}
|
|
63099
|
+
}
|
|
63100
|
+
}
|
|
63101
|
+
const aggregateBody = sections.map((s) => s.body).join("\n");
|
|
63102
|
+
const diffHash = cloudId && pageVersion > 0 ? computeDiffHash(aggregateBody, pageVersion) : void 0;
|
|
63103
|
+
const tokenResult = await maybeConsumeConfirmToken({
|
|
63104
|
+
confirm_token,
|
|
62532
63105
|
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
63106
|
cloudId,
|
|
62541
63107
|
pageId: page_id,
|
|
62542
63108
|
pageVersion,
|
|
62543
63109
|
diffHash
|
|
62544
63110
|
});
|
|
63111
|
+
if (tokenResult === "invalid") {
|
|
63112
|
+
throw new ConverterError(
|
|
63113
|
+
"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.",
|
|
63114
|
+
"CONFIRMATION_TOKEN_INVALID"
|
|
63115
|
+
);
|
|
63116
|
+
} else if (tokenResult === "no_token") {
|
|
63117
|
+
await gateOperation(server, {
|
|
63118
|
+
tool: "update_page_sections",
|
|
63119
|
+
summary: `Update ${sections.length} section${sections.length === 1 ? "" : "s"} in page ${page_id} with ${sectionsFlagsSet.join(", ")}?`,
|
|
63120
|
+
details: {
|
|
63121
|
+
page_id,
|
|
63122
|
+
section_count: sections.length,
|
|
63123
|
+
flags: sectionsFlagsSet.join(","),
|
|
63124
|
+
...any ? { deletionSummary: summed } : {}
|
|
63125
|
+
},
|
|
63126
|
+
cloudId,
|
|
63127
|
+
pageId: page_id,
|
|
63128
|
+
pageVersion,
|
|
63129
|
+
diffHash
|
|
63130
|
+
});
|
|
63131
|
+
}
|
|
62545
63132
|
}
|
|
62546
63133
|
}
|
|
62547
63134
|
const prepared = await safePrepareMultiSectionBody({
|
|
62548
63135
|
currentStorage: fullBody,
|
|
62549
63136
|
sections,
|
|
62550
63137
|
confirmDeletions: confirm_deletions,
|
|
63138
|
+
confirmShrinkage: confirm_shrinkage,
|
|
63139
|
+
confirmStructureLoss: confirm_structure_loss,
|
|
62551
63140
|
confluenceBaseUrl: cfg.url
|
|
62552
63141
|
});
|
|
62553
63142
|
const mergedVersionMessage = prepared.versionMessage && version_message ? `${version_message}; ${prepared.versionMessage}` : prepared.versionMessage || version_message || "";
|
|
63143
|
+
dispatched = true;
|
|
62554
63144
|
const submitted = await safeSubmitPage({
|
|
62555
63145
|
pageId: page_id,
|
|
62556
63146
|
title: page.title,
|
|
@@ -62563,6 +63153,8 @@ ${truncated}${truncationNote(origLen)}`
|
|
|
62563
63153
|
operation: "update_page_section",
|
|
62564
63154
|
clientLabel: getClientLabel(server),
|
|
62565
63155
|
confirmDeletions: confirm_deletions,
|
|
63156
|
+
confirmShrinkage: confirm_shrinkage,
|
|
63157
|
+
confirmStructureLoss: confirm_structure_loss,
|
|
62566
63158
|
cloudId
|
|
62567
63159
|
});
|
|
62568
63160
|
const warnings = [];
|
|
@@ -62572,6 +63164,9 @@ ${truncated}${truncationNote(origLen)}`
|
|
|
62572
63164
|
if (badgeResult.warning) warnings.push(badgeResult.warning);
|
|
62573
63165
|
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
63166
|
const sectionList = prepared.perSectionResults.map((r) => `"${r.section}"`).join(", ");
|
|
63167
|
+
if (batchReservationId !== void 0) {
|
|
63168
|
+
finaliseReservation(batchReservationId);
|
|
63169
|
+
}
|
|
62575
63170
|
return toolResult(
|
|
62576
63171
|
appendWarnings(
|
|
62577
63172
|
`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 +63174,9 @@ ${truncated}${truncationNote(origLen)}`
|
|
|
62579
63174
|
) + echo
|
|
62580
63175
|
);
|
|
62581
63176
|
} catch (err) {
|
|
63177
|
+
if (batchReservationId !== void 0 && !dispatched) {
|
|
63178
|
+
refundReservation(batchReservationId);
|
|
63179
|
+
}
|
|
62582
63180
|
if (err instanceof SoftConfirmationRequiredError) {
|
|
62583
63181
|
return formatSoftConfirmationResult(err, { pageId: page_id });
|
|
62584
63182
|
}
|
|
@@ -63013,12 +63611,18 @@ ${truncated}`);
|
|
|
63013
63611
|
"Name for the diagram file (e.g., 'architecture.drawio'). Will have .drawio appended if not present."
|
|
63014
63612
|
),
|
|
63015
63613
|
append: external_exports.boolean().default(true).describe(
|
|
63016
|
-
"If true, appends the diagram to
|
|
63614
|
+
"If true, appends the diagram to the end of the page. If false, replaces the page body. Ignored when after_section or return_macro_only is set."
|
|
63615
|
+
),
|
|
63616
|
+
after_section: external_exports.string().optional().describe(
|
|
63617
|
+
"Heading text of a section to place the diagram in. The diagram is inserted at the END of that section's content (before the next heading) instead of at the end of the page. Use headings_only / get_page to find section names. Takes precedence over `append`."
|
|
63618
|
+
),
|
|
63619
|
+
return_macro_only: external_exports.boolean().default(false).describe(
|
|
63620
|
+
"If true, upload the diagram as an attachment but DO NOT modify the page body; instead return the draw.io macro storage markup so you can place it yourself with update_page / update_page_section. Use this when you need precise positioning the other options can't express. Takes precedence over `after_section` and `append`. (The attachment is created either way; if you never embed the returned macro it is left orphaned.)"
|
|
63017
63621
|
)
|
|
63018
63622
|
},
|
|
63019
63623
|
annotations: { destructiveHint: false, idempotentHint: false }
|
|
63020
63624
|
},
|
|
63021
|
-
async ({ page_id, diagram_xml, diagram_name, append }) => {
|
|
63625
|
+
async ({ page_id, diagram_xml, diagram_name, append, after_section, return_macro_only }) => {
|
|
63022
63626
|
const blocked = writeGuard("add_drawio_diagram", config3);
|
|
63023
63627
|
if (blocked) return blocked;
|
|
63024
63628
|
try {
|
|
@@ -63053,15 +63657,47 @@ ${truncated}`);
|
|
|
63053
63657
|
].join("\n");
|
|
63054
63658
|
const current = await getPage(page_id, true);
|
|
63055
63659
|
const existingBody = current.body?.storage?.value ?? current.body?.value ?? "";
|
|
63056
|
-
|
|
63660
|
+
if (return_macro_only) {
|
|
63661
|
+
return toolResult(
|
|
63662
|
+
`Diagram "${filename}" uploaded to page ${current.title} (ID: ${page_id}, attachment ID: ${attachmentId}, macro ID: ${macroId}). The page body was NOT modified. Embed the diagram by inserting this macro where you want it (its baseUrl/pageId/diagramName are bound to this page):
|
|
63663
|
+
|
|
63664
|
+
${macro}${echo}`
|
|
63665
|
+
);
|
|
63666
|
+
}
|
|
63667
|
+
let newBody;
|
|
63668
|
+
let placementNote = "";
|
|
63669
|
+
if (after_section !== void 0) {
|
|
63670
|
+
const sectionBody = extractSectionBody(existingBody, after_section);
|
|
63671
|
+
if (sectionBody === null) {
|
|
63672
|
+
return toolError(
|
|
63673
|
+
new Error(
|
|
63674
|
+
`Section "${after_section}" not found. Use headings_only to see available sections.`
|
|
63675
|
+
)
|
|
63676
|
+
);
|
|
63677
|
+
}
|
|
63678
|
+
const spliced = replaceSection(
|
|
63679
|
+
existingBody,
|
|
63680
|
+
after_section,
|
|
63681
|
+
`${sectionBody}
|
|
63682
|
+
${macro}`
|
|
63683
|
+
);
|
|
63684
|
+
if (spliced === null) {
|
|
63685
|
+
return toolError(
|
|
63686
|
+
new Error(
|
|
63687
|
+
`Section "${after_section}" not found. Use headings_only to see available sections.`
|
|
63688
|
+
)
|
|
63689
|
+
);
|
|
63690
|
+
}
|
|
63691
|
+
newBody = spliced;
|
|
63692
|
+
placementNote = ` in section "${after_section}"`;
|
|
63693
|
+
} else {
|
|
63694
|
+
newBody = append ? `${existingBody}
|
|
63057
63695
|
${macro}` : macro;
|
|
63696
|
+
}
|
|
63058
63697
|
const prepared = await safePrepareBody({
|
|
63059
63698
|
body: newBody,
|
|
63060
63699
|
currentBody: existingBody,
|
|
63061
63700
|
scope: "full"
|
|
63062
|
-
// append=true is additive but newBody already contains the concat,
|
|
63063
|
-
// so "full" is correct: guards compare existingBody vs the complete
|
|
63064
|
-
// new body, which is what we want for both branches.
|
|
63065
63701
|
});
|
|
63066
63702
|
const submitted = await safeSubmitPage({
|
|
63067
63703
|
pageId: page_id,
|
|
@@ -63080,7 +63716,7 @@ ${macro}` : macro;
|
|
|
63080
63716
|
const badgeResult = await markPageUnverified(submitted.page.id, config3);
|
|
63081
63717
|
if (badgeResult.warning) warnings.push(badgeResult.warning);
|
|
63082
63718
|
return toolResult(
|
|
63083
|
-
appendWarnings(`Diagram "${filename}" added to page ${submitted.page.title} (ID: ${submitted.page.id}, version: ${submitted.newVersion}, attachment ID: ${attachmentId}, macro ID: ${macroId})`, warnings) + echo
|
|
63719
|
+
appendWarnings(`Diagram "${filename}" added to page ${submitted.page.title}${placementNote} (ID: ${submitted.page.id}, version: ${submitted.newVersion}, attachment ID: ${attachmentId}, macro ID: ${macroId})`, warnings) + echo
|
|
63084
63720
|
);
|
|
63085
63721
|
} catch (err) {
|
|
63086
63722
|
return toolErrorWithContext(err, { operation: "add_drawio_diagram", resource: `page ${page_id}`, profile: config3.profile });
|
|
@@ -63867,7 +64503,7 @@ ${titleFenced}${echo2}`
|
|
|
63867
64503
|
inputSchema: {}
|
|
63868
64504
|
},
|
|
63869
64505
|
async () => {
|
|
63870
|
-
let text2 = `epimethian-mcp v${"6.
|
|
64506
|
+
let text2 = `epimethian-mcp v${"6.9.0"}`;
|
|
63871
64507
|
try {
|
|
63872
64508
|
const pending = await getPendingUpdate();
|
|
63873
64509
|
if (pending) {
|
|
@@ -63898,7 +64534,7 @@ ${label} update available: v${pending.current} \u2192 v${pending.latest}. Run \`
|
|
|
63898
64534
|
const pending = await getPendingUpdate();
|
|
63899
64535
|
if (!pending) {
|
|
63900
64536
|
return toolResult(
|
|
63901
|
-
`epimethian-mcp v${"6.
|
|
64537
|
+
`epimethian-mcp v${"6.9.0"} is already up to date.`
|
|
63902
64538
|
);
|
|
63903
64539
|
}
|
|
63904
64540
|
const output = await performUpgrade(pending.latest);
|
|
@@ -63920,7 +64556,7 @@ async function startRecoveryServer(profile) {
|
|
|
63920
64556
|
const server = new McpServer(
|
|
63921
64557
|
{
|
|
63922
64558
|
name: `confluence-${profile}-setup-needed`,
|
|
63923
|
-
version: "6.
|
|
64559
|
+
version: "6.9.0"
|
|
63924
64560
|
},
|
|
63925
64561
|
{
|
|
63926
64562
|
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 +64607,21 @@ async function main() {
|
|
|
63971
64607
|
const serverName = config3.profile ? `confluence-${config3.profile}` : "confluence";
|
|
63972
64608
|
const server = new McpServer({
|
|
63973
64609
|
name: serverName,
|
|
63974
|
-
version: "6.
|
|
64610
|
+
version: "6.9.0"
|
|
63975
64611
|
});
|
|
63976
64612
|
await registerTools(server, config3);
|
|
63977
64613
|
const transport = new StdioServerTransport();
|
|
63978
64614
|
await server.connect(transport);
|
|
63979
64615
|
try {
|
|
63980
64616
|
const pending = await getPendingUpdate();
|
|
63981
|
-
if (pending && pending.current === "6.
|
|
64617
|
+
if (pending && pending.current === "6.9.0") {
|
|
63982
64618
|
console.error(
|
|
63983
64619
|
`epimethian-mcp: update available: v${pending.current} \u2192 v${pending.latest} (${pending.type}). Run \`epimethian-mcp upgrade\` to install.`
|
|
63984
64620
|
);
|
|
63985
64621
|
}
|
|
63986
64622
|
} catch {
|
|
63987
64623
|
}
|
|
63988
|
-
checkForUpdates("6.
|
|
64624
|
+
checkForUpdates("6.9.0").catch(() => {
|
|
63989
64625
|
});
|
|
63990
64626
|
}
|
|
63991
64627
|
|