ai-dev-requirements 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -211,7 +211,7 @@ function loadConfig(startDir) {
211
211
  }
212
212
  //#endregion
213
213
  //#region package.json
214
- var version = "0.4.0";
214
+ var version = "0.5.0";
215
215
  //#endregion
216
216
  //#region ../../src/utils/ones-issue-kind.ts
217
217
  /**
@@ -291,18 +291,34 @@ var BaseAdapter = class {
291
291
  async updateWikiPage(_params) {
292
292
  throw new Error(`${this.sourceType}: Wiki update endpoint is not verified`);
293
293
  }
294
+ /** Production Wiki deletes stay disabled until the exact provider endpoint is verified. */
295
+ async deleteWikiPage(_params) {
296
+ throw new Error(`${this.sourceType}: Wiki delete endpoint is not verified`);
297
+ }
294
298
  };
295
299
  //#endregion
296
300
  //#region ../../src/adapters/ones/api-client.ts
297
301
  function base64Url(buffer) {
298
302
  return buffer.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
299
303
  }
300
- function getSetCookies(response) {
304
+ function getSetCookies$1(response) {
305
+ if (!response.headers) return [];
301
306
  const headers = response.headers;
302
307
  if (headers.getSetCookie) return headers.getSetCookie();
303
308
  const raw = response.headers.get("set-cookie");
304
309
  return raw ? [raw] : [];
305
310
  }
311
+ function mergeResponseCookies(cookieJar, response) {
312
+ for (const cookie of getSetCookies$1(response)) {
313
+ const pair = cookie.split(";")[0];
314
+ const separator = pair.indexOf("=");
315
+ if (separator <= 0) continue;
316
+ cookieJar.set(pair.slice(0, separator), pair.slice(separator + 1));
317
+ }
318
+ }
319
+ function serializeCookies(cookieJar) {
320
+ return [...cookieJar].map(([name, value]) => `${name}=${value}`).join("; ");
321
+ }
306
322
  function parseRedirectValue(location, names) {
307
323
  try {
308
324
  const parsed = new URL(location);
@@ -330,6 +346,7 @@ var OnesApiClient = class {
330
346
  const email = this.resolvedAuth.email;
331
347
  const password = this.resolvedAuth.password;
332
348
  if (!email || !password) throw new Error("ONES auth requires email and password (ones-pkce auth type)");
349
+ const cookieJar = /* @__PURE__ */ new Map();
333
350
  const certRes = await fetch(`${baseUrl}/identity/api/encryption_cert`, {
334
351
  method: "POST",
335
352
  headers: { "Content-Type": "application/json" },
@@ -350,11 +367,15 @@ var OnesApiClient = class {
350
367
  })
351
368
  });
352
369
  if (!loginRes.ok) throw new Error(`ONES: Login failed with status ${loginRes.status}`);
353
- const cookies = getSetCookies(loginRes).map((cookie) => cookie.split(";")[0]).join("; ");
370
+ mergeResponseCookies(cookieJar, loginRes);
354
371
  const loginData = await loginRes.json();
355
372
  const configuredOrgUuid = this.config.options?.orgUuid;
356
373
  const orgUser = configuredOrgUuid ? loginData.org_users.find((user) => user.org_uuid === configuredOrgUuid) ?? loginData.org_users[0] : loginData.org_users[0];
357
374
  if (!orgUser) throw new Error("ONES: No organizations found for this user");
375
+ cookieJar.set("ones-region-uuid", orgUser.region_uuid);
376
+ cookieJar.set("ones-org-uuid", orgUser.org_uuid);
377
+ const timezone = cookieJar.get("ones-tz");
378
+ if (timezone) cookieJar.set("timezone", timezone);
358
379
  const codeVerifier = base64Url(node_crypto.default.randomBytes(32));
359
380
  const codeChallenge = base64Url(node_crypto.default.createHash("sha256").update(codeVerifier).digest());
360
381
  const authorizeParams = new URLSearchParams({
@@ -366,15 +387,17 @@ var OnesApiClient = class {
366
387
  redirect_uri: `${baseUrl}/auth/authorize/callback`,
367
388
  state: `org_uuid=${orgUser.org_uuid}`
368
389
  });
369
- const authorizeLocation = (await fetch(`${baseUrl}/identity/authorize`, {
390
+ const authorizeRes = await fetch(`${baseUrl}/identity/authorize`, {
370
391
  method: "POST",
371
392
  headers: {
372
393
  "Content-Type": "application/x-www-form-urlencoded",
373
- "Cookie": cookies
394
+ "Cookie": serializeCookies(cookieJar)
374
395
  },
375
396
  body: authorizeParams.toString(),
376
397
  redirect: "manual"
377
- })).headers.get("location");
398
+ });
399
+ mergeResponseCookies(cookieJar, authorizeRes);
400
+ const authorizeLocation = authorizeRes.headers.get("location");
378
401
  if (!authorizeLocation) throw new Error("ONES: Authorize response missing location header");
379
402
  let code = parseRedirectValue(authorizeLocation, ["code"]);
380
403
  if (!code) {
@@ -384,7 +407,7 @@ var OnesApiClient = class {
384
407
  method: "POST",
385
408
  headers: {
386
409
  "Content-Type": "application/json;charset=UTF-8",
387
- "Cookie": cookies
410
+ "Cookie": serializeCookies(cookieJar)
388
411
  },
389
412
  body: JSON.stringify({
390
413
  auth_request_id: authRequestId,
@@ -393,12 +416,15 @@ var OnesApiClient = class {
393
416
  org_user_uuid: orgUser.org_user.org_user_uuid
394
417
  })
395
418
  });
419
+ mergeResponseCookies(cookieJar, finalizeRes);
396
420
  if (!finalizeRes.ok) throw new Error(`ONES: Finalize failed with status ${finalizeRes.status}`);
397
- const callbackLocation = (await fetch(`${baseUrl}/identity/authorize/callback?id=${authRequestId}&lang=zh`, {
421
+ const callbackRes = await fetch(`${baseUrl}/identity/authorize/callback?id=${authRequestId}&lang=zh`, {
398
422
  method: "GET",
399
- headers: { Cookie: cookies },
423
+ headers: { Cookie: serializeCookies(cookieJar) },
400
424
  redirect: "manual"
401
- })).headers.get("location");
425
+ });
426
+ mergeResponseCookies(cookieJar, callbackRes);
427
+ const callbackLocation = callbackRes.headers.get("location");
402
428
  if (!callbackLocation) throw new Error("ONES: Callback response missing location header");
403
429
  code = parseRedirectValue(callbackLocation, ["code"]);
404
430
  }
@@ -407,7 +433,7 @@ var OnesApiClient = class {
407
433
  method: "POST",
408
434
  headers: {
409
435
  "Content-Type": "application/x-www-form-urlencoded",
410
- "Cookie": cookies
436
+ "Cookie": serializeCookies(cookieJar)
411
437
  },
412
438
  body: new URLSearchParams({
413
439
  grant_type: "authorization_code",
@@ -417,16 +443,20 @@ var OnesApiClient = class {
417
443
  redirect_uri: `${baseUrl}/auth/authorize/callback`
418
444
  }).toString()
419
445
  });
446
+ mergeResponseCookies(cookieJar, tokenRes);
420
447
  if (!tokenRes.ok) throw new Error(`ONES: Token exchange failed with status ${tokenRes.status}`);
421
448
  const token = await tokenRes.json();
449
+ cookieJar.set("ones-lt", token.access_token);
422
450
  const teamsRes = await fetch(`${baseUrl}/project/api/project/organization/${orgUser.org_uuid}/stamps/data?t=org_my_team`, {
423
451
  method: "POST",
424
452
  headers: {
425
453
  "Authorization": `Bearer ${token.access_token}`,
426
- "Content-Type": "application/json;charset=UTF-8"
454
+ "Content-Type": "application/json;charset=UTF-8",
455
+ "Cookie": serializeCookies(cookieJar)
427
456
  },
428
457
  body: JSON.stringify({ org_my_team: 0 })
429
458
  });
459
+ mergeResponseCookies(cookieJar, teamsRes);
430
460
  if (!teamsRes.ok) throw new Error(`ONES: Failed to fetch teams: ${teamsRes.status}`);
431
461
  const teams = (await teamsRes.json()).org_my_team?.teams ?? [];
432
462
  const configuredTeamUuid = this.config.options?.teamUuid;
@@ -438,6 +468,9 @@ var OnesApiClient = class {
438
468
  orgUuid: orgUser.org_uuid,
439
469
  userUuid: orgUser.org_user.org_user_uuid,
440
470
  userName: orgUser.org_user.name,
471
+ cookieHeader: serializeCookies(cookieJar),
472
+ legacyAuthToken: loginData.sid,
473
+ legacyUserId: loginData.auth_user_uuid,
441
474
  expiresAt: Date.now() + (token.expires_in - 60) * 1e3
442
475
  };
443
476
  return this.session;
@@ -670,10 +703,11 @@ function mapOnesTypeFromTask(task) {
670
703
  return mapOnesType(task.subIssueType?.name ?? task.issueType?.name ?? "");
671
704
  }
672
705
  function toRequirement(task, description = "", attachments = []) {
706
+ const displayId = taskDisplayId({}, task, task.project?.identifier?.toUpperCase() ?? null);
673
707
  return {
674
708
  id: task.uuid,
675
709
  source: "ones",
676
- title: `#${task.number} ${task.name}`,
710
+ title: `${displayId} ${task.name}`,
677
711
  description,
678
712
  status: mapOnesStatus(task.status?.category ?? "to_do"),
679
713
  priority: mapOnesPriority(task.priority?.value ?? "normal"),
@@ -1192,17 +1226,43 @@ function renderWikiContent(content, context = { imageSources: [] }) {
1192
1226
  return asWikiBlocks(document.blocks).map((block) => renderWikiBlock(block, document, context)).filter(Boolean).join("\n\n").replace(/[ \t]+\n/g, "\n").replace(/\n{3,}/g, "\n\n").trim();
1193
1227
  }
1194
1228
  function newWikiBlockId() {
1195
- return (0, node_crypto.randomBytes)(8).toString("base64url").slice(0, 10);
1229
+ for (;;) {
1230
+ const id = (0, node_crypto.randomBytes)(9).toString("base64url").replace(/[^a-z0-9]/gi, "").slice(0, 9);
1231
+ if (/^[a-z][a-z0-9]{8}$/i.test(id)) return id;
1232
+ }
1233
+ }
1234
+ function markdownTextRuns(text) {
1235
+ if (!text) return [];
1236
+ const runs = [];
1237
+ const inlinePattern = /\[([^\]]+)\]\((https?:\/\/[^)\s]+)\)|`([^`]+)`/g;
1238
+ let cursor = 0;
1239
+ for (const match of text.matchAll(inlinePattern)) {
1240
+ const index = match.index ?? 0;
1241
+ if (index > cursor) runs.push({ insert: text.slice(cursor, index) });
1242
+ if (match[1] && match[2]) runs.push({
1243
+ insert: match[1],
1244
+ attributes: { link: match[2] }
1245
+ });
1246
+ else if (match[3]) runs.push({
1247
+ insert: match[3],
1248
+ attributes: { "style-code": true }
1249
+ });
1250
+ cursor = index + match[0].length;
1251
+ }
1252
+ if (cursor < text.length) runs.push({ insert: text.slice(cursor) });
1253
+ return runs.length ? runs : [{ insert: text }];
1196
1254
  }
1197
1255
  function wikiTextBlock(text, options = {}) {
1198
1256
  return {
1199
1257
  id: newWikiBlockId(),
1200
1258
  type: options.list ? "list" : "text",
1201
- text: text ? [{ insert: text }] : [],
1259
+ text: markdownTextRuns(text),
1202
1260
  ...options.heading ? { heading: options.heading } : {},
1203
1261
  ...options.list ? {
1204
1262
  ordered: options.ordered ?? false,
1205
- level: 1
1263
+ level: options.level ?? 1,
1264
+ ...options.start === void 0 ? {} : { start: options.start },
1265
+ ...options.groupId ? { groupId: options.groupId } : {}
1206
1266
  } : {}
1207
1267
  };
1208
1268
  }
@@ -1213,6 +1273,9 @@ function isMarkdownSeparatorRow(line) {
1213
1273
  const cells = parseMarkdownRow(line);
1214
1274
  return cells.length > 0 && cells.every((cell) => /^:?-{3,}:?$/.test(cell));
1215
1275
  }
1276
+ function estimateWikiTableCellWidth(value) {
1277
+ return [...value].reduce((width, character) => width + (character.codePointAt(0) > 127 ? 14 : 7), 24);
1278
+ }
1216
1279
  function markdownToWikiDocument(markdown) {
1217
1280
  const document = {
1218
1281
  blocks: [],
@@ -1221,9 +1284,15 @@ function markdownToWikiDocument(markdown) {
1221
1284
  };
1222
1285
  const blocks = document.blocks;
1223
1286
  const lines = markdown.replace(/\r\n/g, "\n").split("\n");
1287
+ let activeList = null;
1224
1288
  for (let index = 0; index < lines.length; index += 1) {
1225
1289
  const line = lines[index];
1290
+ if (!line.trim()) {
1291
+ activeList = null;
1292
+ continue;
1293
+ }
1226
1294
  if (line.includes("|") && lines[index + 1] && isMarkdownSeparatorRow(lines[index + 1])) {
1295
+ activeList = null;
1227
1296
  const rows = [parseMarkdownRow(line)];
1228
1297
  index += 2;
1229
1298
  while (index < lines.length && lines[index].includes("|")) {
@@ -1241,34 +1310,120 @@ function markdownToWikiDocument(markdown) {
1241
1310
  blocks.push({
1242
1311
  id: newWikiBlockId(),
1243
1312
  type: "table",
1244
- rows: rows.length,
1245
1313
  cols: columnCount,
1314
+ rows: rows.length,
1315
+ colsWidth: Array.from({ length: columnCount }, (_, column) => Math.max(100, ...rows.map((row) => estimateWikiTableCellWidth(row[column] ?? "")))),
1246
1316
  children
1247
1317
  });
1248
1318
  continue;
1249
1319
  }
1250
1320
  const heading = line.match(/^(#{1,6})[ \t]/);
1251
1321
  if (heading?.[1]) {
1322
+ activeList = null;
1252
1323
  blocks.push(wikiTextBlock(line.slice(heading[0].length).trim(), { heading: heading[1].length }));
1253
1324
  continue;
1254
1325
  }
1255
1326
  const unordered = line.match(/^[ \t]*[-*+][ \t]/);
1256
1327
  if (unordered) {
1257
- blocks.push(wikiTextBlock(line.slice(unordered[0].length).trim(), { list: true }));
1328
+ const groupId = activeList && !activeList.ordered ? activeList.groupId : newWikiBlockId();
1329
+ const start = activeList && !activeList.ordered ? activeList.nextStart : 1;
1330
+ activeList = {
1331
+ ordered: false,
1332
+ groupId,
1333
+ nextStart: start + 1
1334
+ };
1335
+ blocks.push(wikiTextBlock(line.slice(unordered[0].length).trim(), {
1336
+ list: true,
1337
+ groupId,
1338
+ start
1339
+ }));
1258
1340
  continue;
1259
1341
  }
1260
- const ordered = line.match(/^[ \t]*\d+[.)][ \t]/);
1261
- if (ordered) {
1342
+ const ordered = line.match(/^[ \t]*(\d+)[.)][ \t]/);
1343
+ if (ordered?.[1]) {
1344
+ const groupId = activeList?.ordered ? activeList.groupId : newWikiBlockId();
1345
+ const start = Number.parseInt(ordered[1], 10);
1346
+ activeList = {
1347
+ ordered: true,
1348
+ groupId,
1349
+ nextStart: start + 1
1350
+ };
1262
1351
  blocks.push(wikiTextBlock(line.slice(ordered[0].length).trim(), {
1263
1352
  list: true,
1264
- ordered: true
1353
+ ordered: true,
1354
+ start,
1355
+ groupId
1265
1356
  }));
1266
1357
  continue;
1267
1358
  }
1359
+ activeList = null;
1268
1360
  blocks.push(wikiTextBlock(line));
1269
1361
  }
1270
1362
  return document;
1271
1363
  }
1364
+ function markdownToWikiHtml(markdown) {
1365
+ const renderInline = (text) => markdownTextRuns(text).map((run) => {
1366
+ const escaped = escapeWikiHtml(run.insert);
1367
+ const content = run.attributes?.["style-code"] ? `<code>${escaped}</code>` : escaped;
1368
+ return run.attributes?.link ? `<a href="${escapeWikiHtml(run.attributes.link)}" target="_blank" rel="noopener noreferrer">${content}</a>` : content;
1369
+ }).join("");
1370
+ const lines = markdown.replace(/\r\n/g, "\n").split("\n");
1371
+ const html = [];
1372
+ for (let index = 0; index < lines.length; index += 1) {
1373
+ const line = lines[index];
1374
+ if (!line.trim()) continue;
1375
+ if (line.includes("|") && lines[index + 1] && isMarkdownSeparatorRow(lines[index + 1])) {
1376
+ const rows = [parseMarkdownRow(line)];
1377
+ index += 2;
1378
+ while (index < lines.length && lines[index].includes("|")) {
1379
+ rows.push(parseMarkdownRow(lines[index]));
1380
+ index += 1;
1381
+ }
1382
+ index -= 1;
1383
+ const columnCount = Math.max(...rows.map((row) => row.length));
1384
+ const width = Math.floor(100 / columnCount);
1385
+ const header = Array.from({ length: columnCount }).map((_, column) => `<th style="width:${width}%">${renderInline(rows[0][column] ?? "")}</th>`).join("");
1386
+ const body = rows.slice(1).map((row) => `<tr>${Array.from({ length: columnCount }).map((_, column) => `<td>${renderInline(row[column] ?? "")}</td>`).join("")}</tr>`).join("");
1387
+ html.push(`<table style="width:100%"><thead><tr>${header}</tr></thead><tbody>${body}</tbody></table>`);
1388
+ continue;
1389
+ }
1390
+ const heading = line.match(/^(#{1,6})[ \t]/);
1391
+ if (heading?.[1]) {
1392
+ const level = heading[1].length;
1393
+ html.push(`<h${level}>${renderInline(line.slice(heading[0].length).trim())}</h${level}>`);
1394
+ continue;
1395
+ }
1396
+ if (line.match(/^[ \t]*(\d+)[.)][ \t]/)?.[1]) {
1397
+ const items = [];
1398
+ while (index < lines.length) {
1399
+ const item = lines[index].match(/^[ \t]*(\d+)[.)][ \t]/);
1400
+ if (!item?.[1]) break;
1401
+ items.push({
1402
+ value: Number.parseInt(item[1], 10),
1403
+ text: lines[index].slice(item[0].length).trim()
1404
+ });
1405
+ index += 1;
1406
+ }
1407
+ index -= 1;
1408
+ html.push(`<ol start="${items[0].value}">${items.map((item) => `<li>${renderInline(item.text)}</li>`).join("")}</ol>`);
1409
+ continue;
1410
+ }
1411
+ if (line.match(/^[ \t]*[-*+][ \t]/)) {
1412
+ const items = [];
1413
+ while (index < lines.length) {
1414
+ const item = lines[index].match(/^[ \t]*[-*+][ \t]/);
1415
+ if (!item) break;
1416
+ items.push(lines[index].slice(item[0].length).trim());
1417
+ index += 1;
1418
+ }
1419
+ index -= 1;
1420
+ html.push(`<ul>${items.map((item) => `<li>${renderInline(item)}</li>`).join("")}</ul>`);
1421
+ continue;
1422
+ }
1423
+ html.push(`<p>${renderInline(line.trim())}</p>`);
1424
+ }
1425
+ return html.join("\n");
1426
+ }
1272
1427
  function parseWikiDocument(content) {
1273
1428
  const document = parseJsonRecord(content);
1274
1429
  if (!document || !Array.isArray(document.blocks)) throw new Error("ONES: Wiki content is not a supported collaborative document");
@@ -1325,6 +1480,17 @@ function appendWikiTableRow(document, operation) {
1325
1480
  table.rows = (typeof table.rows === "number" ? table.rows : layout.rows.length) + 1;
1326
1481
  }
1327
1482
  function applyWikiUpdateOperation(content, operation) {
1483
+ if (operation.type === "replace_document") {
1484
+ const current = parseWikiDocument(content);
1485
+ const replacement = markdownToWikiDocument(operation.markdown);
1486
+ for (const key of [
1487
+ "comments",
1488
+ "meta",
1489
+ "authors",
1490
+ "commentators"
1491
+ ]) if (Object.hasOwn(current, key)) replacement[key] = current[key];
1492
+ return JSON.stringify(replacement);
1493
+ }
1328
1494
  const document = parseWikiDocument(content);
1329
1495
  if (operation.type === "append_blocks") appendWikiDocument(document, markdownToWikiDocument(operation.markdown));
1330
1496
  else if (operation.type === "replace_text") replaceWikiText(document, operation.find, operation.replace);
@@ -1363,6 +1529,26 @@ var WikiPathResolutionError = class extends Error {
1363
1529
  };
1364
1530
  //#endregion
1365
1531
  //#region ../../src/adapters/ones/wiki-reader.ts
1532
+ const CURRENT_USER_PATH_ALIASES = {
1533
+ "i": true,
1534
+ "me": true,
1535
+ "mine": true,
1536
+ "my": true,
1537
+ "myself": true,
1538
+ "current user": true,
1539
+ "current-user": true,
1540
+ "current_user": true,
1541
+ "我": true,
1542
+ "我的": true,
1543
+ "本人": true,
1544
+ "当前用户": true,
1545
+ "当前账号": true,
1546
+ "自己": true
1547
+ };
1548
+ function currentUserPathAlias(segment) {
1549
+ const trimmed = segment.trim();
1550
+ return CURRENT_USER_PATH_ALIASES[trimmed.toLocaleLowerCase()] ? trimmed : null;
1551
+ }
1366
1552
  var OnesWikiOpenApiError = class extends Error {
1367
1553
  status;
1368
1554
  reason;
@@ -1457,12 +1643,29 @@ function segmentSimilarity(left, right) {
1457
1643
  var OnesWikiReader = class {
1458
1644
  options;
1459
1645
  treeCache = /* @__PURE__ */ new Map();
1646
+ currentUserNameRequest = null;
1460
1647
  constructor(options) {
1461
1648
  this.options = options;
1462
1649
  }
1463
1650
  invalidateTree(teamId, spaceId) {
1464
1651
  this.treeCache.delete(`${teamId}:${spaceId}`);
1465
1652
  }
1653
+ async fetchCurrentUserName() {
1654
+ const session = await this.options.getSession();
1655
+ const response = await fetch(new URL("/wiki/api/project/auth/token_info", this.options.apiBase).toString(), { headers: { Authorization: `Bearer ${session.accessToken}` } });
1656
+ if (!response.ok) throw new Error(`ONES Wiki token info error: ${response.status}`);
1657
+ const payload = await response.json();
1658
+ const name = firstNonEmptyString$1(payload.user?.name, payload.data?.user?.name, payload.data?.name, payload.name, payload.user_name, payload.userName);
1659
+ if (!name) throw new Error("ONES Wiki token info did not include a resolvable current account");
1660
+ return name;
1661
+ }
1662
+ currentUserName() {
1663
+ if (!this.currentUserNameRequest) this.currentUserNameRequest = this.fetchCurrentUserName().catch((error) => {
1664
+ this.currentUserNameRequest = null;
1665
+ throw error;
1666
+ });
1667
+ return this.currentUserNameRequest;
1668
+ }
1466
1669
  async openApi(apiPath) {
1467
1670
  if (!apiPath.startsWith("/wiki/")) throw new Error("ONES: Invalid Wiki Open API path");
1468
1671
  if (!this.options.openApiToken) throw new OnesWikiOpenApiError(null, "ONES Wiki Open API credential is not configured. Set source.openApiAuth.tokenEnv to a Personal API Key with read scopes.", "missing_credential");
@@ -1722,8 +1925,19 @@ var OnesWikiReader = class {
1722
1925
  return score >= MIN_PATH_MATCH_SIMILARITY ? score : null;
1723
1926
  }
1724
1927
  async resolvePath(params) {
1725
- const path = params.path.map((segment) => segment.trim()).filter(Boolean);
1726
- if (!path.length) throw new Error("ONES: Wiki path is empty");
1928
+ const requestedPath = params.path.map((segment) => segment.trim()).filter(Boolean);
1929
+ if (!requestedPath.length) throw new Error("ONES: Wiki path is empty");
1930
+ const currentUserAlias = requestedPath.map(currentUserPathAlias).find((alias) => alias !== null) ?? null;
1931
+ const currentUserName = currentUserAlias ? await this.currentUserName() : null;
1932
+ const path = currentUserName ? requestedPath.map((segment) => currentUserPathAlias(segment) ? currentUserName : segment) : requestedPath;
1933
+ const redactCurrentUser = (page) => {
1934
+ if (!currentUserName || !currentUserAlias) return page;
1935
+ return {
1936
+ ...page,
1937
+ title: page.title === currentUserName ? currentUserAlias : page.title,
1938
+ breadcrumb: page.breadcrumb.map((segment) => segment === currentUserName ? currentUserAlias : segment)
1939
+ };
1940
+ };
1727
1941
  const candidates = await this.search({
1728
1942
  query: path.at(-1),
1729
1943
  teamId: params.teamId,
@@ -1770,26 +1984,32 @@ var OnesWikiReader = class {
1770
1984
  });
1771
1985
  }
1772
1986
  }
1773
- if (matches.length > 1) throw new WikiPathResolutionError("ambiguous", path, matches);
1987
+ if (matches.length > 1) throw new WikiPathResolutionError("ambiguous", requestedPath, matches.map(redactCurrentUser));
1774
1988
  let match = matches[0];
1775
1989
  if (!match && fuzzy.length) {
1776
1990
  fuzzy.sort((left, right) => right.score - left.score);
1777
1991
  const competing = fuzzy.filter((candidate) => fuzzy[0].score - candidate.score < PATH_MATCH_MARGIN);
1778
- if (competing.length > 1) throw new WikiPathResolutionError("ambiguous", path, competing.map((candidate) => candidate.page));
1992
+ if (competing.length > 1) throw new WikiPathResolutionError("ambiguous", requestedPath, competing.map((candidate) => redactCurrentUser(candidate.page)));
1779
1993
  match = fuzzy[0].page;
1780
1994
  }
1781
1995
  if (!match) {
1782
1996
  const inspectedIds = new Set(inspected.map((page) => page.pageId));
1783
- throw new WikiPathResolutionError("not_found", path, [...inspected, ...candidates.filter((page) => !inspectedIds.has(page.pageId))]);
1997
+ throw new WikiPathResolutionError("not_found", requestedPath, [...inspected, ...candidates.filter((page) => !inspectedIds.has(page.pageId))].map(redactCurrentUser));
1784
1998
  }
1785
1999
  if (!match.spaceId) throw new Error("ONES: Wiki space ID could not be verified");
1786
- return {
2000
+ const publicMatch = redactCurrentUser(match);
2001
+ const resolution = {
1787
2002
  teamId: match.teamId,
1788
2003
  spaceId: match.spaceId,
1789
2004
  pageId: match.pageId,
1790
- title: match.title,
1791
- breadcrumb: match.breadcrumb
2005
+ title: publicMatch.title,
2006
+ breadcrumb: publicMatch.breadcrumb
1792
2007
  };
2008
+ if (currentUserName && currentUserAlias) Object.defineProperty(resolution, "redactPrivateValues", {
2009
+ enumerable: false,
2010
+ value: (value) => value.split(currentUserName).join(currentUserAlias)
2011
+ });
2012
+ return resolution;
1793
2013
  }
1794
2014
  };
1795
2015
  //#endregion
@@ -1991,8 +2211,9 @@ var OnesTaskContent = class {
1991
2211
  attachments: rendered.attachments
1992
2212
  };
1993
2213
  })), containsInlineTaskImages(task) ? this.getTaskImageAttachments(task) : Promise.resolve([])]);
2214
+ const projectIdentifier = task.project?.identifier?.toUpperCase() ?? null;
1994
2215
  const parts = [
1995
- `# #${task.number} ${task.name}`,
2216
+ `# ${taskDisplayId({}, task, projectIdentifier)} ${task.name}`,
1996
2217
  "",
1997
2218
  `- **Type**: ${task.issueType?.name ?? "Unknown"}`,
1998
2219
  "- **Work Item Kind**: requirement",
@@ -2004,7 +2225,7 @@ var OnesTaskContent = class {
2004
2225
  parts.push(`- **UUID**: ${task.uuid}`);
2005
2226
  if (task.relatedTasks?.length) {
2006
2227
  parts.push("", "## Related Tasks");
2007
- for (const related of task.relatedTasks) parts.push(`- #${related.number} ${related.name} [${related.issueType?.name}] (${related.status?.name}) — ${related.assign?.name ?? "Unassigned"}`);
2228
+ for (const related of task.relatedTasks) parts.push(`- ${taskDisplayId({}, related, related.project?.identifier?.toUpperCase() ?? projectIdentifier)} ${related.name} [${related.issueType?.name}] (${related.status?.name}) — ${related.assign?.name ?? "Unassigned"}`);
2008
2229
  }
2009
2230
  if (relatedActivities.length) {
2010
2231
  parts.push("", "## Related Work Items");
@@ -2019,7 +2240,7 @@ var OnesTaskContent = class {
2019
2240
  }
2020
2241
  if (task.parent?.uuid) {
2021
2242
  parts.push("", "## Parent Task", `- UUID: ${task.parent.uuid}`);
2022
- if (task.parent.number) parts.push(`- Number: #${task.parent.number}`);
2243
+ if (task.parent.number) parts.push(`- Number: ${projectIdentifier ? `${projectIdentifier}-${task.parent.number}` : `#${task.parent.number}`}`);
2023
2244
  }
2024
2245
  if (wikiContents.length > 0) {
2025
2246
  parts.push("", "---", "", "## Requirement Documents");
@@ -2042,8 +2263,9 @@ var OnesTaskContent = class {
2042
2263
  }
2043
2264
  buildWorkItemSummary(task, kind) {
2044
2265
  const nextTool = kind === "defect" ? "get_issue_detail" : "get_related_issues / get_testcases";
2266
+ const projectIdentifier = task.project?.identifier?.toUpperCase() ?? null;
2045
2267
  const parts = [
2046
- `# #${task.number} ${task.name}`,
2268
+ `# ${taskDisplayId({}, task, projectIdentifier)} ${task.name}`,
2047
2269
  "",
2048
2270
  `- **Type**: ${task.subIssueType?.name ?? task.issueType?.name ?? "Unknown"}`,
2049
2271
  `- **Work Item Kind**: ${kind}`,
@@ -2055,14 +2277,14 @@ var OnesTaskContent = class {
2055
2277
  parts.push(`- **UUID**: ${task.uuid}`);
2056
2278
  if (task.parent?.uuid) {
2057
2279
  parts.push("", "## Parent Task", `- UUID: ${task.parent.uuid}`);
2058
- if (task.parent.number) parts.push(`- Number: #${task.parent.number}`);
2280
+ if (task.parent.number) parts.push(`- Number: ${projectIdentifier ? `${projectIdentifier}-${task.parent.number}` : `#${task.parent.number}`}`);
2059
2281
  }
2060
2282
  const detailText = getTaskDetailText(task);
2061
2283
  if (detailText) parts.push("", "---", "", kind === "defect" ? "## Defect Detail" : "## Task Detail", "", detailText);
2062
2284
  parts.push("", "## Next Tool", "", `This ID is a ${workItemKindLabel(kind)}, not a requirement document.`, `Do not treat wiki/requirement docs as the source of truth. Use \`${nextTool}\` for the next lookup.`);
2063
2285
  if (task.relatedTasks?.length) {
2064
2286
  parts.push("", "## Related Tasks");
2065
- for (const related of task.relatedTasks) parts.push(`- #${related.number} ${related.name} [${related.issueType?.name}] (${related.status?.name}) — ${related.assign?.name ?? "Unassigned"}`);
2287
+ for (const related of task.relatedTasks) parts.push(`- ${taskDisplayId({}, related, related.project?.identifier?.toUpperCase() ?? projectIdentifier)} ${related.name} [${related.issueType?.name}] (${related.status?.name}) — ${related.assign?.name ?? "Unassigned"}`);
2066
2288
  }
2067
2289
  const requirement = toRequirement(task, parts.join("\n"));
2068
2290
  requirement.raw = {
@@ -2124,7 +2346,7 @@ const TASK_DETAIL_QUERY = `
2124
2346
  priority { value }
2125
2347
  assign { uuid name }
2126
2348
  owner { uuid name }
2127
- project { uuid name }
2349
+ project { uuid name identifier }
2128
2350
  parent { uuid number issueType { uuid name } }
2129
2351
  relatedTasks {
2130
2352
  key uuid number name
@@ -2135,6 +2357,7 @@ const TASK_DETAIL_QUERY = `
2135
2357
  subIssueType { uuid name detailType }
2136
2358
  status { uuid name category }
2137
2359
  assign { uuid name }
2360
+ project { uuid name identifier }
2138
2361
  }
2139
2362
  relatedWikiPages {
2140
2363
  uuid title referenceType subReferenceType errorMessage
@@ -2271,7 +2494,9 @@ var OnesTaskPlanning = class {
2271
2494
  if (!Number.isInteger(raw.number)) throw new TypeError("ONES: Standalone wiki pages cannot be decomposed into requirement tasks");
2272
2495
  const parsedDisplayId = parseDisplayId(params.requirementId);
2273
2496
  const requirementInfo = await this.options.fetchTaskInfo(workItem.id);
2274
- const projectIdentifier = parsedDisplayId?.identifier.toUpperCase() ?? firstString(requirementInfo, ["projectIdentifier", "project_identifier"]);
2497
+ let projectIdentifier = parsedDisplayId?.identifier ?? firstString(requirementInfo, ["projectIdentifier", "project_identifier"]) ?? raw.project?.identifier ?? null;
2498
+ if (!projectIdentifier && raw.project?.uuid) projectIdentifier = await this.options.resolveProjectIdentifier(raw.project.uuid);
2499
+ projectIdentifier = projectIdentifier?.toUpperCase() ?? null;
2275
2500
  const displayId = firstString(requirementInfo, ["displayId", "display_id"]) ?? (projectIdentifier ? `${projectIdentifier}-${raw.number}` : `#${raw.number}`);
2276
2501
  const relatedTasks = (raw.relatedTasks ?? []).filter((task) => classifyOnesWorkItem(task.issueType, task.subIssueType) === "task");
2277
2502
  const relatedInfos = await Promise.all(relatedTasks.map((task) => this.options.fetchTaskInfo(task.uuid)));
@@ -2600,6 +2825,12 @@ var OnesTestcaseReader = class {
2600
2825
  const JSON0_URI = "http://sharejs.org/types/JSONv0";
2601
2826
  const JSON1_URI = "http://sharejs.org/types/JSONv1";
2602
2827
  const DEFAULT_TIMEOUT_MS$1 = 15e3;
2828
+ function getSetCookies(response) {
2829
+ const headers = response.headers;
2830
+ if (headers.getSetCookie) return headers.getSetCookie();
2831
+ const raw = headers.get("set-cookie");
2832
+ return raw ? [raw] : [];
2833
+ }
2603
2834
  function isRecord(value) {
2604
2835
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
2605
2836
  }
@@ -2612,28 +2843,44 @@ function asDoc(value) {
2612
2843
  }
2613
2844
  function createTopLevelJson1Operation(current, next) {
2614
2845
  let operation = null;
2846
+ const appendComponent = (component) => {
2847
+ operation = operation === null ? component : ot_json1.type.compose(operation, component);
2848
+ };
2615
2849
  const keys = [.../* @__PURE__ */ new Set([...Object.keys(current), ...Object.keys(next)])].sort();
2616
2850
  for (const key of keys) {
2851
+ if (key === "blocks") continue;
2617
2852
  const hasCurrent = Object.hasOwn(current, key);
2618
2853
  const hasNext = Object.hasOwn(next, key);
2619
- let component;
2620
- if (!hasCurrent) component = (0, ot_json1.insertOp)([key], asDoc(next[key]));
2621
- else if (!hasNext) component = (0, ot_json1.removeOp)([key]);
2622
- else if (!(0, node_util.isDeepStrictEqual)(current[key], next[key])) component = (0, ot_json1.replaceOp)([key], asDoc(current[key]), asDoc(next[key]));
2623
- else continue;
2624
- operation = operation === null ? component : ot_json1.type.compose(operation, component);
2625
- }
2854
+ if (!hasCurrent && hasNext) appendComponent((0, ot_json1.insertOp)([key], asDoc(next[key])));
2855
+ else if (hasCurrent && !hasNext) appendComponent((0, ot_json1.removeOp)([key]));
2856
+ else if (hasCurrent && hasNext && !(0, node_util.isDeepStrictEqual)(current[key], next[key])) appendComponent((0, ot_json1.replaceOp)([key], asDoc(current[key]), asDoc(next[key])));
2857
+ }
2858
+ const currentBlocks = Array.isArray(current.blocks) ? current.blocks : [];
2859
+ const nextBlocks = Array.isArray(next.blocks) ? next.blocks : [];
2860
+ const sharedBlockCount = Math.min(currentBlocks.length, nextBlocks.length);
2861
+ for (let index = 0; index < sharedBlockCount; index += 1) if (!(0, node_util.isDeepStrictEqual)(currentBlocks[index], nextBlocks[index])) {
2862
+ appendComponent((0, ot_json1.removeOp)(["blocks", index]));
2863
+ appendComponent((0, ot_json1.insertOp)(["blocks", index], asDoc(nextBlocks[index])));
2864
+ }
2865
+ for (let index = currentBlocks.length - 1; index >= nextBlocks.length; index -= 1) appendComponent((0, ot_json1.removeOp)(["blocks", index]));
2866
+ for (let index = currentBlocks.length; index < nextBlocks.length; index += 1) appendComponent((0, ot_json1.insertOp)(["blocks", index], asDoc(nextBlocks[index])));
2626
2867
  if (operation !== null) ot_json1.type.checkValidOp(operation);
2627
2868
  return operation;
2628
2869
  }
2870
+ function createTopLevelJson1Operations(current, next) {
2871
+ const operation = createTopLevelJson1Operation(current, next);
2872
+ return operation === null ? [] : [operation];
2873
+ }
2629
2874
  function wikiEditorUrls(baseUrl, teamId, documentId) {
2630
2875
  const url = new URL(baseUrl);
2631
2876
  if (url.protocol !== "https:" && url.protocol !== "http:") throw new Error("ONES Wiki collaboration requires an HTTP(S) source URL");
2632
2877
  const path = `/wiki/api/wiki/editor/${encodeURIComponent(teamId)}/${encodeURIComponent(documentId)}`;
2633
- const socketUrl = new URL(path, url);
2878
+ const editorBaseUrl = new URL(path, url);
2879
+ const socketUrl = new URL(editorBaseUrl);
2634
2880
  socketUrl.protocol = url.protocol === "https:" ? "wss:" : "ws:";
2635
2881
  return {
2636
- authUrl: new URL(`${path}/auth`, url).toString(),
2882
+ authUrl: `${editorBaseUrl.toString()}/auth`,
2883
+ editorBaseUrl: editorBaseUrl.toString(),
2637
2884
  socketUrl: socketUrl.toString()
2638
2885
  };
2639
2886
  }
@@ -2650,39 +2897,52 @@ function closeSocket(socket) {
2650
2897
  }
2651
2898
  async function replaceOnesWikiDocument(options, update, dependencies = {
2652
2899
  fetch,
2653
- openWebSocket: (url) => new ws.default(url)
2900
+ openWebSocket: (url, headers) => new ws.default(url, { headers })
2654
2901
  }) {
2655
- const { authUrl, socketUrl } = wikiEditorUrls(options.baseUrl, options.teamId, options.documentId);
2902
+ const { authUrl, editorBaseUrl, socketUrl } = wikiEditorUrls(options.baseUrl, options.teamId, options.documentId);
2656
2903
  const authResponse = await dependencies.fetch(authUrl, {
2657
2904
  method: "GET",
2658
2905
  headers: {
2659
2906
  "Authorization": `Bearer ${options.accessToken}`,
2660
2907
  "x-live-editor-token": options.editorToken,
2661
- "x-live-editor-base-url": Buffer.from(socketUrl).toString("base64url")
2908
+ "x-live-editor-base-url": Buffer.from(editorBaseUrl).toString("base64url"),
2909
+ ...options.cookieHeader ? { Cookie: options.cookieHeader } : {}
2662
2910
  }
2663
2911
  });
2664
2912
  if (!authResponse.ok) throw new Error(`ONES Wiki collaboration auth failed with status ${authResponse.status}`);
2665
2913
  const editorAuth = await authResponse.json();
2666
2914
  if (typeof editorAuth.read !== "string" || !editorAuth.read) throw new Error("ONES Wiki collaboration auth response did not include a read token");
2915
+ const cookies = [options.cookieHeader, ...getSetCookies(authResponse).map((cookie) => cookie.split(";")[0])].filter((cookie) => Boolean(cookie)).join("; ");
2916
+ const socketHeaders = {
2917
+ "Accept-Language": "zh-CN,zh;q=0.9",
2918
+ "Cache-Control": "no-cache",
2919
+ "Pragma": "no-cache",
2920
+ "User-Agent": "Mozilla/5.0 AppleWebKit/537.36 Chrome/128.0.0.0 Safari/537.36",
2921
+ "Origin": new URL(options.baseUrl).origin,
2922
+ ...cookies ? { Cookie: cookies } : {}
2923
+ };
2667
2924
  return new Promise((resolve, reject) => {
2668
- const socket = dependencies.openWebSocket(socketUrl);
2669
- const sequence = 1;
2670
- let clientId = "";
2925
+ const socket = dependencies.openWebSocket(socketUrl, socketHeaders);
2926
+ let sequence = 1;
2671
2927
  let state = "init";
2672
2928
  let settled = false;
2673
2929
  let snapshotVersion = 0;
2930
+ let currentVersion = 0;
2931
+ let pendingOperations = [];
2932
+ const presenceChannel = `${options.teamId}:${options.documentId}`;
2933
+ const presenceId = (0, node_crypto.randomBytes)(7).toString("base64url").slice(0, 9);
2674
2934
  let timeout;
2675
2935
  const finish = (result) => {
2676
2936
  if (settled) return;
2677
2937
  settled = true;
2678
- if (timeout) clearTimeout(timeout);
2938
+ clearTimeout(timeout);
2679
2939
  resolve(result);
2680
2940
  closeSocket(socket);
2681
2941
  };
2682
2942
  const fail = (error) => {
2683
2943
  if (settled) return;
2684
2944
  settled = true;
2685
- if (timeout) clearTimeout(timeout);
2945
+ clearTimeout(timeout);
2686
2946
  reject(error);
2687
2947
  closeSocket(socket);
2688
2948
  };
@@ -2691,9 +2951,38 @@ async function replaceOnesWikiDocument(options, update, dependencies = {
2691
2951
  if (error) fail(/* @__PURE__ */ new Error(`ONES Wiki collaboration send failed: ${error.message}`));
2692
2952
  });
2693
2953
  };
2954
+ const sendHandshake = () => {
2955
+ send({
2956
+ a: "hs",
2957
+ id: null,
2958
+ auth: {
2959
+ appId: options.teamId,
2960
+ docId: options.documentId,
2961
+ userId: options.userId,
2962
+ permission: "w",
2963
+ token: options.editorToken,
2964
+ displayName: options.displayName ?? "",
2965
+ avatarUrl: options.avatarUrl ?? ""
2966
+ }
2967
+ });
2968
+ };
2969
+ const sendNextOperation = () => {
2970
+ const operation = pendingOperations[0];
2971
+ if (operation === void 0) return;
2972
+ send({
2973
+ a: "op",
2974
+ c: options.teamId,
2975
+ d: options.documentId,
2976
+ v: currentVersion,
2977
+ seq: sequence,
2978
+ x: {},
2979
+ op: operation
2980
+ });
2981
+ };
2694
2982
  timeout = setTimeout(() => {
2695
2983
  fail(/* @__PURE__ */ new Error(`ONES Wiki collaboration timed out while waiting for ${state}`));
2696
2984
  }, options.timeoutMs ?? DEFAULT_TIMEOUT_MS$1);
2985
+ socket.on("open", sendHandshake);
2697
2986
  socket.on("error", () => fail(/* @__PURE__ */ new Error("ONES Wiki collaboration WebSocket failed")));
2698
2987
  socket.on("close", (code) => {
2699
2988
  if (!settled) fail(/* @__PURE__ */ new Error(`ONES Wiki collaboration WebSocket closed before ${state} (code ${code})`));
@@ -2720,58 +3009,64 @@ async function replaceOnesWikiDocument(options, update, dependencies = {
2720
3009
  fail(/* @__PURE__ */ new Error("ONES Wiki collaboration returned an unsupported init frame"));
2721
3010
  return;
2722
3011
  }
2723
- clientId = message.id;
2724
3012
  state = "handshake";
2725
- send({
2726
- a: "hs",
2727
- id: clientId,
2728
- auth: {
2729
- appId: options.teamId,
2730
- docId: options.documentId,
2731
- userId: options.userId,
2732
- permission: "w",
2733
- token: editorAuth.read,
2734
- displayName: options.displayName ?? "",
2735
- avatarUrl: options.avatarUrl ?? ""
2736
- },
2737
- options: { ping: {
2738
- interval: 5e4,
2739
- timeout: 15e4
2740
- } }
2741
- });
3013
+ sendHandshake();
2742
3014
  return;
2743
3015
  }
2744
3016
  if (state === "handshake") {
2745
- if (message.a !== "hs" || message.id !== clientId || message.protocol !== 1 || message.protocolMinor !== 1 || message.type !== JSON0_URI) {
3017
+ if (message.a !== "hs" || typeof message.id !== "string" || message.protocol !== 1 || message.protocolMinor !== 1 || message.type !== JSON0_URI) {
2746
3018
  fail(/* @__PURE__ */ new Error("ONES Wiki collaboration returned an unsupported handshake frame"));
2747
3019
  return;
2748
3020
  }
2749
- state = "snapshot";
3021
+ state = "fetch";
2750
3022
  send({
2751
- a: "s",
3023
+ a: "f",
2752
3024
  c: options.teamId,
2753
3025
  d: options.documentId
2754
3026
  });
2755
3027
  return;
2756
3028
  }
2757
- if (state === "snapshot") {
3029
+ if (state === "fetch") {
2758
3030
  const snapshot = message.data;
2759
- if (message.a !== "s" || message.c !== options.teamId || message.d !== options.documentId || typeof snapshot?.v !== "number" || snapshot.type !== JSON1_URI) {
2760
- fail(/* @__PURE__ */ new Error("ONES Wiki collaboration returned an unsupported snapshot frame"));
2761
- return;
2762
- }
3031
+ if (message.a !== "f" || message.c !== options.teamId || message.d !== options.documentId || typeof snapshot?.v !== "number" || snapshot.type !== JSON1_URI) return;
2763
3032
  const current = asWikiSnapshot(snapshot.data);
2764
- let next;
2765
- let operation;
2766
3033
  try {
2767
- next = asJsonDocument(update(structuredClone(current)), "update");
2768
- operation = createTopLevelJson1Operation(current, next);
3034
+ pendingOperations = createTopLevelJson1Operations(current, asJsonDocument(update(structuredClone(current)), "update"));
2769
3035
  } catch (error) {
2770
3036
  fail(error instanceof Error ? error : /* @__PURE__ */ new Error("ONES Wiki collaboration update failed"));
2771
3037
  return;
2772
3038
  }
2773
3039
  snapshotVersion = snapshot.v;
2774
- if (operation === null) {
3040
+ currentVersion = snapshotVersion;
3041
+ state = "presence";
3042
+ send({
3043
+ a: "p",
3044
+ ch: presenceChannel,
3045
+ id: presenceId,
3046
+ p: null,
3047
+ pv: 2
3048
+ });
3049
+ send({
3050
+ a: "ps",
3051
+ ch: presenceChannel,
3052
+ seq: 1
3053
+ });
3054
+ return;
3055
+ }
3056
+ if (state === "presence") {
3057
+ if (message.a !== "ps" || message.ch !== presenceChannel || message.seq !== 1) return;
3058
+ state = "subscribe";
3059
+ send({
3060
+ a: "s",
3061
+ c: options.teamId,
3062
+ d: options.documentId,
3063
+ v: snapshotVersion
3064
+ });
3065
+ return;
3066
+ }
3067
+ if (state === "subscribe") {
3068
+ if (message.a !== "s" || message.c !== options.teamId || message.d !== options.documentId) return;
3069
+ if (!pendingOperations.length) {
2775
3070
  finish({
2776
3071
  snapshotVersion,
2777
3072
  version: snapshotVersion,
@@ -2780,21 +3075,23 @@ async function replaceOnesWikiDocument(options, update, dependencies = {
2780
3075
  return;
2781
3076
  }
2782
3077
  state = "ack";
2783
- send({
2784
- a: "op",
2785
- c: options.teamId,
2786
- d: options.documentId,
2787
- v: snapshotVersion,
2788
- seq: sequence,
2789
- op: operation
2790
- });
3078
+ sendNextOperation();
2791
3079
  return;
2792
3080
  }
2793
- if (state === "ack" && message.a === "op" && message.c === options.teamId && message.d === options.documentId && message.seq === sequence) finish({
2794
- snapshotVersion,
2795
- version: typeof message.v === "number" ? message.v : snapshotVersion + 1,
2796
- changed: true
2797
- });
3081
+ if (state === "ack" && message.a === "op" && message.c === options.teamId && message.d === options.documentId && message.seq === sequence) {
3082
+ currentVersion = typeof message.v === "number" ? message.v : currentVersion + 1;
3083
+ pendingOperations.shift();
3084
+ if (!pendingOperations.length) {
3085
+ finish({
3086
+ snapshotVersion,
3087
+ version: currentVersion,
3088
+ changed: true
3089
+ });
3090
+ return;
3091
+ }
3092
+ sequence += 1;
3093
+ sendNextOperation();
3094
+ }
2798
3095
  });
2799
3096
  });
2800
3097
  }
@@ -2832,6 +3129,10 @@ var OnesWikiProductWriter = class {
2832
3129
  const encodedTeamId = encodeIdentifier(teamId, "team UUID");
2833
3130
  const headers = new Headers(init.headers);
2834
3131
  headers.set("Authorization", `Bearer ${session.accessToken}`);
3132
+ headers.set("Ones-Auth-Token", session.legacyAuthToken);
3133
+ headers.set("Ones-User-Id", session.legacyUserId);
3134
+ headers.set("Referer", this.options.apiBase);
3135
+ if (session.cookieHeader) headers.set("Cookie", session.cookieHeader);
2835
3136
  if (typeof init.body === "string" && !headers.has("Content-Type")) headers.set("Content-Type", "application/json;charset=UTF-8");
2836
3137
  const response = await fetch(`${this.options.apiBase}/wiki/api/wiki/team/${encodedTeamId}${apiPath}`, {
2837
3138
  ...init,
@@ -2854,9 +3155,9 @@ var OnesWikiProductWriter = class {
2854
3155
  }
2855
3156
  throw new Error("ONES Wiki editor token endpoint was not found");
2856
3157
  }
2857
- async replaceDocument(teamId, resourceId, documentId, update, preferDraft = false) {
3158
+ async replaceDocument(teamId, resourceId, documentId, update, preferDraft = false, editorTokenOverride) {
2858
3159
  const session = await this.options.getSession();
2859
- const editorToken = await this.fetchEditorToken(teamId, resourceId, preferDraft);
3160
+ const editorToken = editorTokenOverride ?? await this.fetchEditorToken(teamId, resourceId, preferDraft);
2860
3161
  return { version: (await replaceOnesWikiDocument({
2861
3162
  baseUrl: this.options.apiBase,
2862
3163
  teamId,
@@ -2864,6 +3165,7 @@ var OnesWikiProductWriter = class {
2864
3165
  accessToken: session.accessToken,
2865
3166
  editorToken,
2866
3167
  userId: session.userUuid,
3168
+ cookieHeader: session.cookieHeader,
2867
3169
  displayName: session.userName
2868
3170
  }, update)).version };
2869
3171
  }
@@ -2917,20 +3219,120 @@ var OnesWikiProductWriter = class {
2917
3219
  };
2918
3220
  }
2919
3221
  async update(params) {
2920
- const detail = await this.options.fetchPageDetail(params.pageId, params.teamId);
2921
- const documentId = firstNonEmptyString(detail.ref_uuid);
2922
- if (!documentId) throw new Error("ONES Wiki page detail did not include a collaborative document ID");
2923
- const write = await this.replaceDocument(params.teamId, params.pageId, documentId, (snapshot) => {
3222
+ const encodedPageId = encodeIdentifier(params.pageId, "Wiki page UUID");
3223
+ const encodedSpaceId = params.spaceId ? encodeIdentifier(params.spaceId, "Wiki space UUID") : null;
3224
+ let detail = await this.request(params.teamId, `/page/${encodedPageId}?action=edit`);
3225
+ const title = firstNonEmptyString(detail.title, detail.name, detail.page_title) ?? `Wiki ${params.pageId}`;
3226
+ if (detail.ref_type === 6 && detail.ref_uuid) {
3227
+ const editorToken = await this.fetchEditorToken(params.teamId, params.pageId);
3228
+ const write = await this.replaceDocument(params.teamId, params.pageId, detail.ref_uuid, (snapshot) => {
3229
+ return parseWikiDocument(applyWikiUpdateOperation(JSON.stringify(snapshot), params.operation));
3230
+ }, false, editorToken);
3231
+ const published = await this.request(params.teamId, `/online_page/${encodedPageId}/publish`, {
3232
+ method: "POST",
3233
+ body: JSON.stringify({ title })
3234
+ });
3235
+ if (params.spaceId) this.options.invalidateTree(params.teamId, params.spaceId);
3236
+ return {
3237
+ pageId: params.pageId,
3238
+ title,
3239
+ version: String(published.version ?? published.updated_time ?? write.version),
3240
+ url: params.spaceId ? `${this.options.apiBase}/wiki/#/team/${encodeURIComponent(params.teamId)}/space/${encodeURIComponent(params.spaceId)}/page/${encodeURIComponent(params.pageId)}` : null
3241
+ };
3242
+ }
3243
+ let draftId = firstNonEmptyString(detail.online_draft_uuid, detail.onlineDraftUuid, detail.online_draft?.uuid, detail.draft_uuid, detail.draftUuid, detail.draft?.uuid);
3244
+ let documentId = firstNonEmptyString(detail.online_draft_ref_uuid, detail.onlineDraftRefUuid, detail.online_draft?.ref_uuid, detail.draft_ref_uuid, detail.draftRefUuid, detail.draft?.ref_uuid, detail.ref_uuid);
3245
+ if (!draftId) {
3246
+ if (!encodedSpaceId) throw new Error("ONES Wiki page space is required to create an edit draft");
3247
+ try {
3248
+ const createdDraft = await this.request(params.teamId, `/space/${encodedSpaceId}/drafts/add`, {
3249
+ method: "POST",
3250
+ body: JSON.stringify({
3251
+ page_uuid: params.pageId,
3252
+ status: 2,
3253
+ title
3254
+ })
3255
+ });
3256
+ draftId = firstNonEmptyString(createdDraft.draft_uuid, createdDraft.draftUuid, createdDraft.uuid, createdDraft.id);
3257
+ documentId = firstNonEmptyString(createdDraft.ref_uuid, createdDraft.refUuid, documentId);
3258
+ } catch (error) {
3259
+ const refreshedDetail = await this.request(params.teamId, `/space/${encodedSpaceId}/page/${encodedPageId}`);
3260
+ draftId = firstNonEmptyString(refreshedDetail.draft_uuid, refreshedDetail.draftUuid);
3261
+ if (!draftId) throw error;
3262
+ detail = refreshedDetail;
3263
+ documentId = firstNonEmptyString(refreshedDetail.draft_ref_uuid, refreshedDetail.draftRefUuid, refreshedDetail.ref_uuid, documentId);
3264
+ }
3265
+ }
3266
+ if (!draftId || !encodedSpaceId) throw new Error("ONES Wiki edit draft did not include the required resource and space IDs");
3267
+ const encodedDraftId = encodeIdentifier(draftId, "Wiki draft UUID");
3268
+ const draftDetail = await this.request(params.teamId, `/space/${encodedSpaceId}/draft/${encodedDraftId}`);
3269
+ documentId = firstNonEmptyString(draftDetail.ref_uuid, draftDetail.refUuid, documentId);
3270
+ if (!documentId) throw new Error("ONES Wiki edit draft did not include a collaborative document ID");
3271
+ if (typeof draftDetail.content === "string") {
3272
+ let content;
3273
+ if (params.operation.type === "replace_document") content = markdownToWikiHtml(params.operation.markdown);
3274
+ else if (params.operation.type === "append_blocks") content = `${draftDetail.content}\n${markdownToWikiHtml(params.operation.markdown)}`;
3275
+ else if (params.operation.type === "replace_text") {
3276
+ const occurrences = draftDetail.content.split(params.operation.find).length - 1;
3277
+ if (occurrences !== 1) throw new Error(`ONES Wiki draft replace_text requires exactly one match; found ${occurrences}`);
3278
+ content = draftDetail.content.replace(params.operation.find, params.operation.replace);
3279
+ } else throw new Error("ONES Wiki append_table_row is not supported for legacy page drafts");
3280
+ const published = await this.request(params.teamId, `/space/${encodedSpaceId}/draft/${encodedDraftId}/update`, {
3281
+ method: "POST",
3282
+ body: JSON.stringify({
3283
+ ...draftDetail,
3284
+ content,
3285
+ title,
3286
+ page_uuid: params.pageId,
3287
+ space_uuid: params.spaceId,
3288
+ from_version: detail.version ?? draftDetail.from_version,
3289
+ is_published: true,
3290
+ is_forced: true
3291
+ })
3292
+ });
3293
+ this.options.invalidateTree(params.teamId, params.spaceId);
3294
+ const publishedVersion = published.version ?? published.updated_time ?? draftDetail.version ?? draftDetail.updated_time;
3295
+ return {
3296
+ pageId: params.pageId,
3297
+ title,
3298
+ version: publishedVersion === void 0 ? null : String(publishedVersion),
3299
+ url: `${this.options.apiBase}/wiki/#/team/${encodeURIComponent(params.teamId)}/space/${encodeURIComponent(params.spaceId)}/page/${encodeURIComponent(params.pageId)}`
3300
+ };
3301
+ }
3302
+ const write = await this.replaceDocument(params.teamId, draftId, documentId, (snapshot) => {
2924
3303
  return parseWikiDocument(applyWikiUpdateOperation(JSON.stringify(snapshot), params.operation));
3304
+ }, true);
3305
+ const refreshedDraft = await this.request(params.teamId, `/space/${encodedSpaceId}/draft/${encodedDraftId}`);
3306
+ await this.request(params.teamId, `/space/${encodedSpaceId}/draft/${encodedDraftId}/update`, {
3307
+ method: "POST",
3308
+ body: JSON.stringify({
3309
+ ...refreshedDraft,
3310
+ title,
3311
+ page_uuid: params.pageId,
3312
+ space_uuid: params.spaceId,
3313
+ from_version: detail.version ?? refreshedDraft.from_version,
3314
+ is_published: true,
3315
+ is_forced: true
3316
+ })
2925
3317
  });
2926
3318
  if (params.spaceId) this.options.invalidateTree(params.teamId, params.spaceId);
2927
3319
  return {
2928
3320
  pageId: params.pageId,
2929
- title: firstNonEmptyString(detail.title, detail.name, detail.page_title) ?? `Wiki ${params.pageId}`,
3321
+ title,
2930
3322
  version: String(write.version),
2931
3323
  url: params.spaceId ? `${this.options.apiBase}/wiki/#/team/${encodeURIComponent(params.teamId)}/space/${encodeURIComponent(params.spaceId)}/page/${encodeURIComponent(params.pageId)}` : null
2932
3324
  };
2933
3325
  }
3326
+ async delete(params) {
3327
+ const encodedSpaceId = encodeIdentifier(params.spaceId, "Wiki space UUID");
3328
+ const encodedPageId = encodeIdentifier(params.pageId, "Wiki page UUID");
3329
+ await this.request(params.teamId, `/space/${encodedSpaceId}/page/${encodedPageId}/delete`, { method: "POST" });
3330
+ this.options.invalidateTree(params.teamId, params.spaceId);
3331
+ return {
3332
+ pageId: params.pageId,
3333
+ deleted: true
3334
+ };
3335
+ }
2934
3336
  };
2935
3337
  //#endregion
2936
3338
  //#region ../../src/adapters/ones/task-query.ts
@@ -3017,7 +3419,6 @@ var OnesTaskAdapter = class extends BaseAdapter {
3017
3419
  this.wikiProductWriter = new OnesWikiProductWriter({
3018
3420
  apiBase: config.apiBase,
3019
3421
  getSession: () => this.login(),
3020
- fetchPageDetail: (pageId, teamId) => this.wikiReader.fetchPageDetail(pageId, teamId, true),
3021
3422
  invalidateTree: (teamId, spaceId) => this.wikiReader.invalidateTree(teamId, spaceId)
3022
3423
  });
3023
3424
  this.content = new OnesTaskContent({
@@ -3033,7 +3434,10 @@ var OnesTaskAdapter = class extends BaseAdapter {
3033
3434
  this.planning = new OnesTaskPlanning({
3034
3435
  api: this.api,
3035
3436
  getRequirement: (id) => this.getRequirement({ id }),
3036
- fetchTaskInfo: (taskUuid) => this.fetchTaskInfo(taskUuid)
3437
+ fetchTaskInfo: (taskUuid) => this.fetchTaskInfo(taskUuid),
3438
+ resolveProjectIdentifier: async (projectUuid) => {
3439
+ return (await this.fetchProjects()).find((candidate) => candidate.uuid === projectUuid)?.identifier?.toUpperCase() ?? null;
3440
+ }
3037
3441
  });
3038
3442
  this.issueReader = new OnesIssueReader({
3039
3443
  api: this.api,
@@ -3251,6 +3655,9 @@ var OnesTaskAdapter = class extends BaseAdapter {
3251
3655
  async updateWikiPage(params) {
3252
3656
  return this.wikiProductWriter.update(params);
3253
3657
  }
3658
+ async deleteWikiPage(params) {
3659
+ return this.wikiProductWriter.delete(params);
3660
+ }
3254
3661
  /**
3255
3662
  * Fetch a work item by UUID, number, display id, or wiki URL.
3256
3663
  * Routes by issueType.detailType: requirements (1 and 5) load wiki docs;
@@ -3993,7 +4400,7 @@ function formatWorkItem(req) {
3993
4400
  //#endregion
3994
4401
  //#region ../../src/tools/list-pending-work-items.ts
3995
4402
  const ListPendingWorkItemsSchema = zod_v4.z.object({ source: zod_v4.z.string().optional().describe("Source to read. If omitted, uses the default source.") });
3996
- function resolveAdapter$3(source, adapters, defaultSource) {
4403
+ function resolveAdapter$4(source, adapters, defaultSource) {
3997
4404
  const sourceType = source ?? defaultSource;
3998
4405
  if (!sourceType) throw new Error("No source specified and no default source configured");
3999
4406
  const adapter = adapters.get(sourceType);
@@ -4037,7 +4444,7 @@ function formatResult(result) {
4037
4444
  return lines.join("\n");
4038
4445
  }
4039
4446
  async function handleListPendingWorkItems(input, adapters, defaultSource) {
4040
- const result = await resolveAdapter$3(input.source, adapters, defaultSource).listPendingWorkItems();
4447
+ const result = await resolveAdapter$4(input.source, adapters, defaultSource).listPendingWorkItems();
4041
4448
  const safeResult = {
4042
4449
  ...result,
4043
4450
  items: result.items.map(sanitizeItem)
@@ -4151,7 +4558,7 @@ var RequirementDecompositionApprovalStore = class {
4151
4558
  return record;
4152
4559
  }
4153
4560
  };
4154
- function resolveAdapter$2(source, adapters, defaultSource) {
4561
+ function resolveAdapter$3(source, adapters, defaultSource) {
4155
4562
  const sourceType = source ?? defaultSource;
4156
4563
  if (!sourceType) throw new Error("No source specified and no default source configured");
4157
4564
  const adapter = adapters.get(sourceType);
@@ -4254,7 +4661,7 @@ function buildOperations(displayId, tasks) {
4254
4661
  });
4255
4662
  }
4256
4663
  async function handleInspectRequirementDecomposition(input, adapters, defaultSource) {
4257
- const { adapter } = resolveAdapter$2(input.source, adapters, defaultSource);
4664
+ const { adapter } = resolveAdapter$3(input.source, adapters, defaultSource);
4258
4665
  const context = sanitizedContext(await adapter.getRequirementDecompositionContext({ requirementId: input.requirementId }));
4259
4666
  return {
4260
4667
  content: [{
@@ -4265,7 +4672,7 @@ async function handleInspectRequirementDecomposition(input, adapters, defaultSou
4265
4672
  };
4266
4673
  }
4267
4674
  async function handlePrepareRequirementDecomposition(input, adapters, approvals, defaultSource) {
4268
- const { sourceType, adapter } = resolveAdapter$2(input.source, adapters, defaultSource);
4675
+ const { sourceType, adapter } = resolveAdapter$3(input.source, adapters, defaultSource);
4269
4676
  const context = await adapter.getRequirementDecompositionContext({ requirementId: input.requirementId });
4270
4677
  if (context.requirement.workItemKind !== "requirement") throw new Error("Only requirements can be decomposed");
4271
4678
  if (!context.decompositionRelation.verified || !context.decompositionRelation.uuid) throw new Error("The \"requirement decomposition task\" relationship could not be verified from the read response. No plan or write was prepared.");
@@ -4315,7 +4722,7 @@ async function handleApplyRequirementDecomposition(input, adapters, approvals, o
4315
4722
  if (!record) throw new Error("Approval token is invalid, expired, or already used. Prepare the decomposition again.");
4316
4723
  if ((input.source ?? options.defaultSource) !== record.source) throw new Error("Approval token source does not match the requested source");
4317
4724
  if (input.planHash !== record.planHash) throw new Error("Plan hash does not match the approved decomposition");
4318
- const { adapter } = resolveAdapter$2(record.source, adapters, options.defaultSource);
4725
+ const { adapter } = resolveAdapter$3(record.source, adapters, options.defaultSource);
4319
4726
  const current = await adapter.getRequirementDecompositionContext({ requirementId: record.requirementId });
4320
4727
  if (current.requirement.uuid !== record.requirementUuid || !isSameRequirementBaseline(current.baseline, record.baseline)) throw new Error("Requirement or related tasks changed after preparation. Prepare and confirm a new decomposition.");
4321
4728
  if (!current.decompositionRelation.verified || current.decompositionRelation.uuid !== record.decompositionRelation.uuid) throw new Error("The requirement decomposition relationship changed or is no longer verified. Prepare and confirm again.");
@@ -4421,12 +4828,82 @@ function formatUpdateTaskPlanDatesResult(result) {
4421
4828
  return lines.join("\n");
4422
4829
  }
4423
4830
  //#endregion
4831
+ //#region ../../src/tools/wiki-delete.ts
4832
+ const SourceSchema$1 = zod_v4.z.string().trim().min(1).optional();
4833
+ const DeleteEmptyWikiDuplicatesSchema = zod_v4.z.object({
4834
+ keepPageId: zod_v4.z.string().trim().min(1),
4835
+ duplicatePageIds: zod_v4.z.array(zod_v4.z.string().trim().min(1)).min(1).max(20),
4836
+ expectedTitle: zod_v4.z.string().trim().min(1),
4837
+ confirmed: zod_v4.z.literal(true).describe("Set true only after the user confirms the exact keep and delete page IDs immediately before submission."),
4838
+ source: SourceSchema$1
4839
+ });
4840
+ const DeleteEmptyWikiDuplicatesOutputSchema = zod_v4.z.object({
4841
+ keptPageId: zod_v4.z.string(),
4842
+ deletedPageIds: zod_v4.z.array(zod_v4.z.string())
4843
+ });
4844
+ function resolveAdapter$2(source, adapters, defaultSource) {
4845
+ const sourceType = source ?? defaultSource;
4846
+ if (!sourceType) throw new Error("No source specified and no default source configured");
4847
+ const adapter = adapters.get(sourceType);
4848
+ if (!adapter) throw new Error(`Source "${sourceType}" is not configured`);
4849
+ return {
4850
+ sourceType,
4851
+ adapter
4852
+ };
4853
+ }
4854
+ function assertCleanupTarget(keep, duplicates, expectedTitle) {
4855
+ if (keep.title !== expectedTitle) throw new Error("The retained Wiki page title no longer matches the confirmed title");
4856
+ if (keep.content.trim() === `# ${keep.title}`) throw new Error("The retained Wiki page does not contain a body");
4857
+ for (const duplicate of duplicates) {
4858
+ if (duplicate.title !== expectedTitle) throw new Error(`Wiki duplicate ${duplicate.pageId} title no longer matches the confirmed title`);
4859
+ if (duplicate.teamId !== keep.teamId || duplicate.spaceId !== keep.spaceId || duplicate.parentPageId !== keep.parentPageId) throw new Error(`Wiki duplicate ${duplicate.pageId} is not a sibling of the retained page`);
4860
+ if (duplicate.content.trim() !== `# ${duplicate.title}`) throw new Error(`Wiki duplicate ${duplicate.pageId} is not empty and will not be deleted`);
4861
+ }
4862
+ }
4863
+ async function handleDeleteEmptyWikiDuplicates(input, adapters, options) {
4864
+ if (!options.writesEnabled) throw new Error("Wiki writes are disabled. Enable both ONES_WIKI_ENABLE_WRITES=true and source option wikiWrites=true.");
4865
+ const uniqueDuplicateIds = [...new Set(input.duplicatePageIds)];
4866
+ if (uniqueDuplicateIds.length !== input.duplicatePageIds.length) throw new Error("duplicatePageIds contains repeated page IDs");
4867
+ if (uniqueDuplicateIds.includes(input.keepPageId)) throw new Error("The retained Wiki page cannot also be deleted");
4868
+ const { adapter } = resolveAdapter$2(input.source, adapters, options.defaultSource);
4869
+ const [keep, ...duplicates] = await Promise.all([adapter.getWikiPage({ pageId: input.keepPageId }), ...uniqueDuplicateIds.map((pageId) => adapter.getWikiPage({ pageId }))]);
4870
+ assertCleanupTarget(keep, duplicates, input.expectedTitle);
4871
+ const spaceId = keep.spaceId;
4872
+ if (!spaceId) throw new Error("The retained Wiki page space could not be verified");
4873
+ const operationHash = node_crypto.default.createHash("sha256").update(JSON.stringify({
4874
+ keepPageId: input.keepPageId,
4875
+ duplicatePageIds: uniqueDuplicateIds,
4876
+ expectedTitle: input.expectedTitle,
4877
+ baselines: [keep, ...duplicates].map((page) => ({
4878
+ pageId: page.pageId,
4879
+ version: page.version,
4880
+ contentHash: page.contentHash
4881
+ }))
4882
+ })).digest("hex");
4883
+ for (const duplicate of duplicates) await adapter.deleteWikiPage({
4884
+ teamId: duplicate.teamId,
4885
+ spaceId,
4886
+ pageId: duplicate.pageId
4887
+ });
4888
+ const result = {
4889
+ keptPageId: keep.pageId,
4890
+ deletedPageIds: duplicates.map((page) => page.pageId)
4891
+ };
4892
+ return {
4893
+ content: [{
4894
+ type: "text",
4895
+ text: `Kept Wiki page ${result.keptPageId} and deleted empty duplicates: ${result.deletedPageIds.join(", ")}.\noperationHash: ${operationHash}`
4896
+ }],
4897
+ structuredContent: result
4898
+ };
4899
+ }
4900
+ //#endregion
4424
4901
  //#region ../../src/tools/wiki-read.ts
4425
4902
  const WikiSourceSchema = zod_v4.z.string().trim().min(1).optional();
4426
4903
  const GetOnesWikiPageSchema = zod_v4.z.object({
4427
4904
  pageId: zod_v4.z.string().trim().min(1).optional(),
4428
4905
  url: zod_v4.z.string().url().optional(),
4429
- path: zod_v4.z.union([zod_v4.z.string().trim().min(1), zod_v4.z.array(zod_v4.z.string().trim().min(1)).min(1)]).optional().describe("Wiki path such as \"Department/Annual Plans/2026\". Resolves a unique exact, title-prefix, or confidently close match; otherwise returns candidate pages for confirmation."),
4906
+ path: zod_v4.z.union([zod_v4.z.string().trim().min(1), zod_v4.z.array(zod_v4.z.string().trim().min(1)).min(1)]).optional().describe("Wiki path such as \"Department/Annual Plans/2026\". Exact self-reference segments such as \"我的\", \"我\", \"me\", or \"my\" resolve the authenticated user through ONES token info without exposing the display name. Resolves a unique exact, title-prefix, or confidently close match; otherwise returns candidate pages for confirmation."),
4430
4907
  teamId: zod_v4.z.string().trim().min(1).optional(),
4431
4908
  spaceId: zod_v4.z.string().trim().min(1).optional(),
4432
4909
  revealSensitiveSecrets: zod_v4.z.boolean().default(false).describe("Default false. Set true only when the user explicitly asks to reveal secrets."),
@@ -4528,6 +5005,19 @@ async function handleGetOnesWikiPage(input, adapters, defaultSource) {
4528
5005
  teamId: resolved?.teamId ?? input.teamId,
4529
5006
  spaceId: resolved?.spaceId ?? input.spaceId
4530
5007
  }), input.revealSensitiveSecrets);
5008
+ if (resolved) {
5009
+ page.title = resolved.title;
5010
+ page.breadcrumb = resolved.breadcrumb;
5011
+ const redactPrivateValues = resolved.redactPrivateValues;
5012
+ if (redactPrivateValues) {
5013
+ page.content = redactPrivateValues(page.content);
5014
+ page.attachments = page.attachments.map((attachment) => ({
5015
+ ...attachment,
5016
+ name: redactPrivateValues(attachment.name),
5017
+ url: redactPrivateValues(attachment.url)
5018
+ }));
5019
+ }
5020
+ }
4531
5021
  return {
4532
5022
  content: [{
4533
5023
  type: "text",
@@ -4675,7 +5165,7 @@ async function handleLookupEnvironmentAccess(input, adapters, defaultSource) {
4675
5165
  //#region ../../src/tools/wiki-write.ts
4676
5166
  const APPROVAL_TTL_MS = 1800 * 1e3;
4677
5167
  const SourceSchema = zod_v4.z.string().trim().min(1).optional();
4678
- const PathSchema = zod_v4.z.union([zod_v4.z.string().trim().min(1), zod_v4.z.array(zod_v4.z.string().trim().min(1)).min(1)]);
5168
+ const PathSchema = zod_v4.z.union([zod_v4.z.string().trim().min(1), zod_v4.z.array(zod_v4.z.string().trim().min(1)).min(1)]).describe("Wiki path. Exact self-reference segments such as \"我的\", \"我\", \"me\", or \"my\" resolve the authenticated user through ONES token info without exposing the display name.");
4679
5169
  const PrepareWikiCreateSchema = zod_v4.z.object({
4680
5170
  parentPageId: zod_v4.z.string().trim().min(1).optional(),
4681
5171
  parentPath: PathSchema.optional(),
@@ -4699,6 +5189,10 @@ const ReplaceTextSchema = zod_v4.z.object({
4699
5189
  find: zod_v4.z.string().min(1),
4700
5190
  replace: zod_v4.z.string()
4701
5191
  });
5192
+ const ReplaceDocumentSchema = zod_v4.z.object({
5193
+ type: zod_v4.z.literal("replace_document"),
5194
+ markdown: zod_v4.z.string().trim().min(1)
5195
+ });
4702
5196
  const PrepareWikiUpdateSchema = zod_v4.z.object({
4703
5197
  pageId: zod_v4.z.string().trim().min(1).optional(),
4704
5198
  url: zod_v4.z.string().url().optional(),
@@ -4708,7 +5202,8 @@ const PrepareWikiUpdateSchema = zod_v4.z.object({
4708
5202
  operation: zod_v4.z.discriminatedUnion("type", [
4709
5203
  AppendBlocksSchema,
4710
5204
  AppendTableRowSchema,
4711
- ReplaceTextSchema
5205
+ ReplaceTextSchema,
5206
+ ReplaceDocumentSchema
4712
5207
  ]),
4713
5208
  source: SourceSchema
4714
5209
  }).refine((value) => [
@@ -4749,6 +5244,10 @@ const WikiUpdateOperationSchema = zod_v4.z.discriminatedUnion("type", [
4749
5244
  type: zod_v4.z.literal("replace_text"),
4750
5245
  find: zod_v4.z.string(),
4751
5246
  replace: zod_v4.z.string()
5247
+ }),
5248
+ zod_v4.z.object({
5249
+ type: zod_v4.z.literal("replace_document"),
5250
+ markdown: zod_v4.z.string()
4752
5251
  })
4753
5252
  ]);
4754
5253
  const WikiUpdateRequestSchema = zod_v4.z.object({
@@ -4878,11 +5377,24 @@ async function handlePrepareWikiCreate(input, adapters, approvals, defaultSource
4878
5377
  const teamId = resolved?.teamId ?? parent.teamId;
4879
5378
  const spaceId = resolved?.spaceId ?? parent.spaceId;
4880
5379
  if (!spaceId) throw new Error("The target space could not be verified");
5380
+ const title = input.title.trim();
5381
+ const titleCandidates = await adapter.searchWikiPages({
5382
+ query: title,
5383
+ teamId,
5384
+ spaceId,
5385
+ limit: 50
5386
+ });
5387
+ const siblingConflicts = (await Promise.all(titleCandidates.filter((candidate) => candidate.title === title).map((candidate) => adapter.getWikiPage({
5388
+ pageId: candidate.pageId,
5389
+ teamId: candidate.teamId,
5390
+ spaceId: candidate.spaceId ?? spaceId
5391
+ })))).filter((page) => page.parentPageId === parent.pageId);
5392
+ if (siblingConflicts.length) throw new Error(`Wiki title already exists under the selected parent (${siblingConflicts.map((page) => page.pageId).join(", ")}). Ask the user to choose one action: edit the existing page, delete it and recreate, or create with a new title.`);
4881
5393
  const requestWithoutKey = {
4882
5394
  teamId,
4883
5395
  spaceId,
4884
5396
  parentPageId: parent.pageId,
4885
- title: input.title.trim(),
5397
+ title,
4886
5398
  markdown: input.markdown
4887
5399
  };
4888
5400
  const operationHash = hashOperation({
@@ -4904,7 +5416,7 @@ async function handlePrepareWikiCreate(input, adapters, approvals, defaultSource
4904
5416
  });
4905
5417
  const plan = {
4906
5418
  kind: "create",
4907
- targetBreadcrumb: [...parent.breadcrumb, input.title.trim()],
5419
+ targetBreadcrumb: [...resolved?.breadcrumb ?? parent.breadcrumb, input.title.trim()],
4908
5420
  request,
4909
5421
  parentBaseline: baseline(parent),
4910
5422
  operationHash,
@@ -4961,7 +5473,7 @@ async function handlePrepareWikiUpdate(input, adapters, approvals, defaultSource
4961
5473
  });
4962
5474
  const plan = {
4963
5475
  kind: "update",
4964
- targetBreadcrumb: page.breadcrumb,
5476
+ targetBreadcrumb: resolved?.breadcrumb ?? page.breadcrumb,
4965
5477
  request,
4966
5478
  operationHash,
4967
5479
  approvalToken: approval.token,
@@ -4971,7 +5483,7 @@ async function handlePrepareWikiUpdate(input, adapters, approvals, defaultSource
4971
5483
  content: [{
4972
5484
  type: "text",
4973
5485
  text: [
4974
- `Prepared Wiki update for ${page.breadcrumb.join(" / ") || page.title}.`,
5486
+ `Prepared Wiki update for ${plan.targetBreadcrumb.join(" / ") || page.title}.`,
4975
5487
  "No write was performed. Ask the user to confirm this exact operation immediately before apply.",
4976
5488
  `operationHash: ${plan.operationHash}`,
4977
5489
  `approvalToken: ${plan.approvalToken}`,
@@ -5084,7 +5596,7 @@ function createRequirementsServer(config, adapterOverrides) {
5084
5596
  });
5085
5597
  server.registerTool("get_ones_wiki_page", {
5086
5598
  title: "Get ONES Wiki Page",
5087
- description: "Read one ONES Wiki page as Markdown by page ID, URL, or hierarchical path. Prefer path for requests like \"Department/Annual Plans/2026\". Unique confidently close paths are corrected automatically; unresolved or ambiguous paths return candidate pages for confirmation. Sensitive values are redacted by default; revealing them requires an explicit argument based on an explicit user request.",
5599
+ description: "Read one ONES Wiki page as Markdown by page ID, URL, or hierarchical path. Prefer path for requests like \"Department/Annual Plans/2026\". Exact self-reference path segments such as \"我的\", \"我\", \"me\", or \"my\" resolve the authenticated user through ONES token info; never infer or expose the display name from local Git or machine configuration. Unique confidently close paths are corrected automatically; unresolved or ambiguous paths return candidate pages for confirmation. Sensitive values are redacted by default; revealing them requires an explicit argument based on an explicit user request.",
5088
5600
  inputSchema: GetOnesWikiPageSchema,
5089
5601
  annotations: {
5090
5602
  readOnlyHint: true,
@@ -5146,7 +5658,7 @@ function createRequirementsServer(config, adapterOverrides) {
5146
5658
  });
5147
5659
  server.registerTool("prepare_wiki_create", {
5148
5660
  title: "Prepare ONES Wiki Create",
5149
- description: "Resolve an exact parent page and prepare one exact Wiki create operation. Never writes. Returns a one-time 30-minute approval token.",
5661
+ description: "Resolve an exact parent page and prepare one exact Wiki create operation. If a sibling page already has the requested title, fail closed and require the user to choose: edit the existing page, delete it and recreate, or use a new title. Exact self-reference path segments such as \"我的\", \"我\", \"me\", or \"my\" resolve the authenticated user through ONES token info; never infer or expose the display name from local Git or machine configuration. Never writes. Returns a one-time 30-minute approval token.",
5150
5662
  inputSchema: PrepareWikiCreateSchema,
5151
5663
  outputSchema: PrepareWikiCreateOutputSchema,
5152
5664
  annotations: {
@@ -5185,7 +5697,7 @@ function createRequirementsServer(config, adapterOverrides) {
5185
5697
  });
5186
5698
  server.registerTool("prepare_wiki_update", {
5187
5699
  title: "Prepare ONES Wiki Update",
5188
- description: "Prepare an exact minimal Wiki update, including exact table-row targeting. Never writes. Ambiguous pages, tables, or text matches fail closed.",
5700
+ description: "Prepare an exact minimal Wiki update, including exact table-row targeting. Exact self-reference path segments such as \"我的\", \"我\", \"me\", or \"my\" resolve the authenticated user through ONES token info; never infer or expose the display name from local Git or machine configuration. Never writes. Ambiguous pages, tables, or text matches fail closed.",
5189
5701
  inputSchema: PrepareWikiUpdateSchema,
5190
5702
  outputSchema: PrepareWikiUpdateOutputSchema,
5191
5703
  annotations: {
@@ -5222,6 +5734,28 @@ function createRequirementsServer(config, adapterOverrides) {
5222
5734
  return toolError(err);
5223
5735
  }
5224
5736
  });
5737
+ server.registerTool("delete_empty_wiki_duplicates", {
5738
+ title: "Delete Empty ONES Wiki Duplicates",
5739
+ description: "Delete only explicitly confirmed, title-only duplicate sibling pages while retaining one verified page with a body. Revalidates every page immediately before deletion and fails closed if any duplicate contains additional content.",
5740
+ inputSchema: DeleteEmptyWikiDuplicatesSchema,
5741
+ outputSchema: DeleteEmptyWikiDuplicatesOutputSchema,
5742
+ annotations: {
5743
+ readOnlyHint: false,
5744
+ destructiveHint: true,
5745
+ idempotentHint: false,
5746
+ openWorldHint: true
5747
+ }
5748
+ }, async (params) => {
5749
+ try {
5750
+ const sourceType = params.source ?? defaultSource;
5751
+ return await handleDeleteEmptyWikiDuplicates(params, adapters, {
5752
+ defaultSource,
5753
+ writesEnabled: wikiWritesEnabled(sourceType)
5754
+ });
5755
+ } catch (err) {
5756
+ return toolError(err);
5757
+ }
5758
+ });
5225
5759
  server.registerTool("list_sources", {
5226
5760
  title: "List Sources",
5227
5761
  description: "List all configured requirement sources and their status",