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.mjs CHANGED
@@ -185,7 +185,7 @@ function loadConfig(startDir) {
185
185
  }
186
186
  //#endregion
187
187
  //#region package.json
188
- var version = "0.4.0";
188
+ var version = "0.5.0";
189
189
  //#endregion
190
190
  //#region ../../src/utils/ones-issue-kind.ts
191
191
  /**
@@ -265,18 +265,34 @@ var BaseAdapter = class {
265
265
  async updateWikiPage(_params) {
266
266
  throw new Error(`${this.sourceType}: Wiki update endpoint is not verified`);
267
267
  }
268
+ /** Production Wiki deletes stay disabled until the exact provider endpoint is verified. */
269
+ async deleteWikiPage(_params) {
270
+ throw new Error(`${this.sourceType}: Wiki delete endpoint is not verified`);
271
+ }
268
272
  };
269
273
  //#endregion
270
274
  //#region ../../src/adapters/ones/api-client.ts
271
275
  function base64Url(buffer) {
272
276
  return buffer.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
273
277
  }
274
- function getSetCookies(response) {
278
+ function getSetCookies$1(response) {
279
+ if (!response.headers) return [];
275
280
  const headers = response.headers;
276
281
  if (headers.getSetCookie) return headers.getSetCookie();
277
282
  const raw = response.headers.get("set-cookie");
278
283
  return raw ? [raw] : [];
279
284
  }
285
+ function mergeResponseCookies(cookieJar, response) {
286
+ for (const cookie of getSetCookies$1(response)) {
287
+ const pair = cookie.split(";")[0];
288
+ const separator = pair.indexOf("=");
289
+ if (separator <= 0) continue;
290
+ cookieJar.set(pair.slice(0, separator), pair.slice(separator + 1));
291
+ }
292
+ }
293
+ function serializeCookies(cookieJar) {
294
+ return [...cookieJar].map(([name, value]) => `${name}=${value}`).join("; ");
295
+ }
280
296
  function parseRedirectValue(location, names) {
281
297
  try {
282
298
  const parsed = new URL(location);
@@ -304,6 +320,7 @@ var OnesApiClient = class {
304
320
  const email = this.resolvedAuth.email;
305
321
  const password = this.resolvedAuth.password;
306
322
  if (!email || !password) throw new Error("ONES auth requires email and password (ones-pkce auth type)");
323
+ const cookieJar = /* @__PURE__ */ new Map();
307
324
  const certRes = await fetch(`${baseUrl}/identity/api/encryption_cert`, {
308
325
  method: "POST",
309
326
  headers: { "Content-Type": "application/json" },
@@ -324,11 +341,15 @@ var OnesApiClient = class {
324
341
  })
325
342
  });
326
343
  if (!loginRes.ok) throw new Error(`ONES: Login failed with status ${loginRes.status}`);
327
- const cookies = getSetCookies(loginRes).map((cookie) => cookie.split(";")[0]).join("; ");
344
+ mergeResponseCookies(cookieJar, loginRes);
328
345
  const loginData = await loginRes.json();
329
346
  const configuredOrgUuid = this.config.options?.orgUuid;
330
347
  const orgUser = configuredOrgUuid ? loginData.org_users.find((user) => user.org_uuid === configuredOrgUuid) ?? loginData.org_users[0] : loginData.org_users[0];
331
348
  if (!orgUser) throw new Error("ONES: No organizations found for this user");
349
+ cookieJar.set("ones-region-uuid", orgUser.region_uuid);
350
+ cookieJar.set("ones-org-uuid", orgUser.org_uuid);
351
+ const timezone = cookieJar.get("ones-tz");
352
+ if (timezone) cookieJar.set("timezone", timezone);
332
353
  const codeVerifier = base64Url(crypto.randomBytes(32));
333
354
  const codeChallenge = base64Url(crypto.createHash("sha256").update(codeVerifier).digest());
334
355
  const authorizeParams = new URLSearchParams({
@@ -340,15 +361,17 @@ var OnesApiClient = class {
340
361
  redirect_uri: `${baseUrl}/auth/authorize/callback`,
341
362
  state: `org_uuid=${orgUser.org_uuid}`
342
363
  });
343
- const authorizeLocation = (await fetch(`${baseUrl}/identity/authorize`, {
364
+ const authorizeRes = await fetch(`${baseUrl}/identity/authorize`, {
344
365
  method: "POST",
345
366
  headers: {
346
367
  "Content-Type": "application/x-www-form-urlencoded",
347
- "Cookie": cookies
368
+ "Cookie": serializeCookies(cookieJar)
348
369
  },
349
370
  body: authorizeParams.toString(),
350
371
  redirect: "manual"
351
- })).headers.get("location");
372
+ });
373
+ mergeResponseCookies(cookieJar, authorizeRes);
374
+ const authorizeLocation = authorizeRes.headers.get("location");
352
375
  if (!authorizeLocation) throw new Error("ONES: Authorize response missing location header");
353
376
  let code = parseRedirectValue(authorizeLocation, ["code"]);
354
377
  if (!code) {
@@ -358,7 +381,7 @@ var OnesApiClient = class {
358
381
  method: "POST",
359
382
  headers: {
360
383
  "Content-Type": "application/json;charset=UTF-8",
361
- "Cookie": cookies
384
+ "Cookie": serializeCookies(cookieJar)
362
385
  },
363
386
  body: JSON.stringify({
364
387
  auth_request_id: authRequestId,
@@ -367,12 +390,15 @@ var OnesApiClient = class {
367
390
  org_user_uuid: orgUser.org_user.org_user_uuid
368
391
  })
369
392
  });
393
+ mergeResponseCookies(cookieJar, finalizeRes);
370
394
  if (!finalizeRes.ok) throw new Error(`ONES: Finalize failed with status ${finalizeRes.status}`);
371
- const callbackLocation = (await fetch(`${baseUrl}/identity/authorize/callback?id=${authRequestId}&lang=zh`, {
395
+ const callbackRes = await fetch(`${baseUrl}/identity/authorize/callback?id=${authRequestId}&lang=zh`, {
372
396
  method: "GET",
373
- headers: { Cookie: cookies },
397
+ headers: { Cookie: serializeCookies(cookieJar) },
374
398
  redirect: "manual"
375
- })).headers.get("location");
399
+ });
400
+ mergeResponseCookies(cookieJar, callbackRes);
401
+ const callbackLocation = callbackRes.headers.get("location");
376
402
  if (!callbackLocation) throw new Error("ONES: Callback response missing location header");
377
403
  code = parseRedirectValue(callbackLocation, ["code"]);
378
404
  }
@@ -381,7 +407,7 @@ var OnesApiClient = class {
381
407
  method: "POST",
382
408
  headers: {
383
409
  "Content-Type": "application/x-www-form-urlencoded",
384
- "Cookie": cookies
410
+ "Cookie": serializeCookies(cookieJar)
385
411
  },
386
412
  body: new URLSearchParams({
387
413
  grant_type: "authorization_code",
@@ -391,16 +417,20 @@ var OnesApiClient = class {
391
417
  redirect_uri: `${baseUrl}/auth/authorize/callback`
392
418
  }).toString()
393
419
  });
420
+ mergeResponseCookies(cookieJar, tokenRes);
394
421
  if (!tokenRes.ok) throw new Error(`ONES: Token exchange failed with status ${tokenRes.status}`);
395
422
  const token = await tokenRes.json();
423
+ cookieJar.set("ones-lt", token.access_token);
396
424
  const teamsRes = await fetch(`${baseUrl}/project/api/project/organization/${orgUser.org_uuid}/stamps/data?t=org_my_team`, {
397
425
  method: "POST",
398
426
  headers: {
399
427
  "Authorization": `Bearer ${token.access_token}`,
400
- "Content-Type": "application/json;charset=UTF-8"
428
+ "Content-Type": "application/json;charset=UTF-8",
429
+ "Cookie": serializeCookies(cookieJar)
401
430
  },
402
431
  body: JSON.stringify({ org_my_team: 0 })
403
432
  });
433
+ mergeResponseCookies(cookieJar, teamsRes);
404
434
  if (!teamsRes.ok) throw new Error(`ONES: Failed to fetch teams: ${teamsRes.status}`);
405
435
  const teams = (await teamsRes.json()).org_my_team?.teams ?? [];
406
436
  const configuredTeamUuid = this.config.options?.teamUuid;
@@ -412,6 +442,9 @@ var OnesApiClient = class {
412
442
  orgUuid: orgUser.org_uuid,
413
443
  userUuid: orgUser.org_user.org_user_uuid,
414
444
  userName: orgUser.org_user.name,
445
+ cookieHeader: serializeCookies(cookieJar),
446
+ legacyAuthToken: loginData.sid,
447
+ legacyUserId: loginData.auth_user_uuid,
415
448
  expiresAt: Date.now() + (token.expires_in - 60) * 1e3
416
449
  };
417
450
  return this.session;
@@ -644,10 +677,11 @@ function mapOnesTypeFromTask(task) {
644
677
  return mapOnesType(task.subIssueType?.name ?? task.issueType?.name ?? "");
645
678
  }
646
679
  function toRequirement(task, description = "", attachments = []) {
680
+ const displayId = taskDisplayId({}, task, task.project?.identifier?.toUpperCase() ?? null);
647
681
  return {
648
682
  id: task.uuid,
649
683
  source: "ones",
650
- title: `#${task.number} ${task.name}`,
684
+ title: `${displayId} ${task.name}`,
651
685
  description,
652
686
  status: mapOnesStatus(task.status?.category ?? "to_do"),
653
687
  priority: mapOnesPriority(task.priority?.value ?? "normal"),
@@ -1166,17 +1200,43 @@ function renderWikiContent(content, context = { imageSources: [] }) {
1166
1200
  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();
1167
1201
  }
1168
1202
  function newWikiBlockId() {
1169
- return randomBytes(8).toString("base64url").slice(0, 10);
1203
+ for (;;) {
1204
+ const id = randomBytes(9).toString("base64url").replace(/[^a-z0-9]/gi, "").slice(0, 9);
1205
+ if (/^[a-z][a-z0-9]{8}$/i.test(id)) return id;
1206
+ }
1207
+ }
1208
+ function markdownTextRuns(text) {
1209
+ if (!text) return [];
1210
+ const runs = [];
1211
+ const inlinePattern = /\[([^\]]+)\]\((https?:\/\/[^)\s]+)\)|`([^`]+)`/g;
1212
+ let cursor = 0;
1213
+ for (const match of text.matchAll(inlinePattern)) {
1214
+ const index = match.index ?? 0;
1215
+ if (index > cursor) runs.push({ insert: text.slice(cursor, index) });
1216
+ if (match[1] && match[2]) runs.push({
1217
+ insert: match[1],
1218
+ attributes: { link: match[2] }
1219
+ });
1220
+ else if (match[3]) runs.push({
1221
+ insert: match[3],
1222
+ attributes: { "style-code": true }
1223
+ });
1224
+ cursor = index + match[0].length;
1225
+ }
1226
+ if (cursor < text.length) runs.push({ insert: text.slice(cursor) });
1227
+ return runs.length ? runs : [{ insert: text }];
1170
1228
  }
1171
1229
  function wikiTextBlock(text, options = {}) {
1172
1230
  return {
1173
1231
  id: newWikiBlockId(),
1174
1232
  type: options.list ? "list" : "text",
1175
- text: text ? [{ insert: text }] : [],
1233
+ text: markdownTextRuns(text),
1176
1234
  ...options.heading ? { heading: options.heading } : {},
1177
1235
  ...options.list ? {
1178
1236
  ordered: options.ordered ?? false,
1179
- level: 1
1237
+ level: options.level ?? 1,
1238
+ ...options.start === void 0 ? {} : { start: options.start },
1239
+ ...options.groupId ? { groupId: options.groupId } : {}
1180
1240
  } : {}
1181
1241
  };
1182
1242
  }
@@ -1187,6 +1247,9 @@ function isMarkdownSeparatorRow(line) {
1187
1247
  const cells = parseMarkdownRow(line);
1188
1248
  return cells.length > 0 && cells.every((cell) => /^:?-{3,}:?$/.test(cell));
1189
1249
  }
1250
+ function estimateWikiTableCellWidth(value) {
1251
+ return [...value].reduce((width, character) => width + (character.codePointAt(0) > 127 ? 14 : 7), 24);
1252
+ }
1190
1253
  function markdownToWikiDocument(markdown) {
1191
1254
  const document = {
1192
1255
  blocks: [],
@@ -1195,9 +1258,15 @@ function markdownToWikiDocument(markdown) {
1195
1258
  };
1196
1259
  const blocks = document.blocks;
1197
1260
  const lines = markdown.replace(/\r\n/g, "\n").split("\n");
1261
+ let activeList = null;
1198
1262
  for (let index = 0; index < lines.length; index += 1) {
1199
1263
  const line = lines[index];
1264
+ if (!line.trim()) {
1265
+ activeList = null;
1266
+ continue;
1267
+ }
1200
1268
  if (line.includes("|") && lines[index + 1] && isMarkdownSeparatorRow(lines[index + 1])) {
1269
+ activeList = null;
1201
1270
  const rows = [parseMarkdownRow(line)];
1202
1271
  index += 2;
1203
1272
  while (index < lines.length && lines[index].includes("|")) {
@@ -1215,34 +1284,120 @@ function markdownToWikiDocument(markdown) {
1215
1284
  blocks.push({
1216
1285
  id: newWikiBlockId(),
1217
1286
  type: "table",
1218
- rows: rows.length,
1219
1287
  cols: columnCount,
1288
+ rows: rows.length,
1289
+ colsWidth: Array.from({ length: columnCount }, (_, column) => Math.max(100, ...rows.map((row) => estimateWikiTableCellWidth(row[column] ?? "")))),
1220
1290
  children
1221
1291
  });
1222
1292
  continue;
1223
1293
  }
1224
1294
  const heading = line.match(/^(#{1,6})[ \t]/);
1225
1295
  if (heading?.[1]) {
1296
+ activeList = null;
1226
1297
  blocks.push(wikiTextBlock(line.slice(heading[0].length).trim(), { heading: heading[1].length }));
1227
1298
  continue;
1228
1299
  }
1229
1300
  const unordered = line.match(/^[ \t]*[-*+][ \t]/);
1230
1301
  if (unordered) {
1231
- blocks.push(wikiTextBlock(line.slice(unordered[0].length).trim(), { list: true }));
1302
+ const groupId = activeList && !activeList.ordered ? activeList.groupId : newWikiBlockId();
1303
+ const start = activeList && !activeList.ordered ? activeList.nextStart : 1;
1304
+ activeList = {
1305
+ ordered: false,
1306
+ groupId,
1307
+ nextStart: start + 1
1308
+ };
1309
+ blocks.push(wikiTextBlock(line.slice(unordered[0].length).trim(), {
1310
+ list: true,
1311
+ groupId,
1312
+ start
1313
+ }));
1232
1314
  continue;
1233
1315
  }
1234
- const ordered = line.match(/^[ \t]*\d+[.)][ \t]/);
1235
- if (ordered) {
1316
+ const ordered = line.match(/^[ \t]*(\d+)[.)][ \t]/);
1317
+ if (ordered?.[1]) {
1318
+ const groupId = activeList?.ordered ? activeList.groupId : newWikiBlockId();
1319
+ const start = Number.parseInt(ordered[1], 10);
1320
+ activeList = {
1321
+ ordered: true,
1322
+ groupId,
1323
+ nextStart: start + 1
1324
+ };
1236
1325
  blocks.push(wikiTextBlock(line.slice(ordered[0].length).trim(), {
1237
1326
  list: true,
1238
- ordered: true
1327
+ ordered: true,
1328
+ start,
1329
+ groupId
1239
1330
  }));
1240
1331
  continue;
1241
1332
  }
1333
+ activeList = null;
1242
1334
  blocks.push(wikiTextBlock(line));
1243
1335
  }
1244
1336
  return document;
1245
1337
  }
1338
+ function markdownToWikiHtml(markdown) {
1339
+ const renderInline = (text) => markdownTextRuns(text).map((run) => {
1340
+ const escaped = escapeWikiHtml(run.insert);
1341
+ const content = run.attributes?.["style-code"] ? `<code>${escaped}</code>` : escaped;
1342
+ return run.attributes?.link ? `<a href="${escapeWikiHtml(run.attributes.link)}" target="_blank" rel="noopener noreferrer">${content}</a>` : content;
1343
+ }).join("");
1344
+ const lines = markdown.replace(/\r\n/g, "\n").split("\n");
1345
+ const html = [];
1346
+ for (let index = 0; index < lines.length; index += 1) {
1347
+ const line = lines[index];
1348
+ if (!line.trim()) continue;
1349
+ if (line.includes("|") && lines[index + 1] && isMarkdownSeparatorRow(lines[index + 1])) {
1350
+ const rows = [parseMarkdownRow(line)];
1351
+ index += 2;
1352
+ while (index < lines.length && lines[index].includes("|")) {
1353
+ rows.push(parseMarkdownRow(lines[index]));
1354
+ index += 1;
1355
+ }
1356
+ index -= 1;
1357
+ const columnCount = Math.max(...rows.map((row) => row.length));
1358
+ const width = Math.floor(100 / columnCount);
1359
+ const header = Array.from({ length: columnCount }).map((_, column) => `<th style="width:${width}%">${renderInline(rows[0][column] ?? "")}</th>`).join("");
1360
+ const body = rows.slice(1).map((row) => `<tr>${Array.from({ length: columnCount }).map((_, column) => `<td>${renderInline(row[column] ?? "")}</td>`).join("")}</tr>`).join("");
1361
+ html.push(`<table style="width:100%"><thead><tr>${header}</tr></thead><tbody>${body}</tbody></table>`);
1362
+ continue;
1363
+ }
1364
+ const heading = line.match(/^(#{1,6})[ \t]/);
1365
+ if (heading?.[1]) {
1366
+ const level = heading[1].length;
1367
+ html.push(`<h${level}>${renderInline(line.slice(heading[0].length).trim())}</h${level}>`);
1368
+ continue;
1369
+ }
1370
+ if (line.match(/^[ \t]*(\d+)[.)][ \t]/)?.[1]) {
1371
+ const items = [];
1372
+ while (index < lines.length) {
1373
+ const item = lines[index].match(/^[ \t]*(\d+)[.)][ \t]/);
1374
+ if (!item?.[1]) break;
1375
+ items.push({
1376
+ value: Number.parseInt(item[1], 10),
1377
+ text: lines[index].slice(item[0].length).trim()
1378
+ });
1379
+ index += 1;
1380
+ }
1381
+ index -= 1;
1382
+ html.push(`<ol start="${items[0].value}">${items.map((item) => `<li>${renderInline(item.text)}</li>`).join("")}</ol>`);
1383
+ continue;
1384
+ }
1385
+ if (line.match(/^[ \t]*[-*+][ \t]/)) {
1386
+ const items = [];
1387
+ while (index < lines.length) {
1388
+ const item = lines[index].match(/^[ \t]*[-*+][ \t]/);
1389
+ if (!item) break;
1390
+ items.push(lines[index].slice(item[0].length).trim());
1391
+ index += 1;
1392
+ }
1393
+ index -= 1;
1394
+ html.push(`<ul>${items.map((item) => `<li>${renderInline(item)}</li>`).join("")}</ul>`);
1395
+ continue;
1396
+ }
1397
+ html.push(`<p>${renderInline(line.trim())}</p>`);
1398
+ }
1399
+ return html.join("\n");
1400
+ }
1246
1401
  function parseWikiDocument(content) {
1247
1402
  const document = parseJsonRecord(content);
1248
1403
  if (!document || !Array.isArray(document.blocks)) throw new Error("ONES: Wiki content is not a supported collaborative document");
@@ -1299,6 +1454,17 @@ function appendWikiTableRow(document, operation) {
1299
1454
  table.rows = (typeof table.rows === "number" ? table.rows : layout.rows.length) + 1;
1300
1455
  }
1301
1456
  function applyWikiUpdateOperation(content, operation) {
1457
+ if (operation.type === "replace_document") {
1458
+ const current = parseWikiDocument(content);
1459
+ const replacement = markdownToWikiDocument(operation.markdown);
1460
+ for (const key of [
1461
+ "comments",
1462
+ "meta",
1463
+ "authors",
1464
+ "commentators"
1465
+ ]) if (Object.hasOwn(current, key)) replacement[key] = current[key];
1466
+ return JSON.stringify(replacement);
1467
+ }
1302
1468
  const document = parseWikiDocument(content);
1303
1469
  if (operation.type === "append_blocks") appendWikiDocument(document, markdownToWikiDocument(operation.markdown));
1304
1470
  else if (operation.type === "replace_text") replaceWikiText(document, operation.find, operation.replace);
@@ -1337,6 +1503,26 @@ var WikiPathResolutionError = class extends Error {
1337
1503
  };
1338
1504
  //#endregion
1339
1505
  //#region ../../src/adapters/ones/wiki-reader.ts
1506
+ const CURRENT_USER_PATH_ALIASES = {
1507
+ "i": true,
1508
+ "me": true,
1509
+ "mine": true,
1510
+ "my": true,
1511
+ "myself": true,
1512
+ "current user": true,
1513
+ "current-user": true,
1514
+ "current_user": true,
1515
+ "我": true,
1516
+ "我的": true,
1517
+ "本人": true,
1518
+ "当前用户": true,
1519
+ "当前账号": true,
1520
+ "自己": true
1521
+ };
1522
+ function currentUserPathAlias(segment) {
1523
+ const trimmed = segment.trim();
1524
+ return CURRENT_USER_PATH_ALIASES[trimmed.toLocaleLowerCase()] ? trimmed : null;
1525
+ }
1340
1526
  var OnesWikiOpenApiError = class extends Error {
1341
1527
  status;
1342
1528
  reason;
@@ -1431,12 +1617,29 @@ function segmentSimilarity(left, right) {
1431
1617
  var OnesWikiReader = class {
1432
1618
  options;
1433
1619
  treeCache = /* @__PURE__ */ new Map();
1620
+ currentUserNameRequest = null;
1434
1621
  constructor(options) {
1435
1622
  this.options = options;
1436
1623
  }
1437
1624
  invalidateTree(teamId, spaceId) {
1438
1625
  this.treeCache.delete(`${teamId}:${spaceId}`);
1439
1626
  }
1627
+ async fetchCurrentUserName() {
1628
+ const session = await this.options.getSession();
1629
+ const response = await fetch(new URL("/wiki/api/project/auth/token_info", this.options.apiBase).toString(), { headers: { Authorization: `Bearer ${session.accessToken}` } });
1630
+ if (!response.ok) throw new Error(`ONES Wiki token info error: ${response.status}`);
1631
+ const payload = await response.json();
1632
+ const name = firstNonEmptyString$1(payload.user?.name, payload.data?.user?.name, payload.data?.name, payload.name, payload.user_name, payload.userName);
1633
+ if (!name) throw new Error("ONES Wiki token info did not include a resolvable current account");
1634
+ return name;
1635
+ }
1636
+ currentUserName() {
1637
+ if (!this.currentUserNameRequest) this.currentUserNameRequest = this.fetchCurrentUserName().catch((error) => {
1638
+ this.currentUserNameRequest = null;
1639
+ throw error;
1640
+ });
1641
+ return this.currentUserNameRequest;
1642
+ }
1440
1643
  async openApi(apiPath) {
1441
1644
  if (!apiPath.startsWith("/wiki/")) throw new Error("ONES: Invalid Wiki Open API path");
1442
1645
  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");
@@ -1696,8 +1899,19 @@ var OnesWikiReader = class {
1696
1899
  return score >= MIN_PATH_MATCH_SIMILARITY ? score : null;
1697
1900
  }
1698
1901
  async resolvePath(params) {
1699
- const path = params.path.map((segment) => segment.trim()).filter(Boolean);
1700
- if (!path.length) throw new Error("ONES: Wiki path is empty");
1902
+ const requestedPath = params.path.map((segment) => segment.trim()).filter(Boolean);
1903
+ if (!requestedPath.length) throw new Error("ONES: Wiki path is empty");
1904
+ const currentUserAlias = requestedPath.map(currentUserPathAlias).find((alias) => alias !== null) ?? null;
1905
+ const currentUserName = currentUserAlias ? await this.currentUserName() : null;
1906
+ const path = currentUserName ? requestedPath.map((segment) => currentUserPathAlias(segment) ? currentUserName : segment) : requestedPath;
1907
+ const redactCurrentUser = (page) => {
1908
+ if (!currentUserName || !currentUserAlias) return page;
1909
+ return {
1910
+ ...page,
1911
+ title: page.title === currentUserName ? currentUserAlias : page.title,
1912
+ breadcrumb: page.breadcrumb.map((segment) => segment === currentUserName ? currentUserAlias : segment)
1913
+ };
1914
+ };
1701
1915
  const candidates = await this.search({
1702
1916
  query: path.at(-1),
1703
1917
  teamId: params.teamId,
@@ -1744,26 +1958,32 @@ var OnesWikiReader = class {
1744
1958
  });
1745
1959
  }
1746
1960
  }
1747
- if (matches.length > 1) throw new WikiPathResolutionError("ambiguous", path, matches);
1961
+ if (matches.length > 1) throw new WikiPathResolutionError("ambiguous", requestedPath, matches.map(redactCurrentUser));
1748
1962
  let match = matches[0];
1749
1963
  if (!match && fuzzy.length) {
1750
1964
  fuzzy.sort((left, right) => right.score - left.score);
1751
1965
  const competing = fuzzy.filter((candidate) => fuzzy[0].score - candidate.score < PATH_MATCH_MARGIN);
1752
- if (competing.length > 1) throw new WikiPathResolutionError("ambiguous", path, competing.map((candidate) => candidate.page));
1966
+ if (competing.length > 1) throw new WikiPathResolutionError("ambiguous", requestedPath, competing.map((candidate) => redactCurrentUser(candidate.page)));
1753
1967
  match = fuzzy[0].page;
1754
1968
  }
1755
1969
  if (!match) {
1756
1970
  const inspectedIds = new Set(inspected.map((page) => page.pageId));
1757
- throw new WikiPathResolutionError("not_found", path, [...inspected, ...candidates.filter((page) => !inspectedIds.has(page.pageId))]);
1971
+ throw new WikiPathResolutionError("not_found", requestedPath, [...inspected, ...candidates.filter((page) => !inspectedIds.has(page.pageId))].map(redactCurrentUser));
1758
1972
  }
1759
1973
  if (!match.spaceId) throw new Error("ONES: Wiki space ID could not be verified");
1760
- return {
1974
+ const publicMatch = redactCurrentUser(match);
1975
+ const resolution = {
1761
1976
  teamId: match.teamId,
1762
1977
  spaceId: match.spaceId,
1763
1978
  pageId: match.pageId,
1764
- title: match.title,
1765
- breadcrumb: match.breadcrumb
1979
+ title: publicMatch.title,
1980
+ breadcrumb: publicMatch.breadcrumb
1766
1981
  };
1982
+ if (currentUserName && currentUserAlias) Object.defineProperty(resolution, "redactPrivateValues", {
1983
+ enumerable: false,
1984
+ value: (value) => value.split(currentUserName).join(currentUserAlias)
1985
+ });
1986
+ return resolution;
1767
1987
  }
1768
1988
  };
1769
1989
  //#endregion
@@ -1965,8 +2185,9 @@ var OnesTaskContent = class {
1965
2185
  attachments: rendered.attachments
1966
2186
  };
1967
2187
  })), containsInlineTaskImages(task) ? this.getTaskImageAttachments(task) : Promise.resolve([])]);
2188
+ const projectIdentifier = task.project?.identifier?.toUpperCase() ?? null;
1968
2189
  const parts = [
1969
- `# #${task.number} ${task.name}`,
2190
+ `# ${taskDisplayId({}, task, projectIdentifier)} ${task.name}`,
1970
2191
  "",
1971
2192
  `- **Type**: ${task.issueType?.name ?? "Unknown"}`,
1972
2193
  "- **Work Item Kind**: requirement",
@@ -1978,7 +2199,7 @@ var OnesTaskContent = class {
1978
2199
  parts.push(`- **UUID**: ${task.uuid}`);
1979
2200
  if (task.relatedTasks?.length) {
1980
2201
  parts.push("", "## Related Tasks");
1981
- for (const related of task.relatedTasks) parts.push(`- #${related.number} ${related.name} [${related.issueType?.name}] (${related.status?.name}) — ${related.assign?.name ?? "Unassigned"}`);
2202
+ 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"}`);
1982
2203
  }
1983
2204
  if (relatedActivities.length) {
1984
2205
  parts.push("", "## Related Work Items");
@@ -1993,7 +2214,7 @@ var OnesTaskContent = class {
1993
2214
  }
1994
2215
  if (task.parent?.uuid) {
1995
2216
  parts.push("", "## Parent Task", `- UUID: ${task.parent.uuid}`);
1996
- if (task.parent.number) parts.push(`- Number: #${task.parent.number}`);
2217
+ if (task.parent.number) parts.push(`- Number: ${projectIdentifier ? `${projectIdentifier}-${task.parent.number}` : `#${task.parent.number}`}`);
1997
2218
  }
1998
2219
  if (wikiContents.length > 0) {
1999
2220
  parts.push("", "---", "", "## Requirement Documents");
@@ -2016,8 +2237,9 @@ var OnesTaskContent = class {
2016
2237
  }
2017
2238
  buildWorkItemSummary(task, kind) {
2018
2239
  const nextTool = kind === "defect" ? "get_issue_detail" : "get_related_issues / get_testcases";
2240
+ const projectIdentifier = task.project?.identifier?.toUpperCase() ?? null;
2019
2241
  const parts = [
2020
- `# #${task.number} ${task.name}`,
2242
+ `# ${taskDisplayId({}, task, projectIdentifier)} ${task.name}`,
2021
2243
  "",
2022
2244
  `- **Type**: ${task.subIssueType?.name ?? task.issueType?.name ?? "Unknown"}`,
2023
2245
  `- **Work Item Kind**: ${kind}`,
@@ -2029,14 +2251,14 @@ var OnesTaskContent = class {
2029
2251
  parts.push(`- **UUID**: ${task.uuid}`);
2030
2252
  if (task.parent?.uuid) {
2031
2253
  parts.push("", "## Parent Task", `- UUID: ${task.parent.uuid}`);
2032
- if (task.parent.number) parts.push(`- Number: #${task.parent.number}`);
2254
+ if (task.parent.number) parts.push(`- Number: ${projectIdentifier ? `${projectIdentifier}-${task.parent.number}` : `#${task.parent.number}`}`);
2033
2255
  }
2034
2256
  const detailText = getTaskDetailText(task);
2035
2257
  if (detailText) parts.push("", "---", "", kind === "defect" ? "## Defect Detail" : "## Task Detail", "", detailText);
2036
2258
  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.`);
2037
2259
  if (task.relatedTasks?.length) {
2038
2260
  parts.push("", "## Related Tasks");
2039
- for (const related of task.relatedTasks) parts.push(`- #${related.number} ${related.name} [${related.issueType?.name}] (${related.status?.name}) — ${related.assign?.name ?? "Unassigned"}`);
2261
+ 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"}`);
2040
2262
  }
2041
2263
  const requirement = toRequirement(task, parts.join("\n"));
2042
2264
  requirement.raw = {
@@ -2098,7 +2320,7 @@ const TASK_DETAIL_QUERY = `
2098
2320
  priority { value }
2099
2321
  assign { uuid name }
2100
2322
  owner { uuid name }
2101
- project { uuid name }
2323
+ project { uuid name identifier }
2102
2324
  parent { uuid number issueType { uuid name } }
2103
2325
  relatedTasks {
2104
2326
  key uuid number name
@@ -2109,6 +2331,7 @@ const TASK_DETAIL_QUERY = `
2109
2331
  subIssueType { uuid name detailType }
2110
2332
  status { uuid name category }
2111
2333
  assign { uuid name }
2334
+ project { uuid name identifier }
2112
2335
  }
2113
2336
  relatedWikiPages {
2114
2337
  uuid title referenceType subReferenceType errorMessage
@@ -2245,7 +2468,9 @@ var OnesTaskPlanning = class {
2245
2468
  if (!Number.isInteger(raw.number)) throw new TypeError("ONES: Standalone wiki pages cannot be decomposed into requirement tasks");
2246
2469
  const parsedDisplayId = parseDisplayId(params.requirementId);
2247
2470
  const requirementInfo = await this.options.fetchTaskInfo(workItem.id);
2248
- const projectIdentifier = parsedDisplayId?.identifier.toUpperCase() ?? firstString(requirementInfo, ["projectIdentifier", "project_identifier"]);
2471
+ let projectIdentifier = parsedDisplayId?.identifier ?? firstString(requirementInfo, ["projectIdentifier", "project_identifier"]) ?? raw.project?.identifier ?? null;
2472
+ if (!projectIdentifier && raw.project?.uuid) projectIdentifier = await this.options.resolveProjectIdentifier(raw.project.uuid);
2473
+ projectIdentifier = projectIdentifier?.toUpperCase() ?? null;
2249
2474
  const displayId = firstString(requirementInfo, ["displayId", "display_id"]) ?? (projectIdentifier ? `${projectIdentifier}-${raw.number}` : `#${raw.number}`);
2250
2475
  const relatedTasks = (raw.relatedTasks ?? []).filter((task) => classifyOnesWorkItem(task.issueType, task.subIssueType) === "task");
2251
2476
  const relatedInfos = await Promise.all(relatedTasks.map((task) => this.options.fetchTaskInfo(task.uuid)));
@@ -2574,6 +2799,12 @@ var OnesTestcaseReader = class {
2574
2799
  const JSON0_URI = "http://sharejs.org/types/JSONv0";
2575
2800
  const JSON1_URI = "http://sharejs.org/types/JSONv1";
2576
2801
  const DEFAULT_TIMEOUT_MS$1 = 15e3;
2802
+ function getSetCookies(response) {
2803
+ const headers = response.headers;
2804
+ if (headers.getSetCookie) return headers.getSetCookie();
2805
+ const raw = headers.get("set-cookie");
2806
+ return raw ? [raw] : [];
2807
+ }
2577
2808
  function isRecord(value) {
2578
2809
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
2579
2810
  }
@@ -2586,28 +2817,44 @@ function asDoc(value) {
2586
2817
  }
2587
2818
  function createTopLevelJson1Operation(current, next) {
2588
2819
  let operation = null;
2820
+ const appendComponent = (component) => {
2821
+ operation = operation === null ? component : type.compose(operation, component);
2822
+ };
2589
2823
  const keys = [.../* @__PURE__ */ new Set([...Object.keys(current), ...Object.keys(next)])].sort();
2590
2824
  for (const key of keys) {
2825
+ if (key === "blocks") continue;
2591
2826
  const hasCurrent = Object.hasOwn(current, key);
2592
2827
  const hasNext = Object.hasOwn(next, key);
2593
- let component;
2594
- if (!hasCurrent) component = insertOp([key], asDoc(next[key]));
2595
- else if (!hasNext) component = removeOp([key]);
2596
- else if (!isDeepStrictEqual(current[key], next[key])) component = replaceOp([key], asDoc(current[key]), asDoc(next[key]));
2597
- else continue;
2598
- operation = operation === null ? component : type.compose(operation, component);
2599
- }
2828
+ if (!hasCurrent && hasNext) appendComponent(insertOp([key], asDoc(next[key])));
2829
+ else if (hasCurrent && !hasNext) appendComponent(removeOp([key]));
2830
+ else if (hasCurrent && hasNext && !isDeepStrictEqual(current[key], next[key])) appendComponent(replaceOp([key], asDoc(current[key]), asDoc(next[key])));
2831
+ }
2832
+ const currentBlocks = Array.isArray(current.blocks) ? current.blocks : [];
2833
+ const nextBlocks = Array.isArray(next.blocks) ? next.blocks : [];
2834
+ const sharedBlockCount = Math.min(currentBlocks.length, nextBlocks.length);
2835
+ for (let index = 0; index < sharedBlockCount; index += 1) if (!isDeepStrictEqual(currentBlocks[index], nextBlocks[index])) {
2836
+ appendComponent(removeOp(["blocks", index]));
2837
+ appendComponent(insertOp(["blocks", index], asDoc(nextBlocks[index])));
2838
+ }
2839
+ for (let index = currentBlocks.length - 1; index >= nextBlocks.length; index -= 1) appendComponent(removeOp(["blocks", index]));
2840
+ for (let index = currentBlocks.length; index < nextBlocks.length; index += 1) appendComponent(insertOp(["blocks", index], asDoc(nextBlocks[index])));
2600
2841
  if (operation !== null) type.checkValidOp(operation);
2601
2842
  return operation;
2602
2843
  }
2844
+ function createTopLevelJson1Operations(current, next) {
2845
+ const operation = createTopLevelJson1Operation(current, next);
2846
+ return operation === null ? [] : [operation];
2847
+ }
2603
2848
  function wikiEditorUrls(baseUrl, teamId, documentId) {
2604
2849
  const url = new URL(baseUrl);
2605
2850
  if (url.protocol !== "https:" && url.protocol !== "http:") throw new Error("ONES Wiki collaboration requires an HTTP(S) source URL");
2606
2851
  const path = `/wiki/api/wiki/editor/${encodeURIComponent(teamId)}/${encodeURIComponent(documentId)}`;
2607
- const socketUrl = new URL(path, url);
2852
+ const editorBaseUrl = new URL(path, url);
2853
+ const socketUrl = new URL(editorBaseUrl);
2608
2854
  socketUrl.protocol = url.protocol === "https:" ? "wss:" : "ws:";
2609
2855
  return {
2610
- authUrl: new URL(`${path}/auth`, url).toString(),
2856
+ authUrl: `${editorBaseUrl.toString()}/auth`,
2857
+ editorBaseUrl: editorBaseUrl.toString(),
2611
2858
  socketUrl: socketUrl.toString()
2612
2859
  };
2613
2860
  }
@@ -2624,39 +2871,52 @@ function closeSocket(socket) {
2624
2871
  }
2625
2872
  async function replaceOnesWikiDocument(options, update, dependencies = {
2626
2873
  fetch,
2627
- openWebSocket: (url) => new WebSocket(url)
2874
+ openWebSocket: (url, headers) => new WebSocket(url, { headers })
2628
2875
  }) {
2629
- const { authUrl, socketUrl } = wikiEditorUrls(options.baseUrl, options.teamId, options.documentId);
2876
+ const { authUrl, editorBaseUrl, socketUrl } = wikiEditorUrls(options.baseUrl, options.teamId, options.documentId);
2630
2877
  const authResponse = await dependencies.fetch(authUrl, {
2631
2878
  method: "GET",
2632
2879
  headers: {
2633
2880
  "Authorization": `Bearer ${options.accessToken}`,
2634
2881
  "x-live-editor-token": options.editorToken,
2635
- "x-live-editor-base-url": Buffer.from(socketUrl).toString("base64url")
2882
+ "x-live-editor-base-url": Buffer.from(editorBaseUrl).toString("base64url"),
2883
+ ...options.cookieHeader ? { Cookie: options.cookieHeader } : {}
2636
2884
  }
2637
2885
  });
2638
2886
  if (!authResponse.ok) throw new Error(`ONES Wiki collaboration auth failed with status ${authResponse.status}`);
2639
2887
  const editorAuth = await authResponse.json();
2640
2888
  if (typeof editorAuth.read !== "string" || !editorAuth.read) throw new Error("ONES Wiki collaboration auth response did not include a read token");
2889
+ const cookies = [options.cookieHeader, ...getSetCookies(authResponse).map((cookie) => cookie.split(";")[0])].filter((cookie) => Boolean(cookie)).join("; ");
2890
+ const socketHeaders = {
2891
+ "Accept-Language": "zh-CN,zh;q=0.9",
2892
+ "Cache-Control": "no-cache",
2893
+ "Pragma": "no-cache",
2894
+ "User-Agent": "Mozilla/5.0 AppleWebKit/537.36 Chrome/128.0.0.0 Safari/537.36",
2895
+ "Origin": new URL(options.baseUrl).origin,
2896
+ ...cookies ? { Cookie: cookies } : {}
2897
+ };
2641
2898
  return new Promise((resolve, reject) => {
2642
- const socket = dependencies.openWebSocket(socketUrl);
2643
- const sequence = 1;
2644
- let clientId = "";
2899
+ const socket = dependencies.openWebSocket(socketUrl, socketHeaders);
2900
+ let sequence = 1;
2645
2901
  let state = "init";
2646
2902
  let settled = false;
2647
2903
  let snapshotVersion = 0;
2904
+ let currentVersion = 0;
2905
+ let pendingOperations = [];
2906
+ const presenceChannel = `${options.teamId}:${options.documentId}`;
2907
+ const presenceId = randomBytes(7).toString("base64url").slice(0, 9);
2648
2908
  let timeout;
2649
2909
  const finish = (result) => {
2650
2910
  if (settled) return;
2651
2911
  settled = true;
2652
- if (timeout) clearTimeout(timeout);
2912
+ clearTimeout(timeout);
2653
2913
  resolve(result);
2654
2914
  closeSocket(socket);
2655
2915
  };
2656
2916
  const fail = (error) => {
2657
2917
  if (settled) return;
2658
2918
  settled = true;
2659
- if (timeout) clearTimeout(timeout);
2919
+ clearTimeout(timeout);
2660
2920
  reject(error);
2661
2921
  closeSocket(socket);
2662
2922
  };
@@ -2665,9 +2925,38 @@ async function replaceOnesWikiDocument(options, update, dependencies = {
2665
2925
  if (error) fail(/* @__PURE__ */ new Error(`ONES Wiki collaboration send failed: ${error.message}`));
2666
2926
  });
2667
2927
  };
2928
+ const sendHandshake = () => {
2929
+ send({
2930
+ a: "hs",
2931
+ id: null,
2932
+ auth: {
2933
+ appId: options.teamId,
2934
+ docId: options.documentId,
2935
+ userId: options.userId,
2936
+ permission: "w",
2937
+ token: options.editorToken,
2938
+ displayName: options.displayName ?? "",
2939
+ avatarUrl: options.avatarUrl ?? ""
2940
+ }
2941
+ });
2942
+ };
2943
+ const sendNextOperation = () => {
2944
+ const operation = pendingOperations[0];
2945
+ if (operation === void 0) return;
2946
+ send({
2947
+ a: "op",
2948
+ c: options.teamId,
2949
+ d: options.documentId,
2950
+ v: currentVersion,
2951
+ seq: sequence,
2952
+ x: {},
2953
+ op: operation
2954
+ });
2955
+ };
2668
2956
  timeout = setTimeout(() => {
2669
2957
  fail(/* @__PURE__ */ new Error(`ONES Wiki collaboration timed out while waiting for ${state}`));
2670
2958
  }, options.timeoutMs ?? DEFAULT_TIMEOUT_MS$1);
2959
+ socket.on("open", sendHandshake);
2671
2960
  socket.on("error", () => fail(/* @__PURE__ */ new Error("ONES Wiki collaboration WebSocket failed")));
2672
2961
  socket.on("close", (code) => {
2673
2962
  if (!settled) fail(/* @__PURE__ */ new Error(`ONES Wiki collaboration WebSocket closed before ${state} (code ${code})`));
@@ -2694,58 +2983,64 @@ async function replaceOnesWikiDocument(options, update, dependencies = {
2694
2983
  fail(/* @__PURE__ */ new Error("ONES Wiki collaboration returned an unsupported init frame"));
2695
2984
  return;
2696
2985
  }
2697
- clientId = message.id;
2698
2986
  state = "handshake";
2699
- send({
2700
- a: "hs",
2701
- id: clientId,
2702
- auth: {
2703
- appId: options.teamId,
2704
- docId: options.documentId,
2705
- userId: options.userId,
2706
- permission: "w",
2707
- token: editorAuth.read,
2708
- displayName: options.displayName ?? "",
2709
- avatarUrl: options.avatarUrl ?? ""
2710
- },
2711
- options: { ping: {
2712
- interval: 5e4,
2713
- timeout: 15e4
2714
- } }
2715
- });
2987
+ sendHandshake();
2716
2988
  return;
2717
2989
  }
2718
2990
  if (state === "handshake") {
2719
- if (message.a !== "hs" || message.id !== clientId || message.protocol !== 1 || message.protocolMinor !== 1 || message.type !== JSON0_URI) {
2991
+ if (message.a !== "hs" || typeof message.id !== "string" || message.protocol !== 1 || message.protocolMinor !== 1 || message.type !== JSON0_URI) {
2720
2992
  fail(/* @__PURE__ */ new Error("ONES Wiki collaboration returned an unsupported handshake frame"));
2721
2993
  return;
2722
2994
  }
2723
- state = "snapshot";
2995
+ state = "fetch";
2724
2996
  send({
2725
- a: "s",
2997
+ a: "f",
2726
2998
  c: options.teamId,
2727
2999
  d: options.documentId
2728
3000
  });
2729
3001
  return;
2730
3002
  }
2731
- if (state === "snapshot") {
3003
+ if (state === "fetch") {
2732
3004
  const snapshot = message.data;
2733
- if (message.a !== "s" || message.c !== options.teamId || message.d !== options.documentId || typeof snapshot?.v !== "number" || snapshot.type !== JSON1_URI) {
2734
- fail(/* @__PURE__ */ new Error("ONES Wiki collaboration returned an unsupported snapshot frame"));
2735
- return;
2736
- }
3005
+ if (message.a !== "f" || message.c !== options.teamId || message.d !== options.documentId || typeof snapshot?.v !== "number" || snapshot.type !== JSON1_URI) return;
2737
3006
  const current = asWikiSnapshot(snapshot.data);
2738
- let next;
2739
- let operation;
2740
3007
  try {
2741
- next = asJsonDocument(update(structuredClone(current)), "update");
2742
- operation = createTopLevelJson1Operation(current, next);
3008
+ pendingOperations = createTopLevelJson1Operations(current, asJsonDocument(update(structuredClone(current)), "update"));
2743
3009
  } catch (error) {
2744
3010
  fail(error instanceof Error ? error : /* @__PURE__ */ new Error("ONES Wiki collaboration update failed"));
2745
3011
  return;
2746
3012
  }
2747
3013
  snapshotVersion = snapshot.v;
2748
- if (operation === null) {
3014
+ currentVersion = snapshotVersion;
3015
+ state = "presence";
3016
+ send({
3017
+ a: "p",
3018
+ ch: presenceChannel,
3019
+ id: presenceId,
3020
+ p: null,
3021
+ pv: 2
3022
+ });
3023
+ send({
3024
+ a: "ps",
3025
+ ch: presenceChannel,
3026
+ seq: 1
3027
+ });
3028
+ return;
3029
+ }
3030
+ if (state === "presence") {
3031
+ if (message.a !== "ps" || message.ch !== presenceChannel || message.seq !== 1) return;
3032
+ state = "subscribe";
3033
+ send({
3034
+ a: "s",
3035
+ c: options.teamId,
3036
+ d: options.documentId,
3037
+ v: snapshotVersion
3038
+ });
3039
+ return;
3040
+ }
3041
+ if (state === "subscribe") {
3042
+ if (message.a !== "s" || message.c !== options.teamId || message.d !== options.documentId) return;
3043
+ if (!pendingOperations.length) {
2749
3044
  finish({
2750
3045
  snapshotVersion,
2751
3046
  version: snapshotVersion,
@@ -2754,21 +3049,23 @@ async function replaceOnesWikiDocument(options, update, dependencies = {
2754
3049
  return;
2755
3050
  }
2756
3051
  state = "ack";
2757
- send({
2758
- a: "op",
2759
- c: options.teamId,
2760
- d: options.documentId,
2761
- v: snapshotVersion,
2762
- seq: sequence,
2763
- op: operation
2764
- });
3052
+ sendNextOperation();
2765
3053
  return;
2766
3054
  }
2767
- if (state === "ack" && message.a === "op" && message.c === options.teamId && message.d === options.documentId && message.seq === sequence) finish({
2768
- snapshotVersion,
2769
- version: typeof message.v === "number" ? message.v : snapshotVersion + 1,
2770
- changed: true
2771
- });
3055
+ if (state === "ack" && message.a === "op" && message.c === options.teamId && message.d === options.documentId && message.seq === sequence) {
3056
+ currentVersion = typeof message.v === "number" ? message.v : currentVersion + 1;
3057
+ pendingOperations.shift();
3058
+ if (!pendingOperations.length) {
3059
+ finish({
3060
+ snapshotVersion,
3061
+ version: currentVersion,
3062
+ changed: true
3063
+ });
3064
+ return;
3065
+ }
3066
+ sequence += 1;
3067
+ sendNextOperation();
3068
+ }
2772
3069
  });
2773
3070
  });
2774
3071
  }
@@ -2806,6 +3103,10 @@ var OnesWikiProductWriter = class {
2806
3103
  const encodedTeamId = encodeIdentifier(teamId, "team UUID");
2807
3104
  const headers = new Headers(init.headers);
2808
3105
  headers.set("Authorization", `Bearer ${session.accessToken}`);
3106
+ headers.set("Ones-Auth-Token", session.legacyAuthToken);
3107
+ headers.set("Ones-User-Id", session.legacyUserId);
3108
+ headers.set("Referer", this.options.apiBase);
3109
+ if (session.cookieHeader) headers.set("Cookie", session.cookieHeader);
2809
3110
  if (typeof init.body === "string" && !headers.has("Content-Type")) headers.set("Content-Type", "application/json;charset=UTF-8");
2810
3111
  const response = await fetch(`${this.options.apiBase}/wiki/api/wiki/team/${encodedTeamId}${apiPath}`, {
2811
3112
  ...init,
@@ -2828,9 +3129,9 @@ var OnesWikiProductWriter = class {
2828
3129
  }
2829
3130
  throw new Error("ONES Wiki editor token endpoint was not found");
2830
3131
  }
2831
- async replaceDocument(teamId, resourceId, documentId, update, preferDraft = false) {
3132
+ async replaceDocument(teamId, resourceId, documentId, update, preferDraft = false, editorTokenOverride) {
2832
3133
  const session = await this.options.getSession();
2833
- const editorToken = await this.fetchEditorToken(teamId, resourceId, preferDraft);
3134
+ const editorToken = editorTokenOverride ?? await this.fetchEditorToken(teamId, resourceId, preferDraft);
2834
3135
  return { version: (await replaceOnesWikiDocument({
2835
3136
  baseUrl: this.options.apiBase,
2836
3137
  teamId,
@@ -2838,6 +3139,7 @@ var OnesWikiProductWriter = class {
2838
3139
  accessToken: session.accessToken,
2839
3140
  editorToken,
2840
3141
  userId: session.userUuid,
3142
+ cookieHeader: session.cookieHeader,
2841
3143
  displayName: session.userName
2842
3144
  }, update)).version };
2843
3145
  }
@@ -2891,20 +3193,120 @@ var OnesWikiProductWriter = class {
2891
3193
  };
2892
3194
  }
2893
3195
  async update(params) {
2894
- const detail = await this.options.fetchPageDetail(params.pageId, params.teamId);
2895
- const documentId = firstNonEmptyString(detail.ref_uuid);
2896
- if (!documentId) throw new Error("ONES Wiki page detail did not include a collaborative document ID");
2897
- const write = await this.replaceDocument(params.teamId, params.pageId, documentId, (snapshot) => {
3196
+ const encodedPageId = encodeIdentifier(params.pageId, "Wiki page UUID");
3197
+ const encodedSpaceId = params.spaceId ? encodeIdentifier(params.spaceId, "Wiki space UUID") : null;
3198
+ let detail = await this.request(params.teamId, `/page/${encodedPageId}?action=edit`);
3199
+ const title = firstNonEmptyString(detail.title, detail.name, detail.page_title) ?? `Wiki ${params.pageId}`;
3200
+ if (detail.ref_type === 6 && detail.ref_uuid) {
3201
+ const editorToken = await this.fetchEditorToken(params.teamId, params.pageId);
3202
+ const write = await this.replaceDocument(params.teamId, params.pageId, detail.ref_uuid, (snapshot) => {
3203
+ return parseWikiDocument(applyWikiUpdateOperation(JSON.stringify(snapshot), params.operation));
3204
+ }, false, editorToken);
3205
+ const published = await this.request(params.teamId, `/online_page/${encodedPageId}/publish`, {
3206
+ method: "POST",
3207
+ body: JSON.stringify({ title })
3208
+ });
3209
+ if (params.spaceId) this.options.invalidateTree(params.teamId, params.spaceId);
3210
+ return {
3211
+ pageId: params.pageId,
3212
+ title,
3213
+ version: String(published.version ?? published.updated_time ?? write.version),
3214
+ url: params.spaceId ? `${this.options.apiBase}/wiki/#/team/${encodeURIComponent(params.teamId)}/space/${encodeURIComponent(params.spaceId)}/page/${encodeURIComponent(params.pageId)}` : null
3215
+ };
3216
+ }
3217
+ let draftId = firstNonEmptyString(detail.online_draft_uuid, detail.onlineDraftUuid, detail.online_draft?.uuid, detail.draft_uuid, detail.draftUuid, detail.draft?.uuid);
3218
+ 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);
3219
+ if (!draftId) {
3220
+ if (!encodedSpaceId) throw new Error("ONES Wiki page space is required to create an edit draft");
3221
+ try {
3222
+ const createdDraft = await this.request(params.teamId, `/space/${encodedSpaceId}/drafts/add`, {
3223
+ method: "POST",
3224
+ body: JSON.stringify({
3225
+ page_uuid: params.pageId,
3226
+ status: 2,
3227
+ title
3228
+ })
3229
+ });
3230
+ draftId = firstNonEmptyString(createdDraft.draft_uuid, createdDraft.draftUuid, createdDraft.uuid, createdDraft.id);
3231
+ documentId = firstNonEmptyString(createdDraft.ref_uuid, createdDraft.refUuid, documentId);
3232
+ } catch (error) {
3233
+ const refreshedDetail = await this.request(params.teamId, `/space/${encodedSpaceId}/page/${encodedPageId}`);
3234
+ draftId = firstNonEmptyString(refreshedDetail.draft_uuid, refreshedDetail.draftUuid);
3235
+ if (!draftId) throw error;
3236
+ detail = refreshedDetail;
3237
+ documentId = firstNonEmptyString(refreshedDetail.draft_ref_uuid, refreshedDetail.draftRefUuid, refreshedDetail.ref_uuid, documentId);
3238
+ }
3239
+ }
3240
+ if (!draftId || !encodedSpaceId) throw new Error("ONES Wiki edit draft did not include the required resource and space IDs");
3241
+ const encodedDraftId = encodeIdentifier(draftId, "Wiki draft UUID");
3242
+ const draftDetail = await this.request(params.teamId, `/space/${encodedSpaceId}/draft/${encodedDraftId}`);
3243
+ documentId = firstNonEmptyString(draftDetail.ref_uuid, draftDetail.refUuid, documentId);
3244
+ if (!documentId) throw new Error("ONES Wiki edit draft did not include a collaborative document ID");
3245
+ if (typeof draftDetail.content === "string") {
3246
+ let content;
3247
+ if (params.operation.type === "replace_document") content = markdownToWikiHtml(params.operation.markdown);
3248
+ else if (params.operation.type === "append_blocks") content = `${draftDetail.content}\n${markdownToWikiHtml(params.operation.markdown)}`;
3249
+ else if (params.operation.type === "replace_text") {
3250
+ const occurrences = draftDetail.content.split(params.operation.find).length - 1;
3251
+ if (occurrences !== 1) throw new Error(`ONES Wiki draft replace_text requires exactly one match; found ${occurrences}`);
3252
+ content = draftDetail.content.replace(params.operation.find, params.operation.replace);
3253
+ } else throw new Error("ONES Wiki append_table_row is not supported for legacy page drafts");
3254
+ const published = await this.request(params.teamId, `/space/${encodedSpaceId}/draft/${encodedDraftId}/update`, {
3255
+ method: "POST",
3256
+ body: JSON.stringify({
3257
+ ...draftDetail,
3258
+ content,
3259
+ title,
3260
+ page_uuid: params.pageId,
3261
+ space_uuid: params.spaceId,
3262
+ from_version: detail.version ?? draftDetail.from_version,
3263
+ is_published: true,
3264
+ is_forced: true
3265
+ })
3266
+ });
3267
+ this.options.invalidateTree(params.teamId, params.spaceId);
3268
+ const publishedVersion = published.version ?? published.updated_time ?? draftDetail.version ?? draftDetail.updated_time;
3269
+ return {
3270
+ pageId: params.pageId,
3271
+ title,
3272
+ version: publishedVersion === void 0 ? null : String(publishedVersion),
3273
+ url: `${this.options.apiBase}/wiki/#/team/${encodeURIComponent(params.teamId)}/space/${encodeURIComponent(params.spaceId)}/page/${encodeURIComponent(params.pageId)}`
3274
+ };
3275
+ }
3276
+ const write = await this.replaceDocument(params.teamId, draftId, documentId, (snapshot) => {
2898
3277
  return parseWikiDocument(applyWikiUpdateOperation(JSON.stringify(snapshot), params.operation));
3278
+ }, true);
3279
+ const refreshedDraft = await this.request(params.teamId, `/space/${encodedSpaceId}/draft/${encodedDraftId}`);
3280
+ await this.request(params.teamId, `/space/${encodedSpaceId}/draft/${encodedDraftId}/update`, {
3281
+ method: "POST",
3282
+ body: JSON.stringify({
3283
+ ...refreshedDraft,
3284
+ title,
3285
+ page_uuid: params.pageId,
3286
+ space_uuid: params.spaceId,
3287
+ from_version: detail.version ?? refreshedDraft.from_version,
3288
+ is_published: true,
3289
+ is_forced: true
3290
+ })
2899
3291
  });
2900
3292
  if (params.spaceId) this.options.invalidateTree(params.teamId, params.spaceId);
2901
3293
  return {
2902
3294
  pageId: params.pageId,
2903
- title: firstNonEmptyString(detail.title, detail.name, detail.page_title) ?? `Wiki ${params.pageId}`,
3295
+ title,
2904
3296
  version: String(write.version),
2905
3297
  url: params.spaceId ? `${this.options.apiBase}/wiki/#/team/${encodeURIComponent(params.teamId)}/space/${encodeURIComponent(params.spaceId)}/page/${encodeURIComponent(params.pageId)}` : null
2906
3298
  };
2907
3299
  }
3300
+ async delete(params) {
3301
+ const encodedSpaceId = encodeIdentifier(params.spaceId, "Wiki space UUID");
3302
+ const encodedPageId = encodeIdentifier(params.pageId, "Wiki page UUID");
3303
+ await this.request(params.teamId, `/space/${encodedSpaceId}/page/${encodedPageId}/delete`, { method: "POST" });
3304
+ this.options.invalidateTree(params.teamId, params.spaceId);
3305
+ return {
3306
+ pageId: params.pageId,
3307
+ deleted: true
3308
+ };
3309
+ }
2908
3310
  };
2909
3311
  //#endregion
2910
3312
  //#region ../../src/adapters/ones/task-query.ts
@@ -2991,7 +3393,6 @@ var OnesTaskAdapter = class extends BaseAdapter {
2991
3393
  this.wikiProductWriter = new OnesWikiProductWriter({
2992
3394
  apiBase: config.apiBase,
2993
3395
  getSession: () => this.login(),
2994
- fetchPageDetail: (pageId, teamId) => this.wikiReader.fetchPageDetail(pageId, teamId, true),
2995
3396
  invalidateTree: (teamId, spaceId) => this.wikiReader.invalidateTree(teamId, spaceId)
2996
3397
  });
2997
3398
  this.content = new OnesTaskContent({
@@ -3007,7 +3408,10 @@ var OnesTaskAdapter = class extends BaseAdapter {
3007
3408
  this.planning = new OnesTaskPlanning({
3008
3409
  api: this.api,
3009
3410
  getRequirement: (id) => this.getRequirement({ id }),
3010
- fetchTaskInfo: (taskUuid) => this.fetchTaskInfo(taskUuid)
3411
+ fetchTaskInfo: (taskUuid) => this.fetchTaskInfo(taskUuid),
3412
+ resolveProjectIdentifier: async (projectUuid) => {
3413
+ return (await this.fetchProjects()).find((candidate) => candidate.uuid === projectUuid)?.identifier?.toUpperCase() ?? null;
3414
+ }
3011
3415
  });
3012
3416
  this.issueReader = new OnesIssueReader({
3013
3417
  api: this.api,
@@ -3225,6 +3629,9 @@ var OnesTaskAdapter = class extends BaseAdapter {
3225
3629
  async updateWikiPage(params) {
3226
3630
  return this.wikiProductWriter.update(params);
3227
3631
  }
3632
+ async deleteWikiPage(params) {
3633
+ return this.wikiProductWriter.delete(params);
3634
+ }
3228
3635
  /**
3229
3636
  * Fetch a work item by UUID, number, display id, or wiki URL.
3230
3637
  * Routes by issueType.detailType: requirements (1 and 5) load wiki docs;
@@ -3967,7 +4374,7 @@ function formatWorkItem(req) {
3967
4374
  //#endregion
3968
4375
  //#region ../../src/tools/list-pending-work-items.ts
3969
4376
  const ListPendingWorkItemsSchema = z.object({ source: z.string().optional().describe("Source to read. If omitted, uses the default source.") });
3970
- function resolveAdapter$3(source, adapters, defaultSource) {
4377
+ function resolveAdapter$4(source, adapters, defaultSource) {
3971
4378
  const sourceType = source ?? defaultSource;
3972
4379
  if (!sourceType) throw new Error("No source specified and no default source configured");
3973
4380
  const adapter = adapters.get(sourceType);
@@ -4011,7 +4418,7 @@ function formatResult(result) {
4011
4418
  return lines.join("\n");
4012
4419
  }
4013
4420
  async function handleListPendingWorkItems(input, adapters, defaultSource) {
4014
- const result = await resolveAdapter$3(input.source, adapters, defaultSource).listPendingWorkItems();
4421
+ const result = await resolveAdapter$4(input.source, adapters, defaultSource).listPendingWorkItems();
4015
4422
  const safeResult = {
4016
4423
  ...result,
4017
4424
  items: result.items.map(sanitizeItem)
@@ -4125,7 +4532,7 @@ var RequirementDecompositionApprovalStore = class {
4125
4532
  return record;
4126
4533
  }
4127
4534
  };
4128
- function resolveAdapter$2(source, adapters, defaultSource) {
4535
+ function resolveAdapter$3(source, adapters, defaultSource) {
4129
4536
  const sourceType = source ?? defaultSource;
4130
4537
  if (!sourceType) throw new Error("No source specified and no default source configured");
4131
4538
  const adapter = adapters.get(sourceType);
@@ -4228,7 +4635,7 @@ function buildOperations(displayId, tasks) {
4228
4635
  });
4229
4636
  }
4230
4637
  async function handleInspectRequirementDecomposition(input, adapters, defaultSource) {
4231
- const { adapter } = resolveAdapter$2(input.source, adapters, defaultSource);
4638
+ const { adapter } = resolveAdapter$3(input.source, adapters, defaultSource);
4232
4639
  const context = sanitizedContext(await adapter.getRequirementDecompositionContext({ requirementId: input.requirementId }));
4233
4640
  return {
4234
4641
  content: [{
@@ -4239,7 +4646,7 @@ async function handleInspectRequirementDecomposition(input, adapters, defaultSou
4239
4646
  };
4240
4647
  }
4241
4648
  async function handlePrepareRequirementDecomposition(input, adapters, approvals, defaultSource) {
4242
- const { sourceType, adapter } = resolveAdapter$2(input.source, adapters, defaultSource);
4649
+ const { sourceType, adapter } = resolveAdapter$3(input.source, adapters, defaultSource);
4243
4650
  const context = await adapter.getRequirementDecompositionContext({ requirementId: input.requirementId });
4244
4651
  if (context.requirement.workItemKind !== "requirement") throw new Error("Only requirements can be decomposed");
4245
4652
  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.");
@@ -4289,7 +4696,7 @@ async function handleApplyRequirementDecomposition(input, adapters, approvals, o
4289
4696
  if (!record) throw new Error("Approval token is invalid, expired, or already used. Prepare the decomposition again.");
4290
4697
  if ((input.source ?? options.defaultSource) !== record.source) throw new Error("Approval token source does not match the requested source");
4291
4698
  if (input.planHash !== record.planHash) throw new Error("Plan hash does not match the approved decomposition");
4292
- const { adapter } = resolveAdapter$2(record.source, adapters, options.defaultSource);
4699
+ const { adapter } = resolveAdapter$3(record.source, adapters, options.defaultSource);
4293
4700
  const current = await adapter.getRequirementDecompositionContext({ requirementId: record.requirementId });
4294
4701
  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.");
4295
4702
  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.");
@@ -4395,12 +4802,82 @@ function formatUpdateTaskPlanDatesResult(result) {
4395
4802
  return lines.join("\n");
4396
4803
  }
4397
4804
  //#endregion
4805
+ //#region ../../src/tools/wiki-delete.ts
4806
+ const SourceSchema$1 = z.string().trim().min(1).optional();
4807
+ const DeleteEmptyWikiDuplicatesSchema = z.object({
4808
+ keepPageId: z.string().trim().min(1),
4809
+ duplicatePageIds: z.array(z.string().trim().min(1)).min(1).max(20),
4810
+ expectedTitle: z.string().trim().min(1),
4811
+ confirmed: z.literal(true).describe("Set true only after the user confirms the exact keep and delete page IDs immediately before submission."),
4812
+ source: SourceSchema$1
4813
+ });
4814
+ const DeleteEmptyWikiDuplicatesOutputSchema = z.object({
4815
+ keptPageId: z.string(),
4816
+ deletedPageIds: z.array(z.string())
4817
+ });
4818
+ function resolveAdapter$2(source, adapters, defaultSource) {
4819
+ const sourceType = source ?? defaultSource;
4820
+ if (!sourceType) throw new Error("No source specified and no default source configured");
4821
+ const adapter = adapters.get(sourceType);
4822
+ if (!adapter) throw new Error(`Source "${sourceType}" is not configured`);
4823
+ return {
4824
+ sourceType,
4825
+ adapter
4826
+ };
4827
+ }
4828
+ function assertCleanupTarget(keep, duplicates, expectedTitle) {
4829
+ if (keep.title !== expectedTitle) throw new Error("The retained Wiki page title no longer matches the confirmed title");
4830
+ if (keep.content.trim() === `# ${keep.title}`) throw new Error("The retained Wiki page does not contain a body");
4831
+ for (const duplicate of duplicates) {
4832
+ if (duplicate.title !== expectedTitle) throw new Error(`Wiki duplicate ${duplicate.pageId} title no longer matches the confirmed title`);
4833
+ 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`);
4834
+ if (duplicate.content.trim() !== `# ${duplicate.title}`) throw new Error(`Wiki duplicate ${duplicate.pageId} is not empty and will not be deleted`);
4835
+ }
4836
+ }
4837
+ async function handleDeleteEmptyWikiDuplicates(input, adapters, options) {
4838
+ if (!options.writesEnabled) throw new Error("Wiki writes are disabled. Enable both ONES_WIKI_ENABLE_WRITES=true and source option wikiWrites=true.");
4839
+ const uniqueDuplicateIds = [...new Set(input.duplicatePageIds)];
4840
+ if (uniqueDuplicateIds.length !== input.duplicatePageIds.length) throw new Error("duplicatePageIds contains repeated page IDs");
4841
+ if (uniqueDuplicateIds.includes(input.keepPageId)) throw new Error("The retained Wiki page cannot also be deleted");
4842
+ const { adapter } = resolveAdapter$2(input.source, adapters, options.defaultSource);
4843
+ const [keep, ...duplicates] = await Promise.all([adapter.getWikiPage({ pageId: input.keepPageId }), ...uniqueDuplicateIds.map((pageId) => adapter.getWikiPage({ pageId }))]);
4844
+ assertCleanupTarget(keep, duplicates, input.expectedTitle);
4845
+ const spaceId = keep.spaceId;
4846
+ if (!spaceId) throw new Error("The retained Wiki page space could not be verified");
4847
+ const operationHash = crypto.createHash("sha256").update(JSON.stringify({
4848
+ keepPageId: input.keepPageId,
4849
+ duplicatePageIds: uniqueDuplicateIds,
4850
+ expectedTitle: input.expectedTitle,
4851
+ baselines: [keep, ...duplicates].map((page) => ({
4852
+ pageId: page.pageId,
4853
+ version: page.version,
4854
+ contentHash: page.contentHash
4855
+ }))
4856
+ })).digest("hex");
4857
+ for (const duplicate of duplicates) await adapter.deleteWikiPage({
4858
+ teamId: duplicate.teamId,
4859
+ spaceId,
4860
+ pageId: duplicate.pageId
4861
+ });
4862
+ const result = {
4863
+ keptPageId: keep.pageId,
4864
+ deletedPageIds: duplicates.map((page) => page.pageId)
4865
+ };
4866
+ return {
4867
+ content: [{
4868
+ type: "text",
4869
+ text: `Kept Wiki page ${result.keptPageId} and deleted empty duplicates: ${result.deletedPageIds.join(", ")}.\noperationHash: ${operationHash}`
4870
+ }],
4871
+ structuredContent: result
4872
+ };
4873
+ }
4874
+ //#endregion
4398
4875
  //#region ../../src/tools/wiki-read.ts
4399
4876
  const WikiSourceSchema = z.string().trim().min(1).optional();
4400
4877
  const GetOnesWikiPageSchema = z.object({
4401
4878
  pageId: z.string().trim().min(1).optional(),
4402
4879
  url: z.string().url().optional(),
4403
- path: z.union([z.string().trim().min(1), z.array(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."),
4880
+ path: z.union([z.string().trim().min(1), z.array(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."),
4404
4881
  teamId: z.string().trim().min(1).optional(),
4405
4882
  spaceId: z.string().trim().min(1).optional(),
4406
4883
  revealSensitiveSecrets: z.boolean().default(false).describe("Default false. Set true only when the user explicitly asks to reveal secrets."),
@@ -4502,6 +4979,19 @@ async function handleGetOnesWikiPage(input, adapters, defaultSource) {
4502
4979
  teamId: resolved?.teamId ?? input.teamId,
4503
4980
  spaceId: resolved?.spaceId ?? input.spaceId
4504
4981
  }), input.revealSensitiveSecrets);
4982
+ if (resolved) {
4983
+ page.title = resolved.title;
4984
+ page.breadcrumb = resolved.breadcrumb;
4985
+ const redactPrivateValues = resolved.redactPrivateValues;
4986
+ if (redactPrivateValues) {
4987
+ page.content = redactPrivateValues(page.content);
4988
+ page.attachments = page.attachments.map((attachment) => ({
4989
+ ...attachment,
4990
+ name: redactPrivateValues(attachment.name),
4991
+ url: redactPrivateValues(attachment.url)
4992
+ }));
4993
+ }
4994
+ }
4505
4995
  return {
4506
4996
  content: [{
4507
4997
  type: "text",
@@ -4649,7 +5139,7 @@ async function handleLookupEnvironmentAccess(input, adapters, defaultSource) {
4649
5139
  //#region ../../src/tools/wiki-write.ts
4650
5140
  const APPROVAL_TTL_MS = 1800 * 1e3;
4651
5141
  const SourceSchema = z.string().trim().min(1).optional();
4652
- const PathSchema = z.union([z.string().trim().min(1), z.array(z.string().trim().min(1)).min(1)]);
5142
+ const PathSchema = z.union([z.string().trim().min(1), z.array(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.");
4653
5143
  const PrepareWikiCreateSchema = z.object({
4654
5144
  parentPageId: z.string().trim().min(1).optional(),
4655
5145
  parentPath: PathSchema.optional(),
@@ -4673,6 +5163,10 @@ const ReplaceTextSchema = z.object({
4673
5163
  find: z.string().min(1),
4674
5164
  replace: z.string()
4675
5165
  });
5166
+ const ReplaceDocumentSchema = z.object({
5167
+ type: z.literal("replace_document"),
5168
+ markdown: z.string().trim().min(1)
5169
+ });
4676
5170
  const PrepareWikiUpdateSchema = z.object({
4677
5171
  pageId: z.string().trim().min(1).optional(),
4678
5172
  url: z.string().url().optional(),
@@ -4682,7 +5176,8 @@ const PrepareWikiUpdateSchema = z.object({
4682
5176
  operation: z.discriminatedUnion("type", [
4683
5177
  AppendBlocksSchema,
4684
5178
  AppendTableRowSchema,
4685
- ReplaceTextSchema
5179
+ ReplaceTextSchema,
5180
+ ReplaceDocumentSchema
4686
5181
  ]),
4687
5182
  source: SourceSchema
4688
5183
  }).refine((value) => [
@@ -4723,6 +5218,10 @@ const WikiUpdateOperationSchema = z.discriminatedUnion("type", [
4723
5218
  type: z.literal("replace_text"),
4724
5219
  find: z.string(),
4725
5220
  replace: z.string()
5221
+ }),
5222
+ z.object({
5223
+ type: z.literal("replace_document"),
5224
+ markdown: z.string()
4726
5225
  })
4727
5226
  ]);
4728
5227
  const WikiUpdateRequestSchema = z.object({
@@ -4852,11 +5351,24 @@ async function handlePrepareWikiCreate(input, adapters, approvals, defaultSource
4852
5351
  const teamId = resolved?.teamId ?? parent.teamId;
4853
5352
  const spaceId = resolved?.spaceId ?? parent.spaceId;
4854
5353
  if (!spaceId) throw new Error("The target space could not be verified");
5354
+ const title = input.title.trim();
5355
+ const titleCandidates = await adapter.searchWikiPages({
5356
+ query: title,
5357
+ teamId,
5358
+ spaceId,
5359
+ limit: 50
5360
+ });
5361
+ const siblingConflicts = (await Promise.all(titleCandidates.filter((candidate) => candidate.title === title).map((candidate) => adapter.getWikiPage({
5362
+ pageId: candidate.pageId,
5363
+ teamId: candidate.teamId,
5364
+ spaceId: candidate.spaceId ?? spaceId
5365
+ })))).filter((page) => page.parentPageId === parent.pageId);
5366
+ 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.`);
4855
5367
  const requestWithoutKey = {
4856
5368
  teamId,
4857
5369
  spaceId,
4858
5370
  parentPageId: parent.pageId,
4859
- title: input.title.trim(),
5371
+ title,
4860
5372
  markdown: input.markdown
4861
5373
  };
4862
5374
  const operationHash = hashOperation({
@@ -4878,7 +5390,7 @@ async function handlePrepareWikiCreate(input, adapters, approvals, defaultSource
4878
5390
  });
4879
5391
  const plan = {
4880
5392
  kind: "create",
4881
- targetBreadcrumb: [...parent.breadcrumb, input.title.trim()],
5393
+ targetBreadcrumb: [...resolved?.breadcrumb ?? parent.breadcrumb, input.title.trim()],
4882
5394
  request,
4883
5395
  parentBaseline: baseline(parent),
4884
5396
  operationHash,
@@ -4935,7 +5447,7 @@ async function handlePrepareWikiUpdate(input, adapters, approvals, defaultSource
4935
5447
  });
4936
5448
  const plan = {
4937
5449
  kind: "update",
4938
- targetBreadcrumb: page.breadcrumb,
5450
+ targetBreadcrumb: resolved?.breadcrumb ?? page.breadcrumb,
4939
5451
  request,
4940
5452
  operationHash,
4941
5453
  approvalToken: approval.token,
@@ -4945,7 +5457,7 @@ async function handlePrepareWikiUpdate(input, adapters, approvals, defaultSource
4945
5457
  content: [{
4946
5458
  type: "text",
4947
5459
  text: [
4948
- `Prepared Wiki update for ${page.breadcrumb.join(" / ") || page.title}.`,
5460
+ `Prepared Wiki update for ${plan.targetBreadcrumb.join(" / ") || page.title}.`,
4949
5461
  "No write was performed. Ask the user to confirm this exact operation immediately before apply.",
4950
5462
  `operationHash: ${plan.operationHash}`,
4951
5463
  `approvalToken: ${plan.approvalToken}`,
@@ -5058,7 +5570,7 @@ function createRequirementsServer(config, adapterOverrides) {
5058
5570
  });
5059
5571
  server.registerTool("get_ones_wiki_page", {
5060
5572
  title: "Get ONES Wiki Page",
5061
- 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.",
5573
+ 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.",
5062
5574
  inputSchema: GetOnesWikiPageSchema,
5063
5575
  annotations: {
5064
5576
  readOnlyHint: true,
@@ -5120,7 +5632,7 @@ function createRequirementsServer(config, adapterOverrides) {
5120
5632
  });
5121
5633
  server.registerTool("prepare_wiki_create", {
5122
5634
  title: "Prepare ONES Wiki Create",
5123
- description: "Resolve an exact parent page and prepare one exact Wiki create operation. Never writes. Returns a one-time 30-minute approval token.",
5635
+ 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.",
5124
5636
  inputSchema: PrepareWikiCreateSchema,
5125
5637
  outputSchema: PrepareWikiCreateOutputSchema,
5126
5638
  annotations: {
@@ -5159,7 +5671,7 @@ function createRequirementsServer(config, adapterOverrides) {
5159
5671
  });
5160
5672
  server.registerTool("prepare_wiki_update", {
5161
5673
  title: "Prepare ONES Wiki Update",
5162
- description: "Prepare an exact minimal Wiki update, including exact table-row targeting. Never writes. Ambiguous pages, tables, or text matches fail closed.",
5674
+ 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.",
5163
5675
  inputSchema: PrepareWikiUpdateSchema,
5164
5676
  outputSchema: PrepareWikiUpdateOutputSchema,
5165
5677
  annotations: {
@@ -5196,6 +5708,28 @@ function createRequirementsServer(config, adapterOverrides) {
5196
5708
  return toolError(err);
5197
5709
  }
5198
5710
  });
5711
+ server.registerTool("delete_empty_wiki_duplicates", {
5712
+ title: "Delete Empty ONES Wiki Duplicates",
5713
+ 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.",
5714
+ inputSchema: DeleteEmptyWikiDuplicatesSchema,
5715
+ outputSchema: DeleteEmptyWikiDuplicatesOutputSchema,
5716
+ annotations: {
5717
+ readOnlyHint: false,
5718
+ destructiveHint: true,
5719
+ idempotentHint: false,
5720
+ openWorldHint: true
5721
+ }
5722
+ }, async (params) => {
5723
+ try {
5724
+ const sourceType = params.source ?? defaultSource;
5725
+ return await handleDeleteEmptyWikiDuplicates(params, adapters, {
5726
+ defaultSource,
5727
+ writesEnabled: wikiWritesEnabled(sourceType)
5728
+ });
5729
+ } catch (err) {
5730
+ return toolError(err);
5731
+ }
5732
+ });
5199
5733
  server.registerTool("list_sources", {
5200
5734
  title: "List Sources",
5201
5735
  description: "List all configured requirement sources and their status",