@de-otio/epimethian-mcp 6.4.1 → 6.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -0
- package/dist/cli/index.js +890 -129
- 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.6.1"}`;
|
|
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.6.1"}`;
|
|
35506
35506
|
const effectiveClient = cfg.attribution ? opts.clientLabel : void 0;
|
|
35507
35507
|
let versionMessage;
|
|
35508
35508
|
if (opts.versionMessage && effectiveClient)
|
|
@@ -36504,6 +36504,25 @@ var init_confluence_client = __esm({
|
|
|
36504
36504
|
}
|
|
36505
36505
|
});
|
|
36506
36506
|
|
|
36507
|
+
// src/server/converter/types.ts
|
|
36508
|
+
var ConverterError, SHRINKAGE_NOT_CONFIRMED, STRUCTURE_LOSS_NOT_CONFIRMED, EMPTY_BODY_REJECTED, CONTENT_FLOOR_BREACHED;
|
|
36509
|
+
var init_types2 = __esm({
|
|
36510
|
+
"src/server/converter/types.ts"() {
|
|
36511
|
+
"use strict";
|
|
36512
|
+
ConverterError = class extends Error {
|
|
36513
|
+
constructor(message, code2) {
|
|
36514
|
+
super(message);
|
|
36515
|
+
this.code = code2;
|
|
36516
|
+
this.name = "ConverterError";
|
|
36517
|
+
}
|
|
36518
|
+
};
|
|
36519
|
+
SHRINKAGE_NOT_CONFIRMED = "SHRINKAGE_NOT_CONFIRMED";
|
|
36520
|
+
STRUCTURE_LOSS_NOT_CONFIRMED = "STRUCTURE_LOSS_NOT_CONFIRMED";
|
|
36521
|
+
EMPTY_BODY_REJECTED = "EMPTY_BODY_REJECTED";
|
|
36522
|
+
CONTENT_FLOOR_BREACHED = "CONTENT_FLOOR_BREACHED";
|
|
36523
|
+
}
|
|
36524
|
+
});
|
|
36525
|
+
|
|
36507
36526
|
// src/server/converter/tokeniser.ts
|
|
36508
36527
|
function formatTokenId(n) {
|
|
36509
36528
|
return "T" + String(n).padStart(4, "0");
|
|
@@ -36645,6 +36664,221 @@ var init_mutation_log = __esm({
|
|
|
36645
36664
|
}
|
|
36646
36665
|
});
|
|
36647
36666
|
|
|
36667
|
+
// src/server/confirmation-tokens.ts
|
|
36668
|
+
function clampTtl(ttlMs) {
|
|
36669
|
+
if (!Number.isFinite(ttlMs)) return DEFAULT_SOFT_CONFIRM_TTL_MS;
|
|
36670
|
+
if (ttlMs < TTL_MIN_MS) return TTL_MIN_MS;
|
|
36671
|
+
if (ttlMs > TTL_MAX_MS) return TTL_MAX_MS;
|
|
36672
|
+
return ttlMs;
|
|
36673
|
+
}
|
|
36674
|
+
function getMintLimit() {
|
|
36675
|
+
const raw = process.env.EPIMETHIAN_SOFT_CONFIRM_MINT_LIMIT;
|
|
36676
|
+
if (raw === void 0) return MAX_MINTS_PER_15_MIN;
|
|
36677
|
+
const n = parseInt(raw, 10);
|
|
36678
|
+
if (!Number.isFinite(n) || n < 0) return MAX_MINTS_PER_15_MIN;
|
|
36679
|
+
return n;
|
|
36680
|
+
}
|
|
36681
|
+
function getDefaultTtl() {
|
|
36682
|
+
const raw = process.env.EPIMETHIAN_SOFT_CONFIRM_TTL_MS;
|
|
36683
|
+
if (raw === void 0) return DEFAULT_SOFT_CONFIRM_TTL_MS;
|
|
36684
|
+
const n = parseInt(raw, 10);
|
|
36685
|
+
if (!Number.isFinite(n)) return DEFAULT_SOFT_CONFIRM_TTL_MS;
|
|
36686
|
+
return clampTtl(n);
|
|
36687
|
+
}
|
|
36688
|
+
function emitMint(meta) {
|
|
36689
|
+
for (const h of mintHandlers) {
|
|
36690
|
+
try {
|
|
36691
|
+
h(meta);
|
|
36692
|
+
} catch {
|
|
36693
|
+
}
|
|
36694
|
+
}
|
|
36695
|
+
}
|
|
36696
|
+
function emitValidate(meta) {
|
|
36697
|
+
for (const h of validateHandlers) {
|
|
36698
|
+
try {
|
|
36699
|
+
h(meta);
|
|
36700
|
+
} catch {
|
|
36701
|
+
}
|
|
36702
|
+
}
|
|
36703
|
+
}
|
|
36704
|
+
function sleepUntil(targetWallClockMs) {
|
|
36705
|
+
return new Promise((resolve2) => {
|
|
36706
|
+
const remaining = targetWallClockMs - Date.now();
|
|
36707
|
+
if (remaining <= 0) {
|
|
36708
|
+
resolve2();
|
|
36709
|
+
return;
|
|
36710
|
+
}
|
|
36711
|
+
setTimeout(resolve2, remaining);
|
|
36712
|
+
});
|
|
36713
|
+
}
|
|
36714
|
+
function pruneMintTimestamps(now) {
|
|
36715
|
+
const cutoff = now - MINT_WINDOW_MS;
|
|
36716
|
+
if (mintTimestamps.length === 0) return;
|
|
36717
|
+
if (mintTimestamps[0] >= cutoff) return;
|
|
36718
|
+
mintTimestamps = mintTimestamps.filter((ts) => ts >= cutoff);
|
|
36719
|
+
}
|
|
36720
|
+
function evictOldest() {
|
|
36721
|
+
let oldestKey;
|
|
36722
|
+
let oldestSeq = Infinity;
|
|
36723
|
+
for (const [k, v] of store.entries()) {
|
|
36724
|
+
if (v.insertSeq < oldestSeq) {
|
|
36725
|
+
oldestSeq = v.insertSeq;
|
|
36726
|
+
oldestKey = k;
|
|
36727
|
+
}
|
|
36728
|
+
}
|
|
36729
|
+
if (oldestKey === void 0) return;
|
|
36730
|
+
const entry = store.get(oldestKey);
|
|
36731
|
+
store.delete(oldestKey);
|
|
36732
|
+
emitValidate({
|
|
36733
|
+
auditId: entry.auditId,
|
|
36734
|
+
tool: entry.ctx.tool,
|
|
36735
|
+
cloudId: entry.ctx.cloudId,
|
|
36736
|
+
pageId: entry.ctx.pageId,
|
|
36737
|
+
outcome: "evicted"
|
|
36738
|
+
});
|
|
36739
|
+
}
|
|
36740
|
+
function mintToken(ctx, ttlMs) {
|
|
36741
|
+
const now = Date.now();
|
|
36742
|
+
pruneMintTimestamps(now);
|
|
36743
|
+
const limit = getMintLimit();
|
|
36744
|
+
if (limit > 0 && mintTimestamps.length >= limit) {
|
|
36745
|
+
const oldest = mintTimestamps[0];
|
|
36746
|
+
const waitMs = Math.max(0, oldest + MINT_WINDOW_MS - now);
|
|
36747
|
+
throw new SoftConfirmRateLimitedError(
|
|
36748
|
+
mintTimestamps.length,
|
|
36749
|
+
limit,
|
|
36750
|
+
waitMs
|
|
36751
|
+
);
|
|
36752
|
+
}
|
|
36753
|
+
while (store.size >= MAX_OUTSTANDING_TOKENS) {
|
|
36754
|
+
evictOldest();
|
|
36755
|
+
}
|
|
36756
|
+
const resolvedTtl = clampTtl(ttlMs ?? getDefaultTtl());
|
|
36757
|
+
const expiresAt = now + resolvedTtl;
|
|
36758
|
+
const auditId = (0, import_node_crypto4.randomUUID)();
|
|
36759
|
+
const tokenStr = (0, import_node_crypto4.randomBytes)(24).toString("base64url");
|
|
36760
|
+
store.set(tokenStr, {
|
|
36761
|
+
auditId,
|
|
36762
|
+
ctx: { ...ctx },
|
|
36763
|
+
expiresAt,
|
|
36764
|
+
insertSeq: ++insertSeqCounter
|
|
36765
|
+
});
|
|
36766
|
+
mintTimestamps.push(now);
|
|
36767
|
+
emitMint({
|
|
36768
|
+
auditId,
|
|
36769
|
+
tool: ctx.tool,
|
|
36770
|
+
cloudId: ctx.cloudId,
|
|
36771
|
+
pageId: ctx.pageId,
|
|
36772
|
+
pageVersion: ctx.pageVersion,
|
|
36773
|
+
expiresAt,
|
|
36774
|
+
outstanding: store.size
|
|
36775
|
+
});
|
|
36776
|
+
return { token: tokenStr, auditId, expiresAt };
|
|
36777
|
+
}
|
|
36778
|
+
async function validateToken(token, ctx) {
|
|
36779
|
+
const floorTarget = Date.now() + MIN_VALIDATE_FLOOR_MS;
|
|
36780
|
+
let outcome;
|
|
36781
|
+
let auditId;
|
|
36782
|
+
const entry = store.get(token);
|
|
36783
|
+
if (!entry) {
|
|
36784
|
+
outcome = "unknown";
|
|
36785
|
+
} else {
|
|
36786
|
+
auditId = entry.auditId;
|
|
36787
|
+
const now = Date.now();
|
|
36788
|
+
if (now >= entry.expiresAt) {
|
|
36789
|
+
store.delete(token);
|
|
36790
|
+
outcome = "expired";
|
|
36791
|
+
} else if (entry.ctx.tool !== ctx.tool || entry.ctx.cloudId !== ctx.cloudId || entry.ctx.pageId !== ctx.pageId || entry.ctx.pageVersion !== ctx.pageVersion || entry.ctx.diffHash !== ctx.diffHash) {
|
|
36792
|
+
outcome = "mismatch";
|
|
36793
|
+
} else {
|
|
36794
|
+
store.delete(token);
|
|
36795
|
+
const siblings = [];
|
|
36796
|
+
for (const [k, v] of store.entries()) {
|
|
36797
|
+
if (v.ctx.cloudId === entry.ctx.cloudId && v.ctx.pageId === entry.ctx.pageId) {
|
|
36798
|
+
siblings.push([k, v]);
|
|
36799
|
+
}
|
|
36800
|
+
}
|
|
36801
|
+
for (const [k, v] of siblings) {
|
|
36802
|
+
store.delete(k);
|
|
36803
|
+
emitValidate({
|
|
36804
|
+
auditId: v.auditId,
|
|
36805
|
+
tool: v.ctx.tool,
|
|
36806
|
+
cloudId: v.ctx.cloudId,
|
|
36807
|
+
pageId: v.ctx.pageId,
|
|
36808
|
+
outcome: "stale"
|
|
36809
|
+
});
|
|
36810
|
+
}
|
|
36811
|
+
outcome = "ok";
|
|
36812
|
+
}
|
|
36813
|
+
}
|
|
36814
|
+
emitValidate({
|
|
36815
|
+
auditId,
|
|
36816
|
+
tool: ctx.tool,
|
|
36817
|
+
cloudId: ctx.cloudId,
|
|
36818
|
+
pageId: ctx.pageId,
|
|
36819
|
+
outcome
|
|
36820
|
+
});
|
|
36821
|
+
await sleepUntil(floorTarget);
|
|
36822
|
+
return outcome === "ok" ? "ok" : "invalid";
|
|
36823
|
+
}
|
|
36824
|
+
function invalidateForPage(cloudId, pageId) {
|
|
36825
|
+
const victims = [];
|
|
36826
|
+
for (const [k, v] of store.entries()) {
|
|
36827
|
+
if (v.ctx.cloudId === cloudId && v.ctx.pageId === pageId) {
|
|
36828
|
+
victims.push([k, v]);
|
|
36829
|
+
}
|
|
36830
|
+
}
|
|
36831
|
+
for (const [k, v] of victims) {
|
|
36832
|
+
store.delete(k);
|
|
36833
|
+
emitValidate({
|
|
36834
|
+
auditId: v.auditId,
|
|
36835
|
+
tool: v.ctx.tool,
|
|
36836
|
+
cloudId: v.ctx.cloudId,
|
|
36837
|
+
pageId: v.ctx.pageId,
|
|
36838
|
+
outcome: "stale"
|
|
36839
|
+
});
|
|
36840
|
+
}
|
|
36841
|
+
}
|
|
36842
|
+
function computeDiffHash(canonicalStorageXml, pageVersion) {
|
|
36843
|
+
return (0, import_node_crypto4.createHash)("sha256").update(`${canonicalStorageXml}
|
|
36844
|
+
${pageVersion}`).digest("hex");
|
|
36845
|
+
}
|
|
36846
|
+
var import_node_crypto4, DEFAULT_SOFT_CONFIRM_TTL_MS, TTL_MIN_MS, TTL_MAX_MS, MAX_OUTSTANDING_TOKENS, MAX_MINTS_PER_15_MIN, MINT_WINDOW_MS, MIN_VALIDATE_FLOOR_MS, SOFT_CONFIRM_RATE_LIMITED, SoftConfirmRateLimitedError, store, mintTimestamps, insertSeqCounter, mintHandlers, validateHandlers;
|
|
36847
|
+
var init_confirmation_tokens = __esm({
|
|
36848
|
+
"src/server/confirmation-tokens.ts"() {
|
|
36849
|
+
"use strict";
|
|
36850
|
+
import_node_crypto4 = require("node:crypto");
|
|
36851
|
+
DEFAULT_SOFT_CONFIRM_TTL_MS = 5 * 60 * 1e3;
|
|
36852
|
+
TTL_MIN_MS = 6e4;
|
|
36853
|
+
TTL_MAX_MS = 9e5;
|
|
36854
|
+
MAX_OUTSTANDING_TOKENS = 50;
|
|
36855
|
+
MAX_MINTS_PER_15_MIN = 100;
|
|
36856
|
+
MINT_WINDOW_MS = 15 * 60 * 1e3;
|
|
36857
|
+
MIN_VALIDATE_FLOOR_MS = 5;
|
|
36858
|
+
SOFT_CONFIRM_RATE_LIMITED = "SOFT_CONFIRM_RATE_LIMITED";
|
|
36859
|
+
SoftConfirmRateLimitedError = class extends Error {
|
|
36860
|
+
code = SOFT_CONFIRM_RATE_LIMITED;
|
|
36861
|
+
current;
|
|
36862
|
+
limit;
|
|
36863
|
+
waitMs;
|
|
36864
|
+
constructor(current, limit, waitMs) {
|
|
36865
|
+
super(
|
|
36866
|
+
`Soft-confirmation mint cap exhausted: ${current} mints in the last 15 min, limit ${limit}. Window opens again in ~${Math.ceil(waitMs / 6e4)} min. Override via EPIMETHIAN_SOFT_CONFIRM_MINT_LIMIT (set "0" to disable).`
|
|
36867
|
+
);
|
|
36868
|
+
this.name = "SoftConfirmRateLimitedError";
|
|
36869
|
+
this.current = current;
|
|
36870
|
+
this.limit = limit;
|
|
36871
|
+
this.waitMs = waitMs;
|
|
36872
|
+
}
|
|
36873
|
+
};
|
|
36874
|
+
store = /* @__PURE__ */ new Map();
|
|
36875
|
+
mintTimestamps = [];
|
|
36876
|
+
insertSeqCounter = 0;
|
|
36877
|
+
mintHandlers = [];
|
|
36878
|
+
validateHandlers = [];
|
|
36879
|
+
}
|
|
36880
|
+
});
|
|
36881
|
+
|
|
36648
36882
|
// node_modules/mdurl/lib/decode.mjs
|
|
36649
36883
|
function getDecodeCache(exclude) {
|
|
36650
36884
|
let cache = decodeCache[exclude];
|
|
@@ -46232,25 +46466,6 @@ var init_account_id_validator = __esm({
|
|
|
46232
46466
|
}
|
|
46233
46467
|
});
|
|
46234
46468
|
|
|
46235
|
-
// src/server/converter/types.ts
|
|
46236
|
-
var ConverterError, SHRINKAGE_NOT_CONFIRMED, STRUCTURE_LOSS_NOT_CONFIRMED, EMPTY_BODY_REJECTED, CONTENT_FLOOR_BREACHED;
|
|
46237
|
-
var init_types2 = __esm({
|
|
46238
|
-
"src/server/converter/types.ts"() {
|
|
46239
|
-
"use strict";
|
|
46240
|
-
ConverterError = class extends Error {
|
|
46241
|
-
constructor(message, code2) {
|
|
46242
|
-
super(message);
|
|
46243
|
-
this.code = code2;
|
|
46244
|
-
this.name = "ConverterError";
|
|
46245
|
-
}
|
|
46246
|
-
};
|
|
46247
|
-
SHRINKAGE_NOT_CONFIRMED = "SHRINKAGE_NOT_CONFIRMED";
|
|
46248
|
-
STRUCTURE_LOSS_NOT_CONFIRMED = "STRUCTURE_LOSS_NOT_CONFIRMED";
|
|
46249
|
-
EMPTY_BODY_REJECTED = "EMPTY_BODY_REJECTED";
|
|
46250
|
-
CONTENT_FLOOR_BREACHED = "CONTENT_FLOOR_BREACHED";
|
|
46251
|
-
}
|
|
46252
|
-
});
|
|
46253
|
-
|
|
46254
46469
|
// src/server/converter/md-to-storage.ts
|
|
46255
46470
|
function createHeadingSlugger() {
|
|
46256
46471
|
const seen = /* @__PURE__ */ new Map();
|
|
@@ -47649,6 +47864,53 @@ function suppressEquivalentDeletionsEnabled() {
|
|
|
47649
47864
|
const v = process.env.EPIMETHIAN_SUPPRESS_EQUIVALENT_DELETIONS;
|
|
47650
47865
|
return v === "true" || v === "1";
|
|
47651
47866
|
}
|
|
47867
|
+
async function maybeConsumeConfirmToken(args) {
|
|
47868
|
+
const { confirm_token, tool, cloudId, pageId, pageVersion, diffHash } = args;
|
|
47869
|
+
if (confirm_token === void 0 || cloudId === void 0 || pageVersion <= 0 || diffHash === void 0) {
|
|
47870
|
+
return "no_token";
|
|
47871
|
+
}
|
|
47872
|
+
const outcome = await validateToken(confirm_token, {
|
|
47873
|
+
tool,
|
|
47874
|
+
cloudId,
|
|
47875
|
+
pageId,
|
|
47876
|
+
pageVersion,
|
|
47877
|
+
diffHash
|
|
47878
|
+
});
|
|
47879
|
+
return outcome;
|
|
47880
|
+
}
|
|
47881
|
+
function formatSoftConfirmationResult(err, params) {
|
|
47882
|
+
const last8 = err.token.slice(-8);
|
|
47883
|
+
const isoExpires = new Date(err.expiresAt).toISOString();
|
|
47884
|
+
const text2 = `\u26A0\uFE0F Confirmation required (SOFT_CONFIRMATION_REQUIRED)
|
|
47885
|
+
|
|
47886
|
+
${err.humanSummary}
|
|
47887
|
+
|
|
47888
|
+
Your MCP client does not support in-protocol elicitation. This
|
|
47889
|
+
confirmation is being routed through you (the agent). Please ASK
|
|
47890
|
+
THE USER before retrying. If the user approves, re-call this tool
|
|
47891
|
+
with the same parameters plus the \`confirm_token\` from
|
|
47892
|
+
structuredContent.
|
|
47893
|
+
|
|
47894
|
+
Token tail: ...${last8} Expires: ${isoExpires} Audit ID: ${err.auditId}
|
|
47895
|
+
|
|
47896
|
+
The token is single-use, bound to this exact diff and page version,
|
|
47897
|
+
and invalidated by any competing write to this page. If validation
|
|
47898
|
+
fails, mint a new one by re-calling without \`confirm_token\`.`;
|
|
47899
|
+
const structuredContent = {
|
|
47900
|
+
confirm_token: err.token,
|
|
47901
|
+
audit_id: err.auditId,
|
|
47902
|
+
expires_at: isoExpires,
|
|
47903
|
+
page_id: params.pageId
|
|
47904
|
+
};
|
|
47905
|
+
if (params.deletionSummary) {
|
|
47906
|
+
structuredContent.deletion_summary = params.deletionSummary;
|
|
47907
|
+
}
|
|
47908
|
+
return {
|
|
47909
|
+
content: [{ type: "text", text: text2 }],
|
|
47910
|
+
isError: true,
|
|
47911
|
+
structuredContent
|
|
47912
|
+
};
|
|
47913
|
+
}
|
|
47652
47914
|
function detectMixedInput(body) {
|
|
47653
47915
|
let stripped = body.replace(
|
|
47654
47916
|
/^(`{3,})[^\n]*\n[\s\S]*?^\1\s*$/gm,
|
|
@@ -47988,7 +48250,8 @@ async function safeSubmitPage(input) {
|
|
|
47988
48250
|
confirmStructureLoss,
|
|
47989
48251
|
confirmDeletions,
|
|
47990
48252
|
source,
|
|
47991
|
-
assertGrowth
|
|
48253
|
+
assertGrowth,
|
|
48254
|
+
cloudId
|
|
47992
48255
|
} = input;
|
|
47993
48256
|
const isCreate = pageId === void 0;
|
|
47994
48257
|
const resolvedOperation = operation ?? (isCreate ? "create_page" : "update_page");
|
|
@@ -48125,6 +48388,9 @@ async function safeSubmitPage(input) {
|
|
|
48125
48388
|
});
|
|
48126
48389
|
} catch {
|
|
48127
48390
|
}
|
|
48391
|
+
if (cloudId !== void 0 && pageId !== void 0) {
|
|
48392
|
+
invalidateForPage(cloudId, pageId);
|
|
48393
|
+
}
|
|
48128
48394
|
return {
|
|
48129
48395
|
page,
|
|
48130
48396
|
newVersion,
|
|
@@ -48355,6 +48621,7 @@ var init_safe_write = __esm({
|
|
|
48355
48621
|
"src/server/safe-write.ts"() {
|
|
48356
48622
|
"use strict";
|
|
48357
48623
|
init_confluence_client();
|
|
48624
|
+
init_confirmation_tokens();
|
|
48358
48625
|
init_md_to_storage();
|
|
48359
48626
|
init_update_orchestrator();
|
|
48360
48627
|
init_content_safety_guards();
|
|
@@ -48482,7 +48749,7 @@ async function writeCheckState(state) {
|
|
|
48482
48749
|
const data = JSON.stringify(state, null, 2) + "\n";
|
|
48483
48750
|
const tmpFile = (0, import_node_path3.join)(
|
|
48484
48751
|
CONFIG_DIR2,
|
|
48485
|
-
`.update-check.${(0,
|
|
48752
|
+
`.update-check.${(0, import_node_crypto5.randomBytes)(4).toString("hex")}.tmp`
|
|
48486
48753
|
);
|
|
48487
48754
|
await (0, import_promises3.writeFile)(tmpFile, data, { mode: 384 });
|
|
48488
48755
|
await (0, import_promises3.rename)(tmpFile, UPDATE_CHECK_FILE);
|
|
@@ -48623,14 +48890,14 @@ async function checkForUpdates(currentVersion) {
|
|
|
48623
48890
|
return null;
|
|
48624
48891
|
}
|
|
48625
48892
|
}
|
|
48626
|
-
var import_promises3, import_node_path3, import_node_os2,
|
|
48893
|
+
var import_promises3, import_node_path3, import_node_os2, import_node_crypto5, import_node_child_process2, import_node_util, execFileAsync, CONFIG_DIR2, UPDATE_CHECK_FILE, ONE_DAY_MS, NPM_REGISTRY_URL, PACKAGE_NAME;
|
|
48627
48894
|
var init_update_check = __esm({
|
|
48628
48895
|
"src/shared/update-check.ts"() {
|
|
48629
48896
|
"use strict";
|
|
48630
48897
|
import_promises3 = require("node:fs/promises");
|
|
48631
48898
|
import_node_path3 = require("node:path");
|
|
48632
48899
|
import_node_os2 = require("node:os");
|
|
48633
|
-
|
|
48900
|
+
import_node_crypto5 = require("node:crypto");
|
|
48634
48901
|
import_node_child_process2 = require("node:child_process");
|
|
48635
48902
|
import_node_util = require("node:util");
|
|
48636
48903
|
init_safe_fs();
|
|
@@ -48643,11 +48910,182 @@ var init_update_check = __esm({
|
|
|
48643
48910
|
}
|
|
48644
48911
|
});
|
|
48645
48912
|
|
|
48913
|
+
// src/cli/client-configs.ts
|
|
48914
|
+
var client_configs_exports = {};
|
|
48915
|
+
__export(client_configs_exports, {
|
|
48916
|
+
CLIENT_CONFIGS: () => CLIENT_CONFIGS,
|
|
48917
|
+
knownClientIds: () => knownClientIds,
|
|
48918
|
+
renderConfigSnippet: () => renderConfigSnippet
|
|
48919
|
+
});
|
|
48920
|
+
function renderConfigSnippet(clientId, profile, binPath) {
|
|
48921
|
+
const entry = CLIENT_CONFIGS.find((c) => c.id === clientId);
|
|
48922
|
+
if (!entry) {
|
|
48923
|
+
const valid = knownClientIds().join(", ");
|
|
48924
|
+
throw new Error(
|
|
48925
|
+
`Unknown client ID "${clientId}". Valid IDs are: ${valid}`
|
|
48926
|
+
);
|
|
48927
|
+
}
|
|
48928
|
+
const snippet = entry.template.replace(/\{\{PROFILE\}\}/g, profile).replace(/\{\{BIN\}\}/g, binPath);
|
|
48929
|
+
return { snippet, ...entry.warning ? { warning: entry.warning } : {} };
|
|
48930
|
+
}
|
|
48931
|
+
function knownClientIds() {
|
|
48932
|
+
return CLIENT_CONFIGS.map((c) => c.id);
|
|
48933
|
+
}
|
|
48934
|
+
var CLIENT_CONFIGS;
|
|
48935
|
+
var init_client_configs = __esm({
|
|
48936
|
+
"src/cli/client-configs.ts"() {
|
|
48937
|
+
"use strict";
|
|
48938
|
+
CLIENT_CONFIGS = [
|
|
48939
|
+
{
|
|
48940
|
+
id: "claude-code",
|
|
48941
|
+
displayName: "Claude Code",
|
|
48942
|
+
configFileHint: ".mcp.json",
|
|
48943
|
+
template: JSON.stringify(
|
|
48944
|
+
{
|
|
48945
|
+
mcpServers: {
|
|
48946
|
+
"epimethian-mcp": {
|
|
48947
|
+
command: "{{BIN}}",
|
|
48948
|
+
args: ["--profile", "{{PROFILE}}"]
|
|
48949
|
+
}
|
|
48950
|
+
}
|
|
48951
|
+
},
|
|
48952
|
+
null,
|
|
48953
|
+
2
|
|
48954
|
+
)
|
|
48955
|
+
},
|
|
48956
|
+
{
|
|
48957
|
+
id: "claude-desktop",
|
|
48958
|
+
displayName: "Claude Desktop",
|
|
48959
|
+
configFileHint: "~/Library/Application Support/Claude/claude_desktop_config.json (macOS) / %APPDATA%\\Claude\\claude_desktop_config.json (Windows) / ~/.config/Claude/claude_desktop_config.json (Linux)",
|
|
48960
|
+
template: JSON.stringify(
|
|
48961
|
+
{
|
|
48962
|
+
mcpServers: {
|
|
48963
|
+
"epimethian-mcp": {
|
|
48964
|
+
command: "{{BIN}}",
|
|
48965
|
+
args: ["--profile", "{{PROFILE}}"]
|
|
48966
|
+
}
|
|
48967
|
+
}
|
|
48968
|
+
},
|
|
48969
|
+
null,
|
|
48970
|
+
2
|
|
48971
|
+
)
|
|
48972
|
+
},
|
|
48973
|
+
{
|
|
48974
|
+
id: "claude-code-vscode",
|
|
48975
|
+
displayName: "Claude Code (VS Code extension)",
|
|
48976
|
+
configFileHint: "VS Code settings.json (mcp.servers block)",
|
|
48977
|
+
template: JSON.stringify(
|
|
48978
|
+
{
|
|
48979
|
+
"mcp.servers": {
|
|
48980
|
+
"epimethian-mcp": {
|
|
48981
|
+
command: "{{BIN}}",
|
|
48982
|
+
args: ["--profile", "{{PROFILE}}"]
|
|
48983
|
+
}
|
|
48984
|
+
}
|
|
48985
|
+
},
|
|
48986
|
+
null,
|
|
48987
|
+
2
|
|
48988
|
+
),
|
|
48989
|
+
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`."
|
|
48990
|
+
},
|
|
48991
|
+
{
|
|
48992
|
+
id: "cursor",
|
|
48993
|
+
displayName: "Cursor",
|
|
48994
|
+
configFileHint: ".cursor/mcp.json",
|
|
48995
|
+
template: JSON.stringify(
|
|
48996
|
+
{
|
|
48997
|
+
mcpServers: {
|
|
48998
|
+
"epimethian-mcp": {
|
|
48999
|
+
command: "{{BIN}}",
|
|
49000
|
+
args: ["--profile", "{{PROFILE}}"]
|
|
49001
|
+
}
|
|
49002
|
+
}
|
|
49003
|
+
},
|
|
49004
|
+
null,
|
|
49005
|
+
2
|
|
49006
|
+
)
|
|
49007
|
+
},
|
|
49008
|
+
{
|
|
49009
|
+
id: "windsurf",
|
|
49010
|
+
displayName: "Windsurf",
|
|
49011
|
+
configFileHint: "~/.codeium/windsurf/mcp_config.json",
|
|
49012
|
+
template: JSON.stringify(
|
|
49013
|
+
{
|
|
49014
|
+
mcpServers: {
|
|
49015
|
+
"epimethian-mcp": {
|
|
49016
|
+
command: "{{BIN}}",
|
|
49017
|
+
args: ["--profile", "{{PROFILE}}"]
|
|
49018
|
+
}
|
|
49019
|
+
}
|
|
49020
|
+
},
|
|
49021
|
+
null,
|
|
49022
|
+
2
|
|
49023
|
+
)
|
|
49024
|
+
},
|
|
49025
|
+
{
|
|
49026
|
+
id: "zed",
|
|
49027
|
+
displayName: "Zed",
|
|
49028
|
+
configFileHint: "~/.config/zed/settings.json (context_servers block)",
|
|
49029
|
+
template: JSON.stringify(
|
|
49030
|
+
{
|
|
49031
|
+
context_servers: {
|
|
49032
|
+
"epimethian-mcp": {
|
|
49033
|
+
command: {
|
|
49034
|
+
path: "{{BIN}}",
|
|
49035
|
+
args: ["--profile", "{{PROFILE}}"]
|
|
49036
|
+
}
|
|
49037
|
+
}
|
|
49038
|
+
}
|
|
49039
|
+
},
|
|
49040
|
+
null,
|
|
49041
|
+
2
|
|
49042
|
+
)
|
|
49043
|
+
},
|
|
49044
|
+
{
|
|
49045
|
+
id: "opencode",
|
|
49046
|
+
displayName: "OpenCode",
|
|
49047
|
+
configFileHint: "opencode.json or ~/.config/opencode/opencode.json",
|
|
49048
|
+
template: JSON.stringify(
|
|
49049
|
+
{
|
|
49050
|
+
mcp: {
|
|
49051
|
+
"epimethian-mcp": {
|
|
49052
|
+
type: "local",
|
|
49053
|
+
command: ["{{BIN}}", "--profile", "{{PROFILE}}"],
|
|
49054
|
+
environment: {
|
|
49055
|
+
EPIMETHIAN_ALLOW_UNGATED_WRITES: "true"
|
|
49056
|
+
}
|
|
49057
|
+
}
|
|
49058
|
+
}
|
|
49059
|
+
},
|
|
49060
|
+
null,
|
|
49061
|
+
2
|
|
49062
|
+
),
|
|
49063
|
+
warning: "OpenCode does not yet support MCP elicitation. The `EPIMETHIAN_ALLOW_UNGATED_WRITES=true` env var above removes the interactive confirmation prompt for destructive operations. Read tools and additive writes work without any flag. Upgrade to epimethian-mcp v6.6.0 to get soft elicitation (confirmations routed through the agent), and remove the env var when you do."
|
|
49064
|
+
}
|
|
49065
|
+
];
|
|
49066
|
+
}
|
|
49067
|
+
});
|
|
49068
|
+
|
|
48646
49069
|
// src/cli/setup.ts
|
|
48647
49070
|
var setup_exports = {};
|
|
48648
49071
|
__export(setup_exports, {
|
|
48649
49072
|
runSetup: () => runSetup
|
|
48650
49073
|
});
|
|
49074
|
+
function resolveBinPath() {
|
|
49075
|
+
const argv1 = process.argv[1];
|
|
49076
|
+
if (argv1 && argv1.startsWith("/")) {
|
|
49077
|
+
return argv1;
|
|
49078
|
+
}
|
|
49079
|
+
try {
|
|
49080
|
+
const result = (0, import_node_child_process3.execSync)("which epimethian-mcp", { encoding: "utf8" }).trim();
|
|
49081
|
+
if (result) return result;
|
|
49082
|
+
} catch {
|
|
49083
|
+
}
|
|
49084
|
+
process.stderr.write(
|
|
49085
|
+
"Warning: could not determine absolute path to epimethian-mcp. Replace <absolute path to epimethian-mcp> in the snippet below with the correct path.\n"
|
|
49086
|
+
);
|
|
49087
|
+
return "<absolute path to epimethian-mcp>";
|
|
49088
|
+
}
|
|
48651
49089
|
function readPassword(prompt) {
|
|
48652
49090
|
import_node_process2.stdout.write(prompt);
|
|
48653
49091
|
return new Promise((resolve2) => {
|
|
@@ -48678,13 +49116,22 @@ function readPassword(prompt) {
|
|
|
48678
49116
|
import_node_process2.stdin.on("data", onData);
|
|
48679
49117
|
});
|
|
48680
49118
|
}
|
|
48681
|
-
async function runSetup(profile) {
|
|
49119
|
+
async function runSetup(profile, clientId) {
|
|
48682
49120
|
if (!import_node_process2.stdin.isTTY) {
|
|
48683
49121
|
console.error(
|
|
48684
49122
|
"Error: setup requires an interactive terminal.\nFor non-interactive environments, set CONFLUENCE_URL, CONFLUENCE_EMAIL, and CONFLUENCE_API_TOKEN as environment variables."
|
|
48685
49123
|
);
|
|
48686
49124
|
process.exit(1);
|
|
48687
49125
|
}
|
|
49126
|
+
if (clientId !== void 0) {
|
|
49127
|
+
const validIds = knownClientIds();
|
|
49128
|
+
if (!validIds.includes(clientId)) {
|
|
49129
|
+
console.error(
|
|
49130
|
+
`Error: Unknown --client "${clientId}". Valid IDs are: ${validIds.join(", ")}`
|
|
49131
|
+
);
|
|
49132
|
+
process.exit(1);
|
|
49133
|
+
}
|
|
49134
|
+
}
|
|
48688
49135
|
if (profile !== void 0 && !PROFILE_NAME_RE.test(profile)) {
|
|
48689
49136
|
console.error(
|
|
48690
49137
|
`Error: Invalid profile name "${profile}". Use lowercase alphanumeric and hyphens only (1-63 chars).`
|
|
@@ -48843,19 +49290,37 @@ Your choice [default: 1]: `
|
|
|
48843
49290
|
console.log(
|
|
48844
49291
|
"\nSetup complete. Restart your MCP client to use the new credentials."
|
|
48845
49292
|
);
|
|
49293
|
+
const binPath = resolveBinPath();
|
|
49294
|
+
const effectiveProfile = profile ?? "default";
|
|
49295
|
+
const clientsToShow = clientId ? [clientId] : knownClientIds();
|
|
49296
|
+
for (const id of clientsToShow) {
|
|
49297
|
+
const entry = (await Promise.resolve().then(() => (init_client_configs(), client_configs_exports))).CLIENT_CONFIGS.find(
|
|
49298
|
+
(c) => c.id === id
|
|
49299
|
+
);
|
|
49300
|
+
const { snippet, warning } = renderConfigSnippet(id, effectiveProfile, binPath);
|
|
49301
|
+
console.log(`
|
|
49302
|
+
--- ${entry.displayName} config (${entry.configFileHint}) ---`);
|
|
49303
|
+
console.log(snippet);
|
|
49304
|
+
if (warning) {
|
|
49305
|
+
console.log(`
|
|
49306
|
+
Note: ${warning}`);
|
|
49307
|
+
}
|
|
49308
|
+
}
|
|
48846
49309
|
} finally {
|
|
48847
49310
|
rl.close();
|
|
48848
49311
|
}
|
|
48849
49312
|
}
|
|
48850
|
-
var readline, import_node_process2, TOOLS;
|
|
49313
|
+
var readline, import_node_process2, import_node_child_process3, TOOLS;
|
|
48851
49314
|
var init_setup = __esm({
|
|
48852
49315
|
"src/cli/setup.ts"() {
|
|
48853
49316
|
"use strict";
|
|
48854
49317
|
readline = __toESM(require("node:readline/promises"));
|
|
48855
49318
|
import_node_process2 = require("node:process");
|
|
49319
|
+
import_node_child_process3 = require("node:child_process");
|
|
48856
49320
|
init_test_connection();
|
|
48857
49321
|
init_keychain();
|
|
48858
49322
|
init_profiles();
|
|
49323
|
+
init_client_configs();
|
|
48859
49324
|
TOOLS = [
|
|
48860
49325
|
"create_page",
|
|
48861
49326
|
"get_page",
|
|
@@ -49447,7 +49912,7 @@ Informational:
|
|
|
49447
49912
|
var install_agent_default;
|
|
49448
49913
|
var init_install_agent = __esm({
|
|
49449
49914
|
"install-agent.md"() {
|
|
49450
|
-
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\nAdd the server to `.mcp.json` (or the equivalent config file for the user\'s MCP client):\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**IMPORTANT:** The only env var needed is `CONFLUENCE_PROFILE`. The URL, email, and API token are stored securely in the OS keychain \u2014 they should NOT 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## 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';
|
|
49915
|
+
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 elicitation)\n\nSome MCP clients (currently OpenCode, plus others) don\'t implement the in-protocol\nconfirmation prompt. Starting in v6.6.0, epimethian-mcp routes those confirmations\nthrough your agent\'s normal chat surface instead.\n\n### What you (the agent) see\n\nWhen a destructive write is requested against a client without elicitation, the\ntool returns an error with a confirmation token:\n\n```\nisError: true\nstructuredContent:\n {\n "confirm_token": "<opaque token>",\n "audit_id": "<UUID for correlation>",\n "expires_at": "<ISO timestamp>",\n "page_id": "<pageId>",\n ...\n }\ncontent[0].text:\n \u26A0\uFE0F Confirmation required (SOFT_CONFIRMATION_REQUIRED)\n\n {humanSummary}\n\n Please ask the user before retrying. If approved, re-call with:\n "confirm_token": from structuredContent.\n\n Expires at {timestamp}; invalidated by competing writes.\n```\n\n### What to do\n\n1. STOP. Don\'t retry blindly.\n2. Show the user, in their language, what\'s about to happen (use the\n `humanSummary` field from the result).\n3. Ask the user explicitly. Wait for their answer.\n4. If approved: re-call the tool with the SAME parameters plus\n `confirm_token` from the structuredContent.\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).\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### 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 |\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 \u2264 2.1.123** | Fakes it | Set `EPIMETHIAN_BYPASS_ELICITATION=true` (see below). |\n| **Claude Code VS Code extension \u2265 2.1.124** | Likely fixed (verify) | If write tools fail with `NO_USER_RESPONSE`, fall back to `EPIMETHIAN_BYPASS_ELICITATION=true`. |\n| **OpenCode** | No \u2014 capability not advertised | Set `EPIMETHIAN_ALLOW_UNGATED_WRITES=true` or use only read tools / additive writes that don\'t trigger the gate. No tracking issue at sst/opencode yet (as of v6.4.1); a feature request would be needed for real elicitation support. |\n| **Cursor / Windsurf / Zed / others** | Varies | If write tools fail with `ELICITATION_REQUIRED_BUT_UNAVAILABLE`, the client doesn\'t advertise the capability \u2014 use `EPIMETHIAN_ALLOW_UNGATED_WRITES=true`. If write tools fail with `NO_USER_RESPONSE` despite the client claiming support, the client fakes it \u2014 use `EPIMETHIAN_BYPASS_ELICITATION=true`. |\n\n### Difference between the two bypass env vars\n\nThese are **not** interchangeable. Pick the one that matches the failure mode:\n\n- **`EPIMETHIAN_ALLOW_UNGATED_WRITES=true`** \u2014 for clients that *don\'t\n advertise* elicitation during the MCP handshake. The server detects the\n absence and (with this flag) lets writes proceed. OpenCode falls in this\n category.\n- **`EPIMETHIAN_BYPASS_ELICITATION=true`** \u2014 for clients that *advertise*\n elicitation but never actually honour the request (the SDK transport\n silently returns `{action: "decline"}`). The Claude Code VS Code\n extension \u2264 2.1.123 falls in this category. This flag is unconditional \u2014\n it bypasses elicitation even when the client claims to support it.\n\n### Trade-off: what you give up by setting either flag\n\nBoth flags **disable the in-protocol confirmation gate**. Writes still go\nthrough the harness\'s tool allow-list (so users can still block the tool\nin their permission settings) and through every server-side guard\n(provenance, source-policy, write-budget, byte-equivalence) \u2014 but the user\nno longer gets a UI prompt before each destructive operation. Recommend\nthis 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## 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';
|
|
49451
49916
|
}
|
|
49452
49917
|
});
|
|
49453
49918
|
|
|
@@ -49472,7 +49937,7 @@ __export(upgrade_exports, {
|
|
|
49472
49937
|
runUpgrade: () => runUpgrade
|
|
49473
49938
|
});
|
|
49474
49939
|
async function runUpgrade() {
|
|
49475
|
-
const currentVersion = "6.
|
|
49940
|
+
const currentVersion = "6.6.1";
|
|
49476
49941
|
console.log(`epimethian-mcp upgrade: current version v${currentVersion}`);
|
|
49477
49942
|
let pending = await getPendingUpdate();
|
|
49478
49943
|
if (!pending) {
|
|
@@ -60255,6 +60720,7 @@ function computeUnifiedDiff(textA, textB, maxLength) {
|
|
|
60255
60720
|
}
|
|
60256
60721
|
|
|
60257
60722
|
// src/server/index.ts
|
|
60723
|
+
init_types2();
|
|
60258
60724
|
init_untrusted_fence();
|
|
60259
60725
|
|
|
60260
60726
|
// src/server/converter/storage-to-md.ts
|
|
@@ -60407,10 +60873,12 @@ function listDestructiveFlagsSet(flags) {
|
|
|
60407
60873
|
init_write_budget();
|
|
60408
60874
|
|
|
60409
60875
|
// src/server/elicitation.ts
|
|
60876
|
+
init_confirmation_tokens();
|
|
60410
60877
|
var USER_DECLINED = "USER_DECLINED";
|
|
60411
60878
|
var USER_CANCELLED = "USER_CANCELLED";
|
|
60412
60879
|
var NO_USER_RESPONSE = "NO_USER_RESPONSE";
|
|
60413
60880
|
var ELICITATION_REQUIRED_BUT_UNAVAILABLE = "ELICITATION_REQUIRED_BUT_UNAVAILABLE";
|
|
60881
|
+
var SOFT_CONFIRMATION_REQUIRED = "SOFT_CONFIRMATION_REQUIRED";
|
|
60414
60882
|
var GatedOperationError = class extends Error {
|
|
60415
60883
|
code;
|
|
60416
60884
|
constructor(code2, message) {
|
|
@@ -60419,42 +60887,152 @@ var GatedOperationError = class extends Error {
|
|
|
60419
60887
|
this.code = code2;
|
|
60420
60888
|
}
|
|
60421
60889
|
};
|
|
60422
|
-
|
|
60890
|
+
var SoftConfirmationRequiredError = class extends GatedOperationError {
|
|
60891
|
+
token;
|
|
60892
|
+
auditId;
|
|
60893
|
+
expiresAt;
|
|
60894
|
+
humanSummary;
|
|
60895
|
+
retryHint;
|
|
60896
|
+
pageId;
|
|
60897
|
+
constructor(args) {
|
|
60898
|
+
super(SOFT_CONFIRMATION_REQUIRED, args.message);
|
|
60899
|
+
this.name = "SoftConfirmationRequiredError";
|
|
60900
|
+
this.token = args.token;
|
|
60901
|
+
this.auditId = args.auditId;
|
|
60902
|
+
this.expiresAt = args.expiresAt;
|
|
60903
|
+
this.humanSummary = args.humanSummary;
|
|
60904
|
+
this.retryHint = args.retryHint;
|
|
60905
|
+
this.pageId = args.pageId;
|
|
60906
|
+
}
|
|
60907
|
+
};
|
|
60908
|
+
function renderDeletionSummary(s) {
|
|
60909
|
+
const parts = [];
|
|
60910
|
+
if (s.tocs > 0) parts.push(`${s.tocs} TOC macro${s.tocs === 1 ? "" : "s"}`);
|
|
60911
|
+
if (s.links > 0) parts.push(`${s.links} link macro${s.links === 1 ? "" : "s"}`);
|
|
60912
|
+
if (s.codeMacros > 0)
|
|
60913
|
+
parts.push(`${s.codeMacros} code macro${s.codeMacros === 1 ? "" : "s"}`);
|
|
60914
|
+
if (s.structuredMacros > 0)
|
|
60915
|
+
parts.push(
|
|
60916
|
+
`${s.structuredMacros} structured macro${s.structuredMacros === 1 ? "" : "s"}`
|
|
60917
|
+
);
|
|
60918
|
+
if (s.plainElements > 0)
|
|
60919
|
+
parts.push(
|
|
60920
|
+
`${s.plainElements} plain element${s.plainElements === 1 ? "" : "s"}`
|
|
60921
|
+
);
|
|
60922
|
+
if (s.other > 0)
|
|
60923
|
+
parts.push(`${s.other} other element${s.other === 1 ? "" : "s"}`);
|
|
60924
|
+
if (parts.length === 0) {
|
|
60925
|
+
return "This update has no destructive changes.";
|
|
60926
|
+
}
|
|
60927
|
+
const list2 = parts.length === 1 ? parts[0] : parts.slice(0, -1).join(", ") + " and " + parts[parts.length - 1];
|
|
60928
|
+
return `This update will remove ${list2}.`;
|
|
60929
|
+
}
|
|
60930
|
+
var bypassMisconfigWarningFired = false;
|
|
60931
|
+
var FAST_DECLINE_THRESHOLD_MS = 50;
|
|
60932
|
+
var FAST_DECLINE_THRESHOLD_OVERRIDE_ENV = "EPIMETHIAN_FAST_DECLINE_THRESHOLD_MS";
|
|
60933
|
+
var DISABLE_FAST_DECLINE_DETECTION_ENV = "EPIMETHIAN_DISABLE_FAST_DECLINE_DETECTION";
|
|
60934
|
+
var TREAT_ELICITATION_AS_UNSUPPORTED_ENV = "EPIMETHIAN_TREAT_ELICITATION_AS_UNSUPPORTED";
|
|
60935
|
+
var fakingElicitationFlags = /* @__PURE__ */ new WeakMap();
|
|
60936
|
+
function isClientFakingElicitation(server) {
|
|
60937
|
+
return fakingElicitationFlags.get(server) === true;
|
|
60938
|
+
}
|
|
60939
|
+
function _markClientAsFakingElicitation(server) {
|
|
60940
|
+
fakingElicitationFlags.set(server, true);
|
|
60941
|
+
}
|
|
60942
|
+
function readFastDeclineThresholdMs() {
|
|
60943
|
+
const raw = process.env[FAST_DECLINE_THRESHOLD_OVERRIDE_ENV];
|
|
60944
|
+
if (raw === void 0 || raw === "") return FAST_DECLINE_THRESHOLD_MS;
|
|
60945
|
+
const parsed = Number.parseInt(raw, 10);
|
|
60946
|
+
if (!Number.isFinite(parsed)) return FAST_DECLINE_THRESHOLD_MS;
|
|
60947
|
+
if (parsed < 10) return 10;
|
|
60948
|
+
if (parsed > 5e3) return 5e3;
|
|
60949
|
+
return parsed;
|
|
60950
|
+
}
|
|
60951
|
+
function effectiveSupportsElicitation(server) {
|
|
60952
|
+
if (process.env[TREAT_ELICITATION_AS_UNSUPPORTED_ENV] === "true") {
|
|
60953
|
+
return false;
|
|
60954
|
+
}
|
|
60955
|
+
if (isClientFakingElicitation(server)) {
|
|
60956
|
+
return false;
|
|
60957
|
+
}
|
|
60958
|
+
return clientSupportsElicitation(server);
|
|
60959
|
+
}
|
|
60960
|
+
async function evaluateUnsupportedBranch(server, context) {
|
|
60961
|
+
const supported = effectiveSupportsElicitation(server);
|
|
60423
60962
|
if (process.env.EPIMETHIAN_BYPASS_ELICITATION === "true") {
|
|
60963
|
+
if (!supported && !bypassMisconfigWarningFired) {
|
|
60964
|
+
bypassMisconfigWarningFired = true;
|
|
60965
|
+
console.error(
|
|
60966
|
+
`epimethian-mcp: BYPASS_ELICITATION is set, but the connected client does not advertise elicitation support. The intended use of BYPASS_ELICITATION is for clients that falsely advertise the capability and never honour requests. For clients that don't advertise it (e.g. OpenCode), set EPIMETHIAN_ALLOW_UNGATED_WRITES instead, or upgrade to v6.6.0 to benefit from soft elicitation.`
|
|
60967
|
+
);
|
|
60968
|
+
}
|
|
60424
60969
|
console.error(
|
|
60425
60970
|
`epimethian-mcp: [UNGATED] tool=${context.tool} \u2014 bypassing elicitation gate; proceeding because EPIMETHIAN_BYPASS_ELICITATION=true.`
|
|
60426
60971
|
);
|
|
60427
|
-
return;
|
|
60972
|
+
return "handled";
|
|
60973
|
+
}
|
|
60974
|
+
if (!supported && process.env.EPIMETHIAN_ALLOW_UNGATED_WRITES === "true") {
|
|
60975
|
+
console.error(
|
|
60976
|
+
`epimethian-mcp: [UNGATED] tool=${context.tool} \u2014 client does not support elicitation; proceeding because EPIMETHIAN_ALLOW_UNGATED_WRITES=true.`
|
|
60977
|
+
);
|
|
60978
|
+
return "handled";
|
|
60979
|
+
}
|
|
60980
|
+
if (!supported && process.env.EPIMETHIAN_DISABLE_SOFT_CONFIRM === "true") {
|
|
60981
|
+
throw new GatedOperationError(
|
|
60982
|
+
ELICITATION_REQUIRED_BUT_UNAVAILABLE,
|
|
60983
|
+
`This tool requires interactive confirmation but your MCP client does not expose elicitation, and EPIMETHIAN_DISABLE_SOFT_CONFIRM is set. Use \`update_page_section\` instead, or switch to a client that supports MCP elicitation (Claude Code \u2265 2.x, Claude Desktop \u2265 0.10).`
|
|
60984
|
+
);
|
|
60985
|
+
}
|
|
60986
|
+
if (!supported && context.cloudId !== void 0 && context.pageId !== void 0 && context.pageVersion !== void 0 && context.diffHash !== void 0) {
|
|
60987
|
+
const deletionSummary = context.details?.deletionSummary;
|
|
60988
|
+
let humanSummary;
|
|
60989
|
+
if (deletionSummary !== void 0 && typeof deletionSummary === "object" && deletionSummary !== null && !Array.isArray(deletionSummary)) {
|
|
60990
|
+
humanSummary = renderDeletionSummary(deletionSummary);
|
|
60991
|
+
} else {
|
|
60992
|
+
humanSummary = context.summary;
|
|
60993
|
+
}
|
|
60994
|
+
const minted = mintToken({
|
|
60995
|
+
tool: context.tool,
|
|
60996
|
+
cloudId: context.cloudId,
|
|
60997
|
+
pageId: context.pageId,
|
|
60998
|
+
pageVersion: context.pageVersion,
|
|
60999
|
+
diffHash: context.diffHash
|
|
61000
|
+
});
|
|
61001
|
+
const retryHint = `Re-call \`${context.tool}\` with the same parameters plus \`confirm_token\` set to the value in structuredContent.confirm_token.`;
|
|
61002
|
+
throw new SoftConfirmationRequiredError({
|
|
61003
|
+
token: minted.token,
|
|
61004
|
+
auditId: minted.auditId,
|
|
61005
|
+
expiresAt: minted.expiresAt,
|
|
61006
|
+
humanSummary,
|
|
61007
|
+
retryHint,
|
|
61008
|
+
pageId: context.pageId,
|
|
61009
|
+
// The message is consumed by the index.ts catch in §5.5; the
|
|
61010
|
+
// structured fields above are the load-bearing payload. Keep the
|
|
61011
|
+
// text agent-directed (not user-facing) and free of tenant content.
|
|
61012
|
+
message: `Soft confirmation required for ${context.tool}: surface the prompt to the user and retry with the confirm_token.`
|
|
61013
|
+
});
|
|
60428
61014
|
}
|
|
60429
|
-
const supported = clientSupportsElicitation(server);
|
|
60430
61015
|
if (!supported) {
|
|
60431
|
-
if (process.env.EPIMETHIAN_ALLOW_UNGATED_WRITES === "true") {
|
|
60432
|
-
console.error(
|
|
60433
|
-
`epimethian-mcp: [UNGATED] tool=${context.tool} \u2014 client does not support elicitation; proceeding because EPIMETHIAN_ALLOW_UNGATED_WRITES=true.`
|
|
60434
|
-
);
|
|
60435
|
-
return;
|
|
60436
|
-
}
|
|
60437
61016
|
throw new GatedOperationError(
|
|
60438
61017
|
ELICITATION_REQUIRED_BUT_UNAVAILABLE,
|
|
60439
61018
|
`This tool requires interactive confirmation but your MCP client does not expose elicitation. Use \`update_page_section\` instead, or switch to a client that supports MCP elicitation (Claude Code \u2265 2.x, Claude Desktop \u2265 0.10).`
|
|
60440
61019
|
);
|
|
60441
61020
|
}
|
|
61021
|
+
return "fall_through";
|
|
61022
|
+
}
|
|
61023
|
+
async function gateOperation(server, context) {
|
|
61024
|
+
const initial = await evaluateUnsupportedBranch(server, context);
|
|
61025
|
+
if (initial === "handled") return;
|
|
60442
61026
|
const lines = [context.summary];
|
|
60443
61027
|
if (context.details) {
|
|
60444
61028
|
for (const [k, v] of Object.entries(context.details)) {
|
|
60445
61029
|
if (v === void 0) continue;
|
|
60446
61030
|
if (k === "deletionSummary" && typeof v === "object" && v !== null) {
|
|
60447
|
-
const
|
|
60448
|
-
|
|
60449
|
-
|
|
60450
|
-
|
|
60451
|
-
|
|
60452
|
-
if (s.structuredMacros > 0) parts.push(`${s.structuredMacros} structured macro${s.structuredMacros === 1 ? "" : "s"}`);
|
|
60453
|
-
if (s.plainElements > 0) parts.push(`${s.plainElements} plain element${s.plainElements === 1 ? "" : "s"}`);
|
|
60454
|
-
if (s.other > 0) parts.push(`${s.other} other element${s.other === 1 ? "" : "s"}`);
|
|
60455
|
-
if (parts.length > 0) {
|
|
60456
|
-
const list2 = parts.length === 1 ? parts[0] : parts.slice(0, -1).join(", ") + " and " + parts[parts.length - 1];
|
|
60457
|
-
lines.push(` This update will remove ${list2} that the new markdown does not regenerate. Proceed?`);
|
|
61031
|
+
const rendered = renderDeletionSummary(v);
|
|
61032
|
+
if (rendered !== "This update has no destructive changes.") {
|
|
61033
|
+
lines.push(
|
|
61034
|
+
` ${rendered.replace(/\.$/, "")} that the new markdown does not regenerate. Proceed?`
|
|
61035
|
+
);
|
|
60458
61036
|
}
|
|
60459
61037
|
continue;
|
|
60460
61038
|
}
|
|
@@ -60463,6 +61041,7 @@ async function gateOperation(server, context) {
|
|
|
60463
61041
|
}
|
|
60464
61042
|
const message = lines.join("\n");
|
|
60465
61043
|
let result;
|
|
61044
|
+
const startedAt = performance.now();
|
|
60466
61045
|
try {
|
|
60467
61046
|
result = await server.server.elicitInput({
|
|
60468
61047
|
message,
|
|
@@ -60484,6 +61063,19 @@ async function gateOperation(server, context) {
|
|
|
60484
61063
|
`Elicitation for ${context.tool} failed (${err instanceof Error ? err.message : String(err)}) \u2014 refusing the operation.`
|
|
60485
61064
|
);
|
|
60486
61065
|
}
|
|
61066
|
+
const elapsedMs = performance.now() - startedAt;
|
|
61067
|
+
const fastDeclineDisabled = process.env[DISABLE_FAST_DECLINE_DETECTION_ENV] === "true";
|
|
61068
|
+
if (!fastDeclineDisabled && result.action === "decline" && elapsedMs < readFastDeclineThresholdMs()) {
|
|
61069
|
+
_markClientAsFakingElicitation(server);
|
|
61070
|
+
const retry = await evaluateUnsupportedBranch(server, context);
|
|
61071
|
+
if (retry === "fall_through") {
|
|
61072
|
+
throw new GatedOperationError(
|
|
61073
|
+
NO_USER_RESPONSE,
|
|
61074
|
+
`${context.tool} could not be confirmed: fast-decline retry unexpectedly fell through to row 6.`
|
|
61075
|
+
);
|
|
61076
|
+
}
|
|
61077
|
+
return;
|
|
61078
|
+
}
|
|
60487
61079
|
if (result.action === "accept" && result.content?.confirm === true) {
|
|
60488
61080
|
return;
|
|
60489
61081
|
}
|
|
@@ -60505,6 +61097,19 @@ async function gateOperation(server, context) {
|
|
|
60505
61097
|
);
|
|
60506
61098
|
}
|
|
60507
61099
|
|
|
61100
|
+
// src/server/index.ts
|
|
61101
|
+
init_confirmation_tokens();
|
|
61102
|
+
|
|
61103
|
+
// src/server/version-schema.ts
|
|
61104
|
+
init_zod();
|
|
61105
|
+
var versionField = external_exports.union([
|
|
61106
|
+
external_exports.preprocess(
|
|
61107
|
+
(v) => typeof v === "string" && /^\d+$/.test(v) ? Number(v) : v,
|
|
61108
|
+
external_exports.number().int().positive()
|
|
61109
|
+
),
|
|
61110
|
+
external_exports.literal("current")
|
|
61111
|
+
]);
|
|
61112
|
+
|
|
60508
61113
|
// src/server/index.ts
|
|
60509
61114
|
init_update_orchestrator();
|
|
60510
61115
|
init_tokeniser();
|
|
@@ -61059,7 +61664,9 @@ async function registerTools(server, config3) {
|
|
|
61059
61664
|
deletedTokens: prepared.deletedTokens,
|
|
61060
61665
|
clientLabel: getClientLabel(server),
|
|
61061
61666
|
operation: position === "prepend" ? "prepend_to_page" : "append_to_page",
|
|
61062
|
-
assertGrowth: true
|
|
61667
|
+
assertGrowth: true,
|
|
61668
|
+
// 2.E: defense-in-depth invalidation.
|
|
61669
|
+
cloudId: opts.cloudId
|
|
61063
61670
|
});
|
|
61064
61671
|
return { page: submitted.page, newVersion: submitted.newVersion, oldLen: currentStorage.length, newLen: newBody.length };
|
|
61065
61672
|
}
|
|
@@ -61244,14 +61851,14 @@ ${truncated}${truncationNote(origLen)}`
|
|
|
61244
61851
|
{
|
|
61245
61852
|
description: describeWithLock(
|
|
61246
61853
|
withDestructiveWarning(
|
|
61247
|
-
"Update an existing Confluence page. Accepts GFM markdown or Confluence storage format \u2014 markdown is automatically converted via the token-aware write path, which preserves all existing macros and rich elements. Do NOT mix the two: a body that contains both <ac:.../> storage tags AND markdown structural patterns (## headings, lists, fenced code blocks) is rejected with MIXED_INPUT_DETECTED. To inject a TOC macro from markdown, use YAML frontmatter at the top of the body: `---\\ntoc:\\n maxLevel: 3\\n minLevel: 1\\n---`. For other macros from markdown, use directive syntax: `:info[content]`, `:mention[Name]{accountId=...}`, `:date[2026-04-23]`. You must provide the version number from your most recent get_page call. If the page was modified by someone else since then, this will return a conflict error \u2014 re-read the page and retry.\n\nFor narrow changes to a single section, prefer update_page_section \u2014 it leaves the rest of the page untouched and is safer for targeted edits.\n\nMarkdown update flags:\n- confirm_deletions: set to true to acknowledge removing preserved macros/elements (default false \u2014 any deletion errors until confirmed).\n- replace_body: set to true for a wholesale rewrite that skips preservation (default false).\n- confirm_shrinkage: set to true to acknowledge a >50% body size reduction (default false).\n- confirm_structure_loss: set to true to acknowledge a >50% heading count drop (default false).\n- allow_raw_html: allow raw HTML inside markdown bodies (default false).\n- confluence_base_url: override the URL used by the link rewriter.\n\nreplace_body skips all safety nets (token preservation, deletion confirmation). When delegating update_page to a subagent, ensure the agent includes the full existing body \u2014 replace_body replaces ALL content with only what you provide."
|
|
61854
|
+
"Update an existing Confluence page. Accepts GFM markdown or Confluence storage format \u2014 markdown is automatically converted via the token-aware write path, which preserves all existing macros and rich elements. Do NOT mix the two: a body that contains both <ac:.../> storage tags AND markdown structural patterns (## headings, lists, fenced code blocks) is rejected with MIXED_INPUT_DETECTED. To inject a TOC macro from markdown, use YAML frontmatter at the top of the body: `---\\ntoc:\\n maxLevel: 3\\n minLevel: 1\\n---`. For other macros from markdown, use directive syntax: `:info[content]`, `:mention[Name]{accountId=...}`, `:date[2026-04-23]`. You must provide the version number from your most recent get_page call. If the page was modified by someone else since then, this will return a conflict error \u2014 re-read the page and retry.\n\nFor narrow changes to a single section, prefer update_page_section \u2014 it leaves the rest of the page untouched and is safer for targeted edits.\n\nMarkdown update flags:\n- confirm_deletions: set to true to acknowledge removing preserved macros/elements (default false \u2014 any deletion errors until confirmed).\n- replace_body: set to true for a wholesale rewrite that skips preservation (default false).\n- confirm_shrinkage: set to true to acknowledge a >50% body size reduction (default false).\n- confirm_structure_loss: set to true to acknowledge a >50% heading count drop (default false).\n- allow_raw_html: allow raw HTML inside markdown bodies (default false).\n- confluence_base_url: override the URL used by the link rewriter.\n\nreplace_body skips all safety nets (token preservation, deletion confirmation). When delegating update_page to a subagent, ensure the agent includes the full existing body \u2014 replace_body replaces ALL content with only what you provide.\n\nIf your MCP client does not support in-protocol confirmation, this tool returns `SOFT_CONFIRMATION_REQUIRED` on the first call when destructive flags are set. STOP and ask the user before retrying. If the user approves, re-call this tool with the same parameters plus `confirm_token` from the first response. The token expires after 5 minutes and is invalidated by competing writes. See the 'Soft confirmation' section of `install-agent.md` for the full protocol."
|
|
61248
61855
|
),
|
|
61249
61856
|
config3
|
|
61250
61857
|
),
|
|
61251
61858
|
inputSchema: {
|
|
61252
61859
|
page_id: external_exports.string().describe("The Confluence page ID"),
|
|
61253
61860
|
title: external_exports.string().describe("Page title (use the title from get_page if unchanged)"),
|
|
61254
|
-
version:
|
|
61861
|
+
version: versionField.describe(
|
|
61255
61862
|
`The page version number from your most recent get_page call. Pass the literal string "current" to skip the read and apply this update on top of whatever the latest version is right now. WARNING: "current" deliberately bypasses optimistic concurrency \u2014 it is NOT a conflict-resolution strategy. If a coworker (or another agent) writes between our read and submit, the API will still 409 and we propagate the conflict. Use a numeric version when you want the "don't overwrite my coworker's changes" guard. Use "current" only as a shortcut to skip the get_page round-trip when concurrent writes are not a concern (e.g. immediately after create_page).`
|
|
61256
61863
|
),
|
|
61257
61864
|
body: external_exports.string().optional().describe("New body content \u2014 GFM markdown or Confluence storage format (XHTML). Markdown is auto-detected and converted via the token-aware write path. Do not mix the two: inlining <ac:.../> macros inside a markdown body is rejected. For a TOC use YAML frontmatter (toc: { maxLevel, minLevel }); for other macros use directive syntax (:info[...], :mention[...]{...})."),
|
|
@@ -61266,11 +61873,12 @@ ${truncated}${truncationNote(origLen)}`
|
|
|
61266
61873
|
),
|
|
61267
61874
|
allow_raw_html: external_exports.boolean().default(false).describe("Allow raw HTML passthrough inside markdown bodies (disabled by default)."),
|
|
61268
61875
|
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."),
|
|
61269
|
-
source: sourceSchema
|
|
61876
|
+
source: sourceSchema,
|
|
61877
|
+
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.")
|
|
61270
61878
|
},
|
|
61271
61879
|
annotations: { destructiveHint: false, idempotentHint: false }
|
|
61272
61880
|
},
|
|
61273
|
-
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 }) => {
|
|
61881
|
+
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 }) => {
|
|
61274
61882
|
const blocked = writeGuard("update_page", config3);
|
|
61275
61883
|
if (blocked) return blocked;
|
|
61276
61884
|
try {
|
|
@@ -61283,21 +61891,43 @@ ${truncated}${truncationNote(origLen)}`
|
|
|
61283
61891
|
});
|
|
61284
61892
|
const effectiveSource = validateSource(source, flagsSet);
|
|
61285
61893
|
const cfg = await getConfig();
|
|
61894
|
+
const cloudId = cfg.sealedCloudId;
|
|
61286
61895
|
const currentPage = await getPage(page_id, true);
|
|
61287
61896
|
const currentStorage = currentPage.body?.storage?.value ?? currentPage.body?.value ?? "";
|
|
61897
|
+
const pageVersion = currentPage.version?.number ?? 0;
|
|
61288
61898
|
if (flagsSet.length > 0) {
|
|
61289
61899
|
const deletionSummary = confirm_deletions && body ? tryForecastDeletions(currentStorage, body, confluence_base_url ?? cfg.url) : null;
|
|
61290
|
-
|
|
61900
|
+
const diffHash = cloudId && pageVersion > 0 ? computeDiffHash(body ?? currentStorage, pageVersion) : void 0;
|
|
61901
|
+
const tokenResult = await maybeConsumeConfirmToken({
|
|
61902
|
+
confirm_token,
|
|
61291
61903
|
tool: "update_page",
|
|
61292
|
-
|
|
61293
|
-
|
|
61294
|
-
|
|
61295
|
-
|
|
61296
|
-
source: effectiveSource,
|
|
61297
|
-
version: version2,
|
|
61298
|
-
...deletionSummary ? { deletionSummary } : {}
|
|
61299
|
-
}
|
|
61904
|
+
cloudId,
|
|
61905
|
+
pageId: page_id,
|
|
61906
|
+
pageVersion,
|
|
61907
|
+
diffHash
|
|
61300
61908
|
});
|
|
61909
|
+
if (tokenResult === "invalid") {
|
|
61910
|
+
throw new ConverterError(
|
|
61911
|
+
"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.",
|
|
61912
|
+
"CONFIRMATION_TOKEN_INVALID"
|
|
61913
|
+
);
|
|
61914
|
+
} else if (tokenResult === "no_token") {
|
|
61915
|
+
await gateOperation(server, {
|
|
61916
|
+
tool: "update_page",
|
|
61917
|
+
summary: `Update page ${page_id} with destructive flags?`,
|
|
61918
|
+
details: {
|
|
61919
|
+
page_id,
|
|
61920
|
+
flags: flagsSet.join(","),
|
|
61921
|
+
source: effectiveSource,
|
|
61922
|
+
version: version2,
|
|
61923
|
+
...deletionSummary ? { deletionSummary } : {}
|
|
61924
|
+
},
|
|
61925
|
+
cloudId,
|
|
61926
|
+
pageId: page_id,
|
|
61927
|
+
pageVersion,
|
|
61928
|
+
diffHash
|
|
61929
|
+
});
|
|
61930
|
+
}
|
|
61301
61931
|
}
|
|
61302
61932
|
const resolvedVersion = version2 === "current" ? currentPage.version?.number ?? 0 : version2;
|
|
61303
61933
|
if (resolvedVersion <= 0) {
|
|
@@ -61331,7 +61961,9 @@ ${truncated}${truncationNote(origLen)}`
|
|
|
61331
61961
|
confirmStructureLoss: confirm_structure_loss,
|
|
61332
61962
|
confirmDeletions: confirm_deletions,
|
|
61333
61963
|
// E2: thread the validated source into the mutation log.
|
|
61334
|
-
source: effectiveSource
|
|
61964
|
+
source: effectiveSource,
|
|
61965
|
+
// 2.E: defense-in-depth invalidation.
|
|
61966
|
+
cloudId
|
|
61335
61967
|
});
|
|
61336
61968
|
const isTitleOnly = prepared.finalStorage === void 0;
|
|
61337
61969
|
const warnings = [];
|
|
@@ -61349,6 +61981,9 @@ ${truncated}${truncationNote(origLen)}`
|
|
|
61349
61981
|
appendWarnings(`Updated: ${submitted.page.title} (ID: ${submitted.page.id}, version: ${submitted.newVersion}, body: ${submitted.oldLen}\u2192${submitted.newLen} chars${removalNote})`, warnings) + echo
|
|
61350
61982
|
);
|
|
61351
61983
|
} catch (err) {
|
|
61984
|
+
if (err instanceof SoftConfirmationRequiredError) {
|
|
61985
|
+
return formatSoftConfirmationResult(err, { pageId: page_id });
|
|
61986
|
+
}
|
|
61352
61987
|
return toolErrorWithContext(err, { operation: "update_page", resource: `page ${page_id}`, profile: config3.profile });
|
|
61353
61988
|
}
|
|
61354
61989
|
}
|
|
@@ -61358,7 +61993,7 @@ ${truncated}${truncationNote(origLen)}`
|
|
|
61358
61993
|
{
|
|
61359
61994
|
description: describeWithLock(
|
|
61360
61995
|
withDestructiveWarning(
|
|
61361
|
-
"Delete a Confluence page by ID. Requires the current `version` from your most recent get_page call \u2014 delete is refused if the page has been modified since. Set EPIMETHIAN_LEGACY_DELETE_WITHOUT_VERSION=true to restore the previous version-less behaviour for one release while scripts are migrated."
|
|
61996
|
+
"Delete a Confluence page by ID. Requires the current `version` from your most recent get_page call \u2014 delete is refused if the page has been modified since. Set EPIMETHIAN_LEGACY_DELETE_WITHOUT_VERSION=true to restore the previous version-less behaviour for one release while scripts are migrated.\n\nIf your MCP client does not support in-protocol confirmation, this tool returns `SOFT_CONFIRMATION_REQUIRED` on the first call when destructive flags are set. STOP and ask the user before retrying. If the user approves, re-call this tool with the same parameters plus `confirm_token` from the first response. The token expires after 5 minutes and is invalidated by competing writes. See the 'Soft confirmation' section of `install-agent.md` for the full protocol."
|
|
61362
61997
|
),
|
|
61363
61998
|
config3
|
|
61364
61999
|
),
|
|
@@ -61367,11 +62002,12 @@ ${truncated}${truncationNote(origLen)}`
|
|
|
61367
62002
|
version: external_exports.number().int().positive().optional().describe(
|
|
61368
62003
|
"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."
|
|
61369
62004
|
),
|
|
61370
|
-
source: sourceSchema
|
|
62005
|
+
source: sourceSchema,
|
|
62006
|
+
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.")
|
|
61371
62007
|
},
|
|
61372
62008
|
annotations: { destructiveHint: true, idempotentHint: true }
|
|
61373
62009
|
},
|
|
61374
|
-
async ({ page_id, version: version2, source }) => {
|
|
62010
|
+
async ({ page_id, version: version2, source, confirm_token }) => {
|
|
61375
62011
|
const blocked = writeGuard("delete_page", config3);
|
|
61376
62012
|
if (blocked) return blocked;
|
|
61377
62013
|
try {
|
|
@@ -61390,17 +62026,43 @@ ${truncated}${truncationNote(origLen)}`
|
|
|
61390
62026
|
`epimethian-mcp: WARNING: delete_page on page ${page_id} without a version (legacy opt-out active). This opt-out will be removed in a future release.`
|
|
61391
62027
|
);
|
|
61392
62028
|
}
|
|
61393
|
-
await
|
|
62029
|
+
const cfg = await getConfig();
|
|
62030
|
+
const cloudId = cfg.sealedCloudId;
|
|
62031
|
+
const pageVersion = version2 ?? 0;
|
|
62032
|
+
const diffHash = cloudId && pageVersion > 0 ? computeDiffHash("", pageVersion) : void 0;
|
|
62033
|
+
const tokenResult = await maybeConsumeConfirmToken({
|
|
62034
|
+
confirm_token,
|
|
61394
62035
|
tool: "delete_page",
|
|
61395
|
-
|
|
61396
|
-
|
|
61397
|
-
|
|
61398
|
-
|
|
61399
|
-
source: effectiveSource
|
|
61400
|
-
}
|
|
62036
|
+
cloudId,
|
|
62037
|
+
pageId: page_id,
|
|
62038
|
+
pageVersion,
|
|
62039
|
+
diffHash
|
|
61401
62040
|
});
|
|
62041
|
+
if (tokenResult === "invalid") {
|
|
62042
|
+
throw new ConverterError(
|
|
62043
|
+
"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.",
|
|
62044
|
+
"CONFIRMATION_TOKEN_INVALID"
|
|
62045
|
+
);
|
|
62046
|
+
} else if (tokenResult === "no_token") {
|
|
62047
|
+
await gateOperation(server, {
|
|
62048
|
+
tool: "delete_page",
|
|
62049
|
+
summary: `Delete page ${page_id}?`,
|
|
62050
|
+
details: {
|
|
62051
|
+
page_id,
|
|
62052
|
+
version: version2 ?? "(legacy: unversioned)",
|
|
62053
|
+
source: effectiveSource
|
|
62054
|
+
},
|
|
62055
|
+
cloudId,
|
|
62056
|
+
pageId: page_id,
|
|
62057
|
+
pageVersion,
|
|
62058
|
+
diffHash
|
|
62059
|
+
});
|
|
62060
|
+
}
|
|
61402
62061
|
writeBudget.consume();
|
|
61403
62062
|
await deletePage(page_id, version2);
|
|
62063
|
+
if (cloudId !== void 0) {
|
|
62064
|
+
invalidateForPage(cloudId, page_id);
|
|
62065
|
+
}
|
|
61404
62066
|
logMutation({
|
|
61405
62067
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
61406
62068
|
operation: "delete_page",
|
|
@@ -61410,6 +62072,9 @@ ${truncated}${truncationNote(origLen)}`
|
|
|
61410
62072
|
});
|
|
61411
62073
|
return toolResult(`Deleted page ${page_id}` + echo);
|
|
61412
62074
|
} catch (err) {
|
|
62075
|
+
if (err instanceof SoftConfirmationRequiredError) {
|
|
62076
|
+
return formatSoftConfirmationResult(err, { pageId: page_id });
|
|
62077
|
+
}
|
|
61413
62078
|
logMutation(errorRecord("delete_page", page_id, err));
|
|
61414
62079
|
return toolErrorWithContext(err, { operation: "delete_page", resource: `page ${page_id}`, profile: config3.profile });
|
|
61415
62080
|
}
|
|
@@ -61420,7 +62085,7 @@ ${truncated}${truncationNote(origLen)}`
|
|
|
61420
62085
|
{
|
|
61421
62086
|
description: describeWithLock(
|
|
61422
62087
|
withDestructiveWarning(
|
|
61423
|
-
"Update a single section of a Confluence page by heading name. Only the content under the specified heading is replaced; the rest of the page is untouched. Use headings_only to find section names first. Note: in Confluence spaces with heading auto-numbering enabled, stored heading text contains the prefix (e.g. `1.2. Section`); the matcher accepts either the prefixed or plain form."
|
|
62088
|
+
"Update a single section of a Confluence page by heading name. Only the content under the specified heading is replaced; the rest of the page is untouched. Use headings_only to find section names first. Note: in Confluence spaces with heading auto-numbering enabled, stored heading text contains the prefix (e.g. `1.2. Section`); the matcher accepts either the prefixed or plain form.\n\nIf your MCP client does not support in-protocol confirmation, this tool returns `SOFT_CONFIRMATION_REQUIRED` on the first call when destructive flags are set. STOP and ask the user before retrying. If the user approves, re-call this tool with the same parameters plus `confirm_token` from the first response. The token expires after 5 minutes and is invalidated by competing writes. See the 'Soft confirmation' section of `install-agent.md` for the full protocol."
|
|
61424
62089
|
),
|
|
61425
62090
|
config3
|
|
61426
62091
|
),
|
|
@@ -61442,15 +62107,16 @@ ${truncated}${truncationNote(origLen)}`
|
|
|
61442
62107
|
).min(1).optional().describe(
|
|
61443
62108
|
"Alternative to `body`: apply literal string substitutions inside the section's storage XML instead of replacing the whole section. Each entry's `find` is searched for and replaced with `replace`. Pairs are applied in input order; each subsequent `find` searches the partially-substituted body, so chained substitutions work as expected. If a `find` string is not found, the call fails with FIND_REPLACE_MATCH_FAILED \u2014 no silent no-op. Substitutions are ONLY applied to text outside macro boundaries (attribute values and CDATA bodies are protected). Exactly one of `body` or `find_replace` must be provided."
|
|
61444
62109
|
),
|
|
61445
|
-
version:
|
|
62110
|
+
version: versionField.describe(
|
|
61446
62111
|
`The page version number from your most recent get_page call. Pass the literal string "current" to skip the read and apply this update on top of whatever the latest version is right now. WARNING: "current" deliberately bypasses optimistic concurrency \u2014 it is NOT a conflict-resolution strategy. If a coworker (or another agent) writes between our read and submit, the API will still 409. Use a numeric version when you want the "don't overwrite my coworker's changes" guard.`
|
|
61447
62112
|
),
|
|
61448
62113
|
version_message: external_exports.string().optional().describe("Optional version comment"),
|
|
61449
|
-
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.")
|
|
62114
|
+
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."),
|
|
62115
|
+
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.")
|
|
61450
62116
|
},
|
|
61451
62117
|
annotations: { destructiveHint: false, idempotentHint: false }
|
|
61452
62118
|
},
|
|
61453
|
-
async ({ page_id, section, body, find_replace, version: version2, version_message, confirm_deletions }) => {
|
|
62119
|
+
async ({ page_id, section, body, find_replace, version: version2, version_message, confirm_deletions, confirm_token }) => {
|
|
61454
62120
|
const blocked = writeGuard("update_page_section", config3);
|
|
61455
62121
|
if (blocked) return blocked;
|
|
61456
62122
|
try {
|
|
@@ -61472,8 +62138,10 @@ ${truncated}${truncationNote(origLen)}`
|
|
|
61472
62138
|
}
|
|
61473
62139
|
await checkSpaceAllowed({ pageId: page_id });
|
|
61474
62140
|
const cfg = await getConfig();
|
|
62141
|
+
const cloudId = cfg.sealedCloudId;
|
|
61475
62142
|
const page = await getPage(page_id, true);
|
|
61476
62143
|
const fullBody = page.body?.storage?.value ?? page.body?.value ?? "";
|
|
62144
|
+
const pageVersion = page.version?.number ?? 0;
|
|
61477
62145
|
const resolvedVersion = version2 === "current" ? page.version?.number ?? 0 : version2;
|
|
61478
62146
|
if (resolvedVersion <= 0) {
|
|
61479
62147
|
throw new Error(
|
|
@@ -61510,7 +62178,8 @@ ${truncated}${truncationNote(origLen)}`
|
|
|
61510
62178
|
versionMessage: version_message ?? "",
|
|
61511
62179
|
deletedTokens: [],
|
|
61512
62180
|
operation: "update_page_section",
|
|
61513
|
-
clientLabel: getClientLabel(server)
|
|
62181
|
+
clientLabel: getClientLabel(server),
|
|
62182
|
+
cloudId
|
|
61514
62183
|
});
|
|
61515
62184
|
const warnings2 = [];
|
|
61516
62185
|
const labelResult2 = await ensureAttributionLabel(submitted2.page.id);
|
|
@@ -61527,16 +62196,36 @@ ${truncated}${truncationNote(origLen)}`
|
|
|
61527
62196
|
}
|
|
61528
62197
|
if (confirm_deletions) {
|
|
61529
62198
|
const deletionSummary = tryForecastDeletions(currentSectionBody, body, cfg.url);
|
|
61530
|
-
|
|
62199
|
+
const diffHash = cloudId && pageVersion > 0 ? computeDiffHash(body ?? currentSectionBody, pageVersion) : void 0;
|
|
62200
|
+
const tokenResult = await maybeConsumeConfirmToken({
|
|
62201
|
+
confirm_token,
|
|
61531
62202
|
tool: "update_page_section",
|
|
61532
|
-
|
|
61533
|
-
|
|
61534
|
-
|
|
61535
|
-
|
|
61536
|
-
source: "confirm_deletions",
|
|
61537
|
-
...deletionSummary ? { deletionSummary } : {}
|
|
61538
|
-
}
|
|
62203
|
+
cloudId,
|
|
62204
|
+
pageId: page_id,
|
|
62205
|
+
pageVersion,
|
|
62206
|
+
diffHash
|
|
61539
62207
|
});
|
|
62208
|
+
if (tokenResult === "invalid") {
|
|
62209
|
+
throw new ConverterError(
|
|
62210
|
+
"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.",
|
|
62211
|
+
"CONFIRMATION_TOKEN_INVALID"
|
|
62212
|
+
);
|
|
62213
|
+
} else if (tokenResult === "no_token") {
|
|
62214
|
+
await gateOperation(server, {
|
|
62215
|
+
tool: "update_page_section",
|
|
62216
|
+
summary: `Update section "${section}" in page ${page_id} with confirm_deletions?`,
|
|
62217
|
+
details: {
|
|
62218
|
+
page_id,
|
|
62219
|
+
section,
|
|
62220
|
+
source: "confirm_deletions",
|
|
62221
|
+
...deletionSummary ? { deletionSummary } : {}
|
|
62222
|
+
},
|
|
62223
|
+
cloudId,
|
|
62224
|
+
pageId: page_id,
|
|
62225
|
+
pageVersion,
|
|
62226
|
+
diffHash
|
|
62227
|
+
});
|
|
62228
|
+
}
|
|
61540
62229
|
}
|
|
61541
62230
|
const prepared = await safePrepareBody({
|
|
61542
62231
|
body,
|
|
@@ -61563,7 +62252,8 @@ ${truncated}${truncationNote(origLen)}`
|
|
|
61563
62252
|
versionMessage: mergedVersionMessage,
|
|
61564
62253
|
deletedTokens: prepared.deletedTokens,
|
|
61565
62254
|
operation: "update_page_section",
|
|
61566
|
-
clientLabel: getClientLabel(server)
|
|
62255
|
+
clientLabel: getClientLabel(server),
|
|
62256
|
+
cloudId
|
|
61567
62257
|
});
|
|
61568
62258
|
const warnings = [];
|
|
61569
62259
|
const labelResult = await ensureAttributionLabel(submitted.page.id);
|
|
@@ -61575,6 +62265,9 @@ ${truncated}${truncationNote(origLen)}`
|
|
|
61575
62265
|
appendWarnings(`Updated section "${section}" in: ${submitted.page.title} (ID: ${submitted.page.id}, version: ${submitted.newVersion}${removalNote})`, warnings) + echo
|
|
61576
62266
|
);
|
|
61577
62267
|
} catch (err) {
|
|
62268
|
+
if (err instanceof SoftConfirmationRequiredError) {
|
|
62269
|
+
return formatSoftConfirmationResult(err, { pageId: page_id });
|
|
62270
|
+
}
|
|
61578
62271
|
return toolErrorWithContext(err, { operation: "update_page_section", resource: `page ${page_id}`, profile: config3.profile });
|
|
61579
62272
|
}
|
|
61580
62273
|
}
|
|
@@ -61584,19 +62277,20 @@ ${truncated}${truncationNote(origLen)}`
|
|
|
61584
62277
|
{
|
|
61585
62278
|
description: describeWithLock(
|
|
61586
62279
|
withDestructiveWarning(
|
|
61587
|
-
"Update multiple sections of a Confluence page atomically in a single version bump. Either every section applies or none do \u2014 if any section's heading is missing, ambiguous, or its body fails to convert, the whole call is rejected and the page is left unchanged. Use this when you need to update 4+ sections in one go without 4 separate version bumps.\n\nSections are matched against the ORIGINAL page contents (not the cumulative-edited state) and applied in input order; sections cannot reference content introduced by an earlier section in the same call.\n\nUse headings_only to find section names first. Note: in spaces with heading auto-numbering enabled, stored heading text contains the prefix (e.g. `1.2. Section`); the matcher accepts either the prefixed or plain form. Section names must be unique within the input list."
|
|
62280
|
+
"Update multiple sections of a Confluence page atomically in a single version bump. Either every section applies or none do \u2014 if any section's heading is missing, ambiguous, or its body fails to convert, the whole call is rejected and the page is left unchanged. Use this when you need to update 4+ sections in one go without 4 separate version bumps.\n\nSections are matched against the ORIGINAL page contents (not the cumulative-edited state) and applied in input order; sections cannot reference content introduced by an earlier section in the same call.\n\nUse headings_only to find section names first. Note: in spaces with heading auto-numbering enabled, stored heading text contains the prefix (e.g. `1.2. Section`); the matcher accepts either the prefixed or plain form. Section names must be unique within the input list.\n\nIf your MCP client does not support in-protocol confirmation, this tool returns `SOFT_CONFIRMATION_REQUIRED` on the first call when destructive flags are set. STOP and ask the user before retrying. If the user approves, re-call this tool with the same parameters plus `confirm_token` from the first response. The token expires after 5 minutes and is invalidated by competing writes. See the 'Soft confirmation' section of `install-agent.md` for the full protocol."
|
|
61588
62281
|
),
|
|
61589
62282
|
config3
|
|
61590
62283
|
),
|
|
61591
62284
|
inputSchema: {
|
|
61592
62285
|
page_id: external_exports.string().describe("The Confluence page ID"),
|
|
61593
|
-
version:
|
|
62286
|
+
version: versionField.describe(
|
|
61594
62287
|
'The page version number from your most recent get_page call. Pass the literal string "current" to skip the read and apply this update on top of whatever the latest version is right now. WARNING: "current" deliberately bypasses optimistic concurrency.'
|
|
61595
62288
|
),
|
|
61596
62289
|
version_message: external_exports.string().optional().describe("Optional version comment for the single resulting revision"),
|
|
61597
62290
|
confirm_deletions: external_exports.boolean().default(false).describe(
|
|
61598
62291
|
"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."
|
|
61599
62292
|
),
|
|
62293
|
+
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."),
|
|
61600
62294
|
sections: external_exports.array(
|
|
61601
62295
|
external_exports.object({
|
|
61602
62296
|
section: external_exports.string().describe("Heading text identifying the section to replace"),
|
|
@@ -61610,14 +62304,16 @@ ${truncated}${truncationNote(origLen)}`
|
|
|
61610
62304
|
},
|
|
61611
62305
|
annotations: { destructiveHint: false, idempotentHint: false }
|
|
61612
62306
|
},
|
|
61613
|
-
async ({ page_id, version: version2, version_message, confirm_deletions, sections }) => {
|
|
62307
|
+
async ({ page_id, version: version2, version_message, confirm_deletions, sections, confirm_token }) => {
|
|
61614
62308
|
const blocked = writeGuard("update_page_sections", config3);
|
|
61615
62309
|
if (blocked) return blocked;
|
|
61616
62310
|
try {
|
|
61617
62311
|
await checkSpaceAllowed({ pageId: page_id });
|
|
61618
62312
|
const cfg = await getConfig();
|
|
62313
|
+
const cloudId = cfg.sealedCloudId;
|
|
61619
62314
|
const page = await getPage(page_id, true);
|
|
61620
62315
|
const fullBody = page.body?.storage?.value ?? page.body?.value ?? "";
|
|
62316
|
+
const pageVersion = page.version?.number ?? 0;
|
|
61621
62317
|
const resolvedVersion = version2 === "current" ? page.version?.number ?? 0 : version2;
|
|
61622
62318
|
if (resolvedVersion <= 0) {
|
|
61623
62319
|
throw new Error(
|
|
@@ -61657,16 +62353,37 @@ ${truncated}${truncationNote(origLen)}`
|
|
|
61657
62353
|
any = true;
|
|
61658
62354
|
}
|
|
61659
62355
|
}
|
|
61660
|
-
|
|
62356
|
+
const aggregateBody = sections.map((s) => s.body).join("\n");
|
|
62357
|
+
const diffHash = cloudId && pageVersion > 0 ? computeDiffHash(aggregateBody, pageVersion) : void 0;
|
|
62358
|
+
const tokenResult = await maybeConsumeConfirmToken({
|
|
62359
|
+
confirm_token,
|
|
61661
62360
|
tool: "update_page_sections",
|
|
61662
|
-
|
|
61663
|
-
|
|
61664
|
-
|
|
61665
|
-
|
|
61666
|
-
source: "confirm_deletions",
|
|
61667
|
-
...any ? { deletionSummary: summed } : {}
|
|
61668
|
-
}
|
|
62361
|
+
cloudId,
|
|
62362
|
+
pageId: page_id,
|
|
62363
|
+
pageVersion,
|
|
62364
|
+
diffHash
|
|
61669
62365
|
});
|
|
62366
|
+
if (tokenResult === "invalid") {
|
|
62367
|
+
throw new ConverterError(
|
|
62368
|
+
"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.",
|
|
62369
|
+
"CONFIRMATION_TOKEN_INVALID"
|
|
62370
|
+
);
|
|
62371
|
+
} else if (tokenResult === "no_token") {
|
|
62372
|
+
await gateOperation(server, {
|
|
62373
|
+
tool: "update_page_sections",
|
|
62374
|
+
summary: `Update ${sections.length} section${sections.length === 1 ? "" : "s"} in page ${page_id} with confirm_deletions?`,
|
|
62375
|
+
details: {
|
|
62376
|
+
page_id,
|
|
62377
|
+
section_count: sections.length,
|
|
62378
|
+
source: "confirm_deletions",
|
|
62379
|
+
...any ? { deletionSummary: summed } : {}
|
|
62380
|
+
},
|
|
62381
|
+
cloudId,
|
|
62382
|
+
pageId: page_id,
|
|
62383
|
+
pageVersion,
|
|
62384
|
+
diffHash
|
|
62385
|
+
});
|
|
62386
|
+
}
|
|
61670
62387
|
}
|
|
61671
62388
|
const prepared = await safePrepareMultiSectionBody({
|
|
61672
62389
|
currentStorage: fullBody,
|
|
@@ -61686,7 +62403,8 @@ ${truncated}${truncationNote(origLen)}`
|
|
|
61686
62403
|
regeneratedTokens: prepared.aggregatedRegeneratedTokens,
|
|
61687
62404
|
operation: "update_page_section",
|
|
61688
62405
|
clientLabel: getClientLabel(server),
|
|
61689
|
-
confirmDeletions: confirm_deletions
|
|
62406
|
+
confirmDeletions: confirm_deletions,
|
|
62407
|
+
cloudId
|
|
61690
62408
|
});
|
|
61691
62409
|
const warnings = [];
|
|
61692
62410
|
const labelResult = await ensureAttributionLabel(submitted.page.id);
|
|
@@ -61702,6 +62420,9 @@ ${truncated}${truncationNote(origLen)}`
|
|
|
61702
62420
|
) + echo
|
|
61703
62421
|
);
|
|
61704
62422
|
} catch (err) {
|
|
62423
|
+
if (err instanceof SoftConfirmationRequiredError) {
|
|
62424
|
+
return formatSoftConfirmationResult(err, { pageId: page_id });
|
|
62425
|
+
}
|
|
61705
62426
|
if (err instanceof MultiSectionError) {
|
|
61706
62427
|
return toolError(err);
|
|
61707
62428
|
}
|
|
@@ -61714,20 +62435,21 @@ ${truncated}${truncationNote(origLen)}`
|
|
|
61714
62435
|
{
|
|
61715
62436
|
description: describeWithLock(
|
|
61716
62437
|
withDestructiveWarning(
|
|
61717
|
-
"Insert content at the beginning of an existing Confluence page. The caller provides only the new content \u2014 the server fetches the existing body and handles concatenation. Safer than update_page with replace_body for additive operations.\n\nContent can be GFM markdown or Confluence storage format (auto-detected)."
|
|
62438
|
+
"Insert content at the beginning of an existing Confluence page. The caller provides only the new content \u2014 the server fetches the existing body and handles concatenation. Safer than update_page with replace_body for additive operations.\n\nContent can be GFM markdown or Confluence storage format (auto-detected).\n\nIf your MCP client does not support in-protocol confirmation, this tool returns `SOFT_CONFIRMATION_REQUIRED` on the first call when destructive flags are set. STOP and ask the user before retrying. If the user approves, re-call this tool with the same parameters plus `confirm_token` from the first response. The token expires after 5 minutes and is invalidated by competing writes. See the 'Soft confirmation' section of `install-agent.md` for the full protocol."
|
|
61718
62439
|
),
|
|
61719
62440
|
config3
|
|
61720
62441
|
),
|
|
61721
62442
|
inputSchema: {
|
|
61722
62443
|
page_id: external_exports.string().describe("The Confluence page ID"),
|
|
61723
|
-
version:
|
|
62444
|
+
version: versionField.describe(
|
|
61724
62445
|
'Page version from your most recent get_page call. Pass the literal string "current" to skip the read and apply on top of whatever the latest version is right now. WARNING: "current" bypasses optimistic concurrency \u2014 it does not protect against concurrent writes; the API can still 409 between our read and submit.'
|
|
61725
62446
|
),
|
|
61726
62447
|
content: external_exports.string().describe("Content to insert before the existing body. GFM markdown or storage format (auto-detected)."),
|
|
61727
62448
|
separator: external_exports.string().optional().describe("Separator between new and existing content. Max 100 chars, no XML tags. Defaults to blank line (markdown) or empty (storage)."),
|
|
61728
62449
|
version_message: external_exports.string().optional().describe("Optional version comment"),
|
|
61729
62450
|
allow_raw_html: external_exports.boolean().default(false).describe("Allow raw HTML inside markdown content (default false)."),
|
|
61730
|
-
confluence_base_url: external_exports.string().url().optional().describe("Override the Confluence base URL used by the link rewriter.")
|
|
62451
|
+
confluence_base_url: external_exports.string().url().optional().describe("Override the Confluence base URL used by the link rewriter."),
|
|
62452
|
+
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.")
|
|
61731
62453
|
},
|
|
61732
62454
|
annotations: { destructiveHint: false, idempotentHint: false }
|
|
61733
62455
|
},
|
|
@@ -61742,7 +62464,7 @@ ${truncated}${truncationNote(origLen)}`
|
|
|
61742
62464
|
version2,
|
|
61743
62465
|
content,
|
|
61744
62466
|
"prepend",
|
|
61745
|
-
{ separator, versionMessage: version_message ?? "Prepend content", allowRawHtml: allow_raw_html, confluenceBaseUrl: confluence_base_url ?? cfg.url }
|
|
62467
|
+
{ separator, versionMessage: version_message ?? "Prepend content", allowRawHtml: allow_raw_html, confluenceBaseUrl: confluence_base_url ?? cfg.url, cloudId: cfg.sealedCloudId }
|
|
61746
62468
|
);
|
|
61747
62469
|
const warnings = [];
|
|
61748
62470
|
const labelResult = await ensureAttributionLabel(page.id);
|
|
@@ -61751,6 +62473,9 @@ ${truncated}${truncationNote(origLen)}`
|
|
|
61751
62473
|
if (badgeResult.warning) warnings.push(badgeResult.warning);
|
|
61752
62474
|
return toolResult(appendWarnings(`Prepended to: ${page.title} (ID: ${page.id}, version: ${newVersion}, body: ${oldLen}\u2192${newLen} chars)`, warnings) + echo);
|
|
61753
62475
|
} catch (err) {
|
|
62476
|
+
if (err instanceof SoftConfirmationRequiredError) {
|
|
62477
|
+
return formatSoftConfirmationResult(err, { pageId: page_id });
|
|
62478
|
+
}
|
|
61754
62479
|
return toolErrorWithContext(err, { operation: "prepend_to_page", resource: `page ${page_id}`, profile: config3.profile });
|
|
61755
62480
|
}
|
|
61756
62481
|
}
|
|
@@ -61760,20 +62485,21 @@ ${truncated}${truncationNote(origLen)}`
|
|
|
61760
62485
|
{
|
|
61761
62486
|
description: describeWithLock(
|
|
61762
62487
|
withDestructiveWarning(
|
|
61763
|
-
"Insert content at the end of an existing Confluence page. The caller provides only the new content \u2014 the server fetches the existing body and handles concatenation. Safer than update_page with replace_body for additive operations.\n\nContent can be GFM markdown or Confluence storage format (auto-detected)."
|
|
62488
|
+
"Insert content at the end of an existing Confluence page. The caller provides only the new content \u2014 the server fetches the existing body and handles concatenation. Safer than update_page with replace_body for additive operations.\n\nContent can be GFM markdown or Confluence storage format (auto-detected).\n\nIf your MCP client does not support in-protocol confirmation, this tool returns `SOFT_CONFIRMATION_REQUIRED` on the first call when destructive flags are set. STOP and ask the user before retrying. If the user approves, re-call this tool with the same parameters plus `confirm_token` from the first response. The token expires after 5 minutes and is invalidated by competing writes. See the 'Soft confirmation' section of `install-agent.md` for the full protocol."
|
|
61764
62489
|
),
|
|
61765
62490
|
config3
|
|
61766
62491
|
),
|
|
61767
62492
|
inputSchema: {
|
|
61768
62493
|
page_id: external_exports.string().describe("The Confluence page ID"),
|
|
61769
|
-
version:
|
|
62494
|
+
version: versionField.describe(
|
|
61770
62495
|
'Page version from your most recent get_page call. Pass the literal string "current" to skip the read and apply on top of whatever the latest version is right now. WARNING: "current" bypasses optimistic concurrency \u2014 it does not protect against concurrent writes; the API can still 409 between our read and submit.'
|
|
61771
62496
|
),
|
|
61772
62497
|
content: external_exports.string().describe("Content to insert after the existing body. GFM markdown or storage format (auto-detected)."),
|
|
61773
62498
|
separator: external_exports.string().optional().describe("Separator between existing and new content. Max 100 chars, no XML tags. Defaults to blank line (markdown) or empty (storage)."),
|
|
61774
62499
|
version_message: external_exports.string().optional().describe("Optional version comment"),
|
|
61775
62500
|
allow_raw_html: external_exports.boolean().default(false).describe("Allow raw HTML inside markdown content (default false)."),
|
|
61776
|
-
confluence_base_url: external_exports.string().url().optional().describe("Override the Confluence base URL used by the link rewriter.")
|
|
62501
|
+
confluence_base_url: external_exports.string().url().optional().describe("Override the Confluence base URL used by the link rewriter."),
|
|
62502
|
+
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.")
|
|
61777
62503
|
},
|
|
61778
62504
|
annotations: { destructiveHint: false, idempotentHint: false }
|
|
61779
62505
|
},
|
|
@@ -61788,7 +62514,7 @@ ${truncated}${truncationNote(origLen)}`
|
|
|
61788
62514
|
version2,
|
|
61789
62515
|
content,
|
|
61790
62516
|
"append",
|
|
61791
|
-
{ separator, versionMessage: version_message ?? "Append content", allowRawHtml: allow_raw_html, confluenceBaseUrl: confluence_base_url ?? cfg.url }
|
|
62517
|
+
{ separator, versionMessage: version_message ?? "Append content", allowRawHtml: allow_raw_html, confluenceBaseUrl: confluence_base_url ?? cfg.url, cloudId: cfg.sealedCloudId }
|
|
61792
62518
|
);
|
|
61793
62519
|
const warnings = [];
|
|
61794
62520
|
const labelResult = await ensureAttributionLabel(page.id);
|
|
@@ -61797,6 +62523,9 @@ ${truncated}${truncationNote(origLen)}`
|
|
|
61797
62523
|
if (badgeResult.warning) warnings.push(badgeResult.warning);
|
|
61798
62524
|
return toolResult(appendWarnings(`Appended to: ${page.title} (ID: ${page.id}, version: ${newVersion}, body: ${oldLen}\u2192${newLen} chars)`, warnings) + echo);
|
|
61799
62525
|
} catch (err) {
|
|
62526
|
+
if (err instanceof SoftConfirmationRequiredError) {
|
|
62527
|
+
return formatSoftConfirmationResult(err, { pageId: page_id });
|
|
62528
|
+
}
|
|
61800
62529
|
return toolErrorWithContext(err, { operation: "append_to_page", resource: `page ${page_id}`, profile: config3.profile });
|
|
61801
62530
|
}
|
|
61802
62531
|
}
|
|
@@ -62728,7 +63457,7 @@ ${sectionFenced}`
|
|
|
62728
63457
|
{
|
|
62729
63458
|
description: describeWithLock(
|
|
62730
63459
|
withDestructiveWarning(
|
|
62731
|
-
"Revert a Confluence page to a previous version. Fetches the exact storage-format body from the historical version and pushes it as a new version. This is a lossless revert \u2014 unlike reading get_page_version (which returns sanitized markdown) and passing it to update_page, this preserves all macros, formatting, and rich elements exactly.\n\nThe shrinkage guard applies: if the reverted content is significantly smaller than the current content, you will be asked to confirm."
|
|
63460
|
+
"Revert a Confluence page to a previous version. Fetches the exact storage-format body from the historical version and pushes it as a new version. This is a lossless revert \u2014 unlike reading get_page_version (which returns sanitized markdown) and passing it to update_page, this preserves all macros, formatting, and rich elements exactly.\n\nThe shrinkage guard applies: if the reverted content is significantly smaller than the current content, you will be asked to confirm.\n\nIf your MCP client does not support in-protocol confirmation, this tool returns `SOFT_CONFIRMATION_REQUIRED` on the first call when destructive flags are set. STOP and ask the user before retrying. If the user approves, re-call this tool with the same parameters plus `confirm_token` from the first response. The token expires after 5 minutes and is invalidated by competing writes. See the 'Soft confirmation' section of `install-agent.md` for the full protocol."
|
|
62732
63461
|
),
|
|
62733
63462
|
config3
|
|
62734
63463
|
),
|
|
@@ -62749,7 +63478,8 @@ ${sectionFenced}`
|
|
|
62749
63478
|
version_message: external_exports.string().optional().describe(
|
|
62750
63479
|
"Optional version comment. Defaults to 'Revert to version N'."
|
|
62751
63480
|
),
|
|
62752
|
-
source: sourceSchema
|
|
63481
|
+
source: sourceSchema,
|
|
63482
|
+
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.")
|
|
62753
63483
|
},
|
|
62754
63484
|
annotations: { destructiveHint: false, idempotentHint: false }
|
|
62755
63485
|
},
|
|
@@ -62760,7 +63490,8 @@ ${sectionFenced}`
|
|
|
62760
63490
|
confirm_shrinkage,
|
|
62761
63491
|
confirm_structure_loss,
|
|
62762
63492
|
version_message,
|
|
62763
|
-
source
|
|
63493
|
+
source,
|
|
63494
|
+
confirm_token
|
|
62764
63495
|
}) => {
|
|
62765
63496
|
const blocked = writeGuard("revert_page", config3);
|
|
62766
63497
|
if (blocked) return blocked;
|
|
@@ -62772,18 +63503,41 @@ ${sectionFenced}`
|
|
|
62772
63503
|
targetVersion: target_version
|
|
62773
63504
|
});
|
|
62774
63505
|
const effectiveSource = validateSource(source, flagsSet);
|
|
62775
|
-
await
|
|
63506
|
+
const cfg = await getConfig();
|
|
63507
|
+
const cloudId = cfg.sealedCloudId;
|
|
63508
|
+
const pageVersion = current_version;
|
|
63509
|
+
const diffHash = cloudId && pageVersion > 0 ? computeDiffHash("", pageVersion) : void 0;
|
|
63510
|
+
const tokenResult = await maybeConsumeConfirmToken({
|
|
63511
|
+
confirm_token,
|
|
62776
63512
|
tool: "revert_page",
|
|
62777
|
-
|
|
62778
|
-
|
|
62779
|
-
|
|
62780
|
-
|
|
62781
|
-
current_version,
|
|
62782
|
-
confirm_shrinkage,
|
|
62783
|
-
confirm_structure_loss,
|
|
62784
|
-
source: effectiveSource
|
|
62785
|
-
}
|
|
63513
|
+
cloudId,
|
|
63514
|
+
pageId: page_id,
|
|
63515
|
+
pageVersion,
|
|
63516
|
+
diffHash
|
|
62786
63517
|
});
|
|
63518
|
+
if (tokenResult === "invalid") {
|
|
63519
|
+
throw new ConverterError(
|
|
63520
|
+
"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.",
|
|
63521
|
+
"CONFIRMATION_TOKEN_INVALID"
|
|
63522
|
+
);
|
|
63523
|
+
} else if (tokenResult === "no_token") {
|
|
63524
|
+
await gateOperation(server, {
|
|
63525
|
+
tool: "revert_page",
|
|
63526
|
+
summary: `Revert page ${page_id} to version ${target_version}?`,
|
|
63527
|
+
details: {
|
|
63528
|
+
page_id,
|
|
63529
|
+
target_version,
|
|
63530
|
+
current_version,
|
|
63531
|
+
confirm_shrinkage,
|
|
63532
|
+
confirm_structure_loss,
|
|
63533
|
+
source: effectiveSource
|
|
63534
|
+
},
|
|
63535
|
+
cloudId,
|
|
63536
|
+
pageId: page_id,
|
|
63537
|
+
pageVersion,
|
|
63538
|
+
diffHash
|
|
63539
|
+
});
|
|
63540
|
+
}
|
|
62787
63541
|
const currentPage = await getPage(page_id, true);
|
|
62788
63542
|
const currentStorage = currentPage.body?.storage?.value ?? currentPage.body?.value ?? "";
|
|
62789
63543
|
const actualVersion = currentPage.version?.number;
|
|
@@ -62818,7 +63572,9 @@ ${sectionFenced}`
|
|
|
62818
63572
|
confirmShrinkage: confirm_shrinkage,
|
|
62819
63573
|
confirmStructureLoss: confirm_structure_loss,
|
|
62820
63574
|
// E2: thread validated source for the mutation log.
|
|
62821
|
-
source: effectiveSource
|
|
63575
|
+
source: effectiveSource,
|
|
63576
|
+
// 2.E: defense-in-depth token invalidation after successful write.
|
|
63577
|
+
cloudId
|
|
62822
63578
|
});
|
|
62823
63579
|
const warnings = [];
|
|
62824
63580
|
const labelResult = await ensureAttributionLabel(submitted.page.id);
|
|
@@ -62832,6 +63588,9 @@ ${sectionFenced}`
|
|
|
62832
63588
|
) + echo
|
|
62833
63589
|
);
|
|
62834
63590
|
} catch (err) {
|
|
63591
|
+
if (err instanceof SoftConfirmationRequiredError) {
|
|
63592
|
+
return formatSoftConfirmationResult(err, { pageId: page_id });
|
|
63593
|
+
}
|
|
62835
63594
|
return toolErrorWithContext(err, { operation: "revert_page", resource: `page ${page_id}`, profile: config3.profile });
|
|
62836
63595
|
}
|
|
62837
63596
|
}
|
|
@@ -62921,7 +63680,7 @@ ${titleFenced}${echo2}`
|
|
|
62921
63680
|
inputSchema: {}
|
|
62922
63681
|
},
|
|
62923
63682
|
async () => {
|
|
62924
|
-
let text2 = `epimethian-mcp v${"6.
|
|
63683
|
+
let text2 = `epimethian-mcp v${"6.6.1"}`;
|
|
62925
63684
|
try {
|
|
62926
63685
|
const pending = await getPendingUpdate();
|
|
62927
63686
|
if (pending) {
|
|
@@ -62952,7 +63711,7 @@ ${label} update available: v${pending.current} \u2192 v${pending.latest}. Run \`
|
|
|
62952
63711
|
const pending = await getPendingUpdate();
|
|
62953
63712
|
if (!pending) {
|
|
62954
63713
|
return toolResult(
|
|
62955
|
-
`epimethian-mcp v${"6.
|
|
63714
|
+
`epimethian-mcp v${"6.6.1"} is already up to date.`
|
|
62956
63715
|
);
|
|
62957
63716
|
}
|
|
62958
63717
|
const output = await performUpgrade(pending.latest);
|
|
@@ -62974,7 +63733,7 @@ async function startRecoveryServer(profile) {
|
|
|
62974
63733
|
const server = new McpServer(
|
|
62975
63734
|
{
|
|
62976
63735
|
name: `confluence-${profile}-setup-needed`,
|
|
62977
|
-
version: "6.
|
|
63736
|
+
version: "6.6.1"
|
|
62978
63737
|
},
|
|
62979
63738
|
{
|
|
62980
63739
|
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.`
|
|
@@ -63025,21 +63784,21 @@ async function main() {
|
|
|
63025
63784
|
const serverName = config3.profile ? `confluence-${config3.profile}` : "confluence";
|
|
63026
63785
|
const server = new McpServer({
|
|
63027
63786
|
name: serverName,
|
|
63028
|
-
version: "6.
|
|
63787
|
+
version: "6.6.1"
|
|
63029
63788
|
});
|
|
63030
63789
|
await registerTools(server, config3);
|
|
63031
63790
|
const transport = new StdioServerTransport();
|
|
63032
63791
|
await server.connect(transport);
|
|
63033
63792
|
try {
|
|
63034
63793
|
const pending = await getPendingUpdate();
|
|
63035
|
-
if (pending && pending.current === "6.
|
|
63794
|
+
if (pending && pending.current === "6.6.1") {
|
|
63036
63795
|
console.error(
|
|
63037
63796
|
`epimethian-mcp: update available: v${pending.current} \u2192 v${pending.latest} (${pending.type}). Run \`epimethian-mcp upgrade\` to install.`
|
|
63038
63797
|
);
|
|
63039
63798
|
}
|
|
63040
63799
|
} catch {
|
|
63041
63800
|
}
|
|
63042
|
-
checkForUpdates("6.
|
|
63801
|
+
checkForUpdates("6.6.1").catch(() => {
|
|
63043
63802
|
});
|
|
63044
63803
|
}
|
|
63045
63804
|
|
|
@@ -63049,8 +63808,10 @@ async function run() {
|
|
|
63049
63808
|
if (command === "setup") {
|
|
63050
63809
|
const idx = process.argv.indexOf("--profile");
|
|
63051
63810
|
const profile = idx > -1 ? process.argv[idx + 1] : void 0;
|
|
63811
|
+
const clientIdx = process.argv.indexOf("--client");
|
|
63812
|
+
const clientId = clientIdx > -1 ? process.argv[clientIdx + 1] : void 0;
|
|
63052
63813
|
const { runSetup: runSetup2 } = await Promise.resolve().then(() => (init_setup(), setup_exports));
|
|
63053
|
-
await runSetup2(profile);
|
|
63814
|
+
await runSetup2(profile, clientId);
|
|
63054
63815
|
} else if (command === "profiles") {
|
|
63055
63816
|
const { runProfiles: runProfiles2 } = await Promise.resolve().then(() => (init_profiles2(), profiles_exports));
|
|
63056
63817
|
await runProfiles2();
|