ai-dev-requirements 0.4.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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.1";
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"),
@@ -788,7 +822,8 @@ function removeControlCharacters(value) {
788
822
  return output;
789
823
  }
790
824
  function sanitizeExternalText(value) {
791
- return removeControlCharacters(removeUrlCredentials(decodeHTML(value.slice(0, MAX_EXTERNAL_TEXT_CHARS).replace(/<(?:script|style|iframe|object|embed)\b[^>]*>[\s\S]*?<\/(?:script|style|iframe|object|embed)>/gi, "").replace(/<img\b[^>]*>/gi, "[Image omitted]").replace(/<br\s*\/?>/gi, "\n").replace(/<\/p\s*>/gi, "\n").replace(/<\/(?:td|th)\s*>/gi, " | ").replace(/<\/tr\s*>/gi, "\n").replace(/<[^>]+>/g, "")))).replace(/[ \t]+\n/g, "\n").replace(/\n[ \t]+/g, "\n").replace(/\n{3,}/g, "\n\n").trim();
825
+ const withoutActiveContent = value.slice(0, MAX_EXTERNAL_TEXT_CHARS).replace(/<(?:script|style|iframe|object|embed)\b[^>]*>[\s\S]*?<\/(?:script|style|iframe|object|embed)>/gi, "").replace(/<img\b[^>]*>/gi, "[Image omitted]").replace(/<br\s*\/?>/gi, "\n").replace(/<\/p\s*>/gi, "\n").replace(/<\/(?:td|th)\s*>/gi, " | ").replace(/<\/tr\s*>/gi, "\n").replace(/<[^>]+>/g, "");
826
+ return removeControlCharacters(removeUrlCredentials(decodeHTML(withoutActiveContent))).replace(/[ \t]+\n/g, "\n").replace(/\n[ \t]+/g, "\n").replace(/\n{3,}/g, "\n\n").trim();
792
827
  }
793
828
  function sanitizeExternalInline(value) {
794
829
  return sanitizeExternalText(value).replace(/\s+/g, " ").slice(0, MAX_EXTERNAL_INLINE_CHARS);
@@ -1166,17 +1201,43 @@ function renderWikiContent(content, context = { imageSources: [] }) {
1166
1201
  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
1202
  }
1168
1203
  function newWikiBlockId() {
1169
- return randomBytes(8).toString("base64url").slice(0, 10);
1204
+ for (;;) {
1205
+ const id = randomBytes(9).toString("base64url").replace(/[^a-z0-9]/gi, "").slice(0, 9);
1206
+ if (/^[a-z][a-z0-9]{8}$/i.test(id)) return id;
1207
+ }
1208
+ }
1209
+ function markdownTextRuns(text) {
1210
+ if (!text) return [];
1211
+ const runs = [];
1212
+ const inlinePattern = /\[([^\]]+)\]\((https?:\/\/[^)\s]+)\)|`([^`]+)`/g;
1213
+ let cursor = 0;
1214
+ for (const match of text.matchAll(inlinePattern)) {
1215
+ const index = match.index ?? 0;
1216
+ if (index > cursor) runs.push({ insert: text.slice(cursor, index) });
1217
+ if (match[1] && match[2]) runs.push({
1218
+ insert: match[1],
1219
+ attributes: { link: match[2] }
1220
+ });
1221
+ else if (match[3]) runs.push({
1222
+ insert: match[3],
1223
+ attributes: { "style-code": true }
1224
+ });
1225
+ cursor = index + match[0].length;
1226
+ }
1227
+ if (cursor < text.length) runs.push({ insert: text.slice(cursor) });
1228
+ return runs.length ? runs : [{ insert: text }];
1170
1229
  }
1171
1230
  function wikiTextBlock(text, options = {}) {
1172
1231
  return {
1173
1232
  id: newWikiBlockId(),
1174
1233
  type: options.list ? "list" : "text",
1175
- text: text ? [{ insert: text }] : [],
1234
+ text: markdownTextRuns(text),
1176
1235
  ...options.heading ? { heading: options.heading } : {},
1177
1236
  ...options.list ? {
1178
1237
  ordered: options.ordered ?? false,
1179
- level: 1
1238
+ level: options.level ?? 1,
1239
+ ...options.start === void 0 ? {} : { start: options.start },
1240
+ ...options.groupId ? { groupId: options.groupId } : {}
1180
1241
  } : {}
1181
1242
  };
1182
1243
  }
@@ -1187,6 +1248,9 @@ function isMarkdownSeparatorRow(line) {
1187
1248
  const cells = parseMarkdownRow(line);
1188
1249
  return cells.length > 0 && cells.every((cell) => /^:?-{3,}:?$/.test(cell));
1189
1250
  }
1251
+ function estimateWikiTableCellWidth(value) {
1252
+ return [...value].reduce((width, character) => width + (character.codePointAt(0) > 127 ? 14 : 7), 24);
1253
+ }
1190
1254
  function markdownToWikiDocument(markdown) {
1191
1255
  const document = {
1192
1256
  blocks: [],
@@ -1195,9 +1259,15 @@ function markdownToWikiDocument(markdown) {
1195
1259
  };
1196
1260
  const blocks = document.blocks;
1197
1261
  const lines = markdown.replace(/\r\n/g, "\n").split("\n");
1262
+ let activeList = null;
1198
1263
  for (let index = 0; index < lines.length; index += 1) {
1199
1264
  const line = lines[index];
1265
+ if (!line.trim()) {
1266
+ activeList = null;
1267
+ continue;
1268
+ }
1200
1269
  if (line.includes("|") && lines[index + 1] && isMarkdownSeparatorRow(lines[index + 1])) {
1270
+ activeList = null;
1201
1271
  const rows = [parseMarkdownRow(line)];
1202
1272
  index += 2;
1203
1273
  while (index < lines.length && lines[index].includes("|")) {
@@ -1215,34 +1285,120 @@ function markdownToWikiDocument(markdown) {
1215
1285
  blocks.push({
1216
1286
  id: newWikiBlockId(),
1217
1287
  type: "table",
1218
- rows: rows.length,
1219
1288
  cols: columnCount,
1289
+ rows: rows.length,
1290
+ colsWidth: Array.from({ length: columnCount }, (_, column) => Math.max(100, ...rows.map((row) => estimateWikiTableCellWidth(row[column] ?? "")))),
1220
1291
  children
1221
1292
  });
1222
1293
  continue;
1223
1294
  }
1224
1295
  const heading = line.match(/^(#{1,6})[ \t]/);
1225
1296
  if (heading?.[1]) {
1297
+ activeList = null;
1226
1298
  blocks.push(wikiTextBlock(line.slice(heading[0].length).trim(), { heading: heading[1].length }));
1227
1299
  continue;
1228
1300
  }
1229
1301
  const unordered = line.match(/^[ \t]*[-*+][ \t]/);
1230
1302
  if (unordered) {
1231
- blocks.push(wikiTextBlock(line.slice(unordered[0].length).trim(), { list: true }));
1303
+ const groupId = activeList && !activeList.ordered ? activeList.groupId : newWikiBlockId();
1304
+ const start = activeList && !activeList.ordered ? activeList.nextStart : 1;
1305
+ activeList = {
1306
+ ordered: false,
1307
+ groupId,
1308
+ nextStart: start + 1
1309
+ };
1310
+ blocks.push(wikiTextBlock(line.slice(unordered[0].length).trim(), {
1311
+ list: true,
1312
+ groupId,
1313
+ start
1314
+ }));
1232
1315
  continue;
1233
1316
  }
1234
- const ordered = line.match(/^[ \t]*\d+[.)][ \t]/);
1235
- if (ordered) {
1317
+ const ordered = line.match(/^[ \t]*(\d+)[.)][ \t]/);
1318
+ if (ordered?.[1]) {
1319
+ const groupId = activeList?.ordered ? activeList.groupId : newWikiBlockId();
1320
+ const start = Number.parseInt(ordered[1], 10);
1321
+ activeList = {
1322
+ ordered: true,
1323
+ groupId,
1324
+ nextStart: start + 1
1325
+ };
1236
1326
  blocks.push(wikiTextBlock(line.slice(ordered[0].length).trim(), {
1237
1327
  list: true,
1238
- ordered: true
1328
+ ordered: true,
1329
+ start,
1330
+ groupId
1239
1331
  }));
1240
1332
  continue;
1241
1333
  }
1334
+ activeList = null;
1242
1335
  blocks.push(wikiTextBlock(line));
1243
1336
  }
1244
1337
  return document;
1245
1338
  }
1339
+ function markdownToWikiHtml(markdown) {
1340
+ const renderInline = (text) => markdownTextRuns(text).map((run) => {
1341
+ const escaped = escapeWikiHtml(run.insert);
1342
+ const content = run.attributes?.["style-code"] ? `<code>${escaped}</code>` : escaped;
1343
+ return run.attributes?.link ? `<a href="${escapeWikiHtml(run.attributes.link)}" target="_blank" rel="noopener noreferrer">${content}</a>` : content;
1344
+ }).join("");
1345
+ const lines = markdown.replace(/\r\n/g, "\n").split("\n");
1346
+ const html = [];
1347
+ for (let index = 0; index < lines.length; index += 1) {
1348
+ const line = lines[index];
1349
+ if (!line.trim()) continue;
1350
+ if (line.includes("|") && lines[index + 1] && isMarkdownSeparatorRow(lines[index + 1])) {
1351
+ const rows = [parseMarkdownRow(line)];
1352
+ index += 2;
1353
+ while (index < lines.length && lines[index].includes("|")) {
1354
+ rows.push(parseMarkdownRow(lines[index]));
1355
+ index += 1;
1356
+ }
1357
+ index -= 1;
1358
+ const columnCount = Math.max(...rows.map((row) => row.length));
1359
+ const width = Math.floor(100 / columnCount);
1360
+ const header = Array.from({ length: columnCount }).map((_, column) => `<th style="width:${width}%">${renderInline(rows[0][column] ?? "")}</th>`).join("");
1361
+ const body = rows.slice(1).map((row) => `<tr>${Array.from({ length: columnCount }).map((_, column) => `<td>${renderInline(row[column] ?? "")}</td>`).join("")}</tr>`).join("");
1362
+ html.push(`<table style="width:100%"><thead><tr>${header}</tr></thead><tbody>${body}</tbody></table>`);
1363
+ continue;
1364
+ }
1365
+ const heading = line.match(/^(#{1,6})[ \t]/);
1366
+ if (heading?.[1]) {
1367
+ const level = heading[1].length;
1368
+ html.push(`<h${level}>${renderInline(line.slice(heading[0].length).trim())}</h${level}>`);
1369
+ continue;
1370
+ }
1371
+ if (line.match(/^[ \t]*(\d+)[.)][ \t]/)?.[1]) {
1372
+ const items = [];
1373
+ while (index < lines.length) {
1374
+ const item = lines[index].match(/^[ \t]*(\d+)[.)][ \t]/);
1375
+ if (!item?.[1]) break;
1376
+ items.push({
1377
+ value: Number.parseInt(item[1], 10),
1378
+ text: lines[index].slice(item[0].length).trim()
1379
+ });
1380
+ index += 1;
1381
+ }
1382
+ index -= 1;
1383
+ html.push(`<ol start="${items[0].value}">${items.map((item) => `<li>${renderInline(item.text)}</li>`).join("")}</ol>`);
1384
+ continue;
1385
+ }
1386
+ if (line.match(/^[ \t]*[-*+][ \t]/)) {
1387
+ const items = [];
1388
+ while (index < lines.length) {
1389
+ const item = lines[index].match(/^[ \t]*[-*+][ \t]/);
1390
+ if (!item) break;
1391
+ items.push(lines[index].slice(item[0].length).trim());
1392
+ index += 1;
1393
+ }
1394
+ index -= 1;
1395
+ html.push(`<ul>${items.map((item) => `<li>${renderInline(item)}</li>`).join("")}</ul>`);
1396
+ continue;
1397
+ }
1398
+ html.push(`<p>${renderInline(line.trim())}</p>`);
1399
+ }
1400
+ return html.join("\n");
1401
+ }
1246
1402
  function parseWikiDocument(content) {
1247
1403
  const document = parseJsonRecord(content);
1248
1404
  if (!document || !Array.isArray(document.blocks)) throw new Error("ONES: Wiki content is not a supported collaborative document");
@@ -1299,6 +1455,17 @@ function appendWikiTableRow(document, operation) {
1299
1455
  table.rows = (typeof table.rows === "number" ? table.rows : layout.rows.length) + 1;
1300
1456
  }
1301
1457
  function applyWikiUpdateOperation(content, operation) {
1458
+ if (operation.type === "replace_document") {
1459
+ const current = parseWikiDocument(content);
1460
+ const replacement = markdownToWikiDocument(operation.markdown);
1461
+ for (const key of [
1462
+ "comments",
1463
+ "meta",
1464
+ "authors",
1465
+ "commentators"
1466
+ ]) if (Object.hasOwn(current, key)) replacement[key] = current[key];
1467
+ return JSON.stringify(replacement);
1468
+ }
1302
1469
  const document = parseWikiDocument(content);
1303
1470
  if (operation.type === "append_blocks") appendWikiDocument(document, markdownToWikiDocument(operation.markdown));
1304
1471
  else if (operation.type === "replace_text") replaceWikiText(document, operation.find, operation.replace);
@@ -1337,6 +1504,30 @@ var WikiPathResolutionError = class extends Error {
1337
1504
  };
1338
1505
  //#endregion
1339
1506
  //#region ../../src/adapters/ones/wiki-reader.ts
1507
+ function isWikiAttachmentPath(source) {
1508
+ if (/^[a-z][a-z\d+.-]*:/i.test(source)) return false;
1509
+ return source.split("/").every((part) => Boolean(part) && part !== "." && part !== ".." && !part.includes("\\"));
1510
+ }
1511
+ const CURRENT_USER_PATH_ALIASES = {
1512
+ "i": true,
1513
+ "me": true,
1514
+ "mine": true,
1515
+ "my": true,
1516
+ "myself": true,
1517
+ "current user": true,
1518
+ "current-user": true,
1519
+ "current_user": true,
1520
+ "我": true,
1521
+ "我的": true,
1522
+ "本人": true,
1523
+ "当前用户": true,
1524
+ "当前账号": true,
1525
+ "自己": true
1526
+ };
1527
+ function currentUserPathAlias(segment) {
1528
+ const trimmed = segment.trim();
1529
+ return CURRENT_USER_PATH_ALIASES[trimmed.toLocaleLowerCase()] ? trimmed : null;
1530
+ }
1340
1531
  var OnesWikiOpenApiError = class extends Error {
1341
1532
  status;
1342
1533
  reason;
@@ -1431,12 +1622,29 @@ function segmentSimilarity(left, right) {
1431
1622
  var OnesWikiReader = class {
1432
1623
  options;
1433
1624
  treeCache = /* @__PURE__ */ new Map();
1625
+ currentUserNameRequest = null;
1434
1626
  constructor(options) {
1435
1627
  this.options = options;
1436
1628
  }
1437
1629
  invalidateTree(teamId, spaceId) {
1438
1630
  this.treeCache.delete(`${teamId}:${spaceId}`);
1439
1631
  }
1632
+ async fetchCurrentUserName() {
1633
+ const session = await this.options.getSession();
1634
+ const response = await fetch(new URL("/wiki/api/project/auth/token_info", this.options.apiBase).toString(), { headers: { Authorization: `Bearer ${session.accessToken}` } });
1635
+ if (!response.ok) throw new Error(`ONES Wiki token info error: ${response.status}`);
1636
+ const payload = await response.json();
1637
+ const name = firstNonEmptyString$1(payload.user?.name, payload.data?.user?.name, payload.data?.name, payload.name, payload.user_name, payload.userName);
1638
+ if (!name) throw new Error("ONES Wiki token info did not include a resolvable current account");
1639
+ return name;
1640
+ }
1641
+ currentUserName() {
1642
+ if (!this.currentUserNameRequest) this.currentUserNameRequest = this.fetchCurrentUserName().catch((error) => {
1643
+ this.currentUserNameRequest = null;
1644
+ throw error;
1645
+ });
1646
+ return this.currentUserNameRequest;
1647
+ }
1440
1648
  async openApi(apiPath) {
1441
1649
  if (!apiPath.startsWith("/wiki/")) throw new Error("ONES: Invalid Wiki Open API path");
1442
1650
  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");
@@ -1462,9 +1670,8 @@ var OnesWikiReader = class {
1462
1670
  }
1463
1671
  buildImageUrl(session, refUuid, source, token, teamUuid) {
1464
1672
  const encodedRefUuid = encodeIdentifier$1(refUuid, "wiki reference UUID");
1465
- const sourceParts = source.split("/");
1466
- if (sourceParts.some((part) => !part || part === "." || part === ".." || part.includes("\\"))) throw new Error("ONES: Invalid wiki attachment path");
1467
- const encodedSource = sourceParts.map((part) => encodeURIComponent(part)).join("/");
1673
+ if (!isWikiAttachmentPath(source)) throw new Error("ONES: Invalid wiki attachment path");
1674
+ const encodedSource = source.split("/").map((part) => encodeURIComponent(part)).join("/");
1468
1675
  const encodedTeamUuid = encodeIdentifier$1(teamUuid ?? session.teamUuid, "team UUID");
1469
1676
  return `${this.options.apiBase}/wiki/api/wiki/editor/${encodedTeamUuid}/${encodedRefUuid}/resources/${encodedSource}?token=${encodeURIComponent(token)}`;
1470
1677
  }
@@ -1482,7 +1689,8 @@ var OnesWikiReader = class {
1482
1689
  const context = { imageSources: [] };
1483
1690
  const content = renderWikiContent(typeof data.content === "string" ? data.content : "", context);
1484
1691
  const token = typeof data.token === "string" ? data.token : "";
1485
- if (!context.imageSources.length || !token) return {
1692
+ const attachmentSources = context.imageSources.filter(isWikiAttachmentPath);
1693
+ if (!attachmentSources.length || !token) return {
1486
1694
  content,
1487
1695
  attachments: []
1488
1696
  };
@@ -1494,7 +1702,7 @@ var OnesWikiReader = class {
1494
1702
  };
1495
1703
  return {
1496
1704
  content,
1497
- attachments: context.imageSources.map((source, index) => ({
1705
+ attachments: attachmentSources.map((source, index) => ({
1498
1706
  id: `${wikiUuid}-image-${index + 1}`,
1499
1707
  name: attachmentNameFromPath(source),
1500
1708
  url: this.buildImageUrl(session, refUuid, source, token, wikiTeamUuid),
@@ -1696,8 +1904,19 @@ var OnesWikiReader = class {
1696
1904
  return score >= MIN_PATH_MATCH_SIMILARITY ? score : null;
1697
1905
  }
1698
1906
  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");
1907
+ const requestedPath = params.path.map((segment) => segment.trim()).filter(Boolean);
1908
+ if (!requestedPath.length) throw new Error("ONES: Wiki path is empty");
1909
+ const currentUserAlias = requestedPath.map(currentUserPathAlias).find((alias) => alias !== null) ?? null;
1910
+ const currentUserName = currentUserAlias ? await this.currentUserName() : null;
1911
+ const path = currentUserName ? requestedPath.map((segment) => currentUserPathAlias(segment) ? currentUserName : segment) : requestedPath;
1912
+ const redactCurrentUser = (page) => {
1913
+ if (!currentUserName || !currentUserAlias) return page;
1914
+ return {
1915
+ ...page,
1916
+ title: page.title === currentUserName ? currentUserAlias : page.title,
1917
+ breadcrumb: page.breadcrumb.map((segment) => segment === currentUserName ? currentUserAlias : segment)
1918
+ };
1919
+ };
1701
1920
  const candidates = await this.search({
1702
1921
  query: path.at(-1),
1703
1922
  teamId: params.teamId,
@@ -1744,26 +1963,32 @@ var OnesWikiReader = class {
1744
1963
  });
1745
1964
  }
1746
1965
  }
1747
- if (matches.length > 1) throw new WikiPathResolutionError("ambiguous", path, matches);
1966
+ if (matches.length > 1) throw new WikiPathResolutionError("ambiguous", requestedPath, matches.map(redactCurrentUser));
1748
1967
  let match = matches[0];
1749
1968
  if (!match && fuzzy.length) {
1750
1969
  fuzzy.sort((left, right) => right.score - left.score);
1751
1970
  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));
1971
+ if (competing.length > 1) throw new WikiPathResolutionError("ambiguous", requestedPath, competing.map((candidate) => redactCurrentUser(candidate.page)));
1753
1972
  match = fuzzy[0].page;
1754
1973
  }
1755
1974
  if (!match) {
1756
1975
  const inspectedIds = new Set(inspected.map((page) => page.pageId));
1757
- throw new WikiPathResolutionError("not_found", path, [...inspected, ...candidates.filter((page) => !inspectedIds.has(page.pageId))]);
1976
+ throw new WikiPathResolutionError("not_found", requestedPath, [...inspected, ...candidates.filter((page) => !inspectedIds.has(page.pageId))].map(redactCurrentUser));
1758
1977
  }
1759
1978
  if (!match.spaceId) throw new Error("ONES: Wiki space ID could not be verified");
1760
- return {
1979
+ const publicMatch = redactCurrentUser(match);
1980
+ const resolution = {
1761
1981
  teamId: match.teamId,
1762
1982
  spaceId: match.spaceId,
1763
1983
  pageId: match.pageId,
1764
- title: match.title,
1765
- breadcrumb: match.breadcrumb
1984
+ title: publicMatch.title,
1985
+ breadcrumb: publicMatch.breadcrumb
1766
1986
  };
1987
+ if (currentUserName && currentUserAlias) Object.defineProperty(resolution, "redactPrivateValues", {
1988
+ enumerable: false,
1989
+ value: (value) => value.split(currentUserName).join(currentUserAlias)
1990
+ });
1991
+ return resolution;
1767
1992
  }
1768
1993
  };
1769
1994
  //#endregion
@@ -1965,8 +2190,9 @@ var OnesTaskContent = class {
1965
2190
  attachments: rendered.attachments
1966
2191
  };
1967
2192
  })), containsInlineTaskImages(task) ? this.getTaskImageAttachments(task) : Promise.resolve([])]);
2193
+ const projectIdentifier = task.project?.identifier?.toUpperCase() ?? null;
1968
2194
  const parts = [
1969
- `# #${task.number} ${task.name}`,
2195
+ `# ${taskDisplayId({}, task, projectIdentifier)} ${task.name}`,
1970
2196
  "",
1971
2197
  `- **Type**: ${task.issueType?.name ?? "Unknown"}`,
1972
2198
  "- **Work Item Kind**: requirement",
@@ -1978,7 +2204,7 @@ var OnesTaskContent = class {
1978
2204
  parts.push(`- **UUID**: ${task.uuid}`);
1979
2205
  if (task.relatedTasks?.length) {
1980
2206
  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"}`);
2207
+ 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
2208
  }
1983
2209
  if (relatedActivities.length) {
1984
2210
  parts.push("", "## Related Work Items");
@@ -1993,7 +2219,7 @@ var OnesTaskContent = class {
1993
2219
  }
1994
2220
  if (task.parent?.uuid) {
1995
2221
  parts.push("", "## Parent Task", `- UUID: ${task.parent.uuid}`);
1996
- if (task.parent.number) parts.push(`- Number: #${task.parent.number}`);
2222
+ if (task.parent.number) parts.push(`- Number: ${projectIdentifier ? `${projectIdentifier}-${task.parent.number}` : `#${task.parent.number}`}`);
1997
2223
  }
1998
2224
  if (wikiContents.length > 0) {
1999
2225
  parts.push("", "---", "", "## Requirement Documents");
@@ -2016,8 +2242,9 @@ var OnesTaskContent = class {
2016
2242
  }
2017
2243
  buildWorkItemSummary(task, kind) {
2018
2244
  const nextTool = kind === "defect" ? "get_issue_detail" : "get_related_issues / get_testcases";
2245
+ const projectIdentifier = task.project?.identifier?.toUpperCase() ?? null;
2019
2246
  const parts = [
2020
- `# #${task.number} ${task.name}`,
2247
+ `# ${taskDisplayId({}, task, projectIdentifier)} ${task.name}`,
2021
2248
  "",
2022
2249
  `- **Type**: ${task.subIssueType?.name ?? task.issueType?.name ?? "Unknown"}`,
2023
2250
  `- **Work Item Kind**: ${kind}`,
@@ -2029,14 +2256,14 @@ var OnesTaskContent = class {
2029
2256
  parts.push(`- **UUID**: ${task.uuid}`);
2030
2257
  if (task.parent?.uuid) {
2031
2258
  parts.push("", "## Parent Task", `- UUID: ${task.parent.uuid}`);
2032
- if (task.parent.number) parts.push(`- Number: #${task.parent.number}`);
2259
+ if (task.parent.number) parts.push(`- Number: ${projectIdentifier ? `${projectIdentifier}-${task.parent.number}` : `#${task.parent.number}`}`);
2033
2260
  }
2034
2261
  const detailText = getTaskDetailText(task);
2035
2262
  if (detailText) parts.push("", "---", "", kind === "defect" ? "## Defect Detail" : "## Task Detail", "", detailText);
2036
2263
  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
2264
  if (task.relatedTasks?.length) {
2038
2265
  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"}`);
2266
+ 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
2267
  }
2041
2268
  const requirement = toRequirement(task, parts.join("\n"));
2042
2269
  requirement.raw = {
@@ -2098,7 +2325,7 @@ const TASK_DETAIL_QUERY = `
2098
2325
  priority { value }
2099
2326
  assign { uuid name }
2100
2327
  owner { uuid name }
2101
- project { uuid name }
2328
+ project { uuid name identifier }
2102
2329
  parent { uuid number issueType { uuid name } }
2103
2330
  relatedTasks {
2104
2331
  key uuid number name
@@ -2109,6 +2336,7 @@ const TASK_DETAIL_QUERY = `
2109
2336
  subIssueType { uuid name detailType }
2110
2337
  status { uuid name category }
2111
2338
  assign { uuid name }
2339
+ project { uuid name identifier }
2112
2340
  }
2113
2341
  relatedWikiPages {
2114
2342
  uuid title referenceType subReferenceType errorMessage
@@ -2245,7 +2473,9 @@ var OnesTaskPlanning = class {
2245
2473
  if (!Number.isInteger(raw.number)) throw new TypeError("ONES: Standalone wiki pages cannot be decomposed into requirement tasks");
2246
2474
  const parsedDisplayId = parseDisplayId(params.requirementId);
2247
2475
  const requirementInfo = await this.options.fetchTaskInfo(workItem.id);
2248
- const projectIdentifier = parsedDisplayId?.identifier.toUpperCase() ?? firstString(requirementInfo, ["projectIdentifier", "project_identifier"]);
2476
+ let projectIdentifier = parsedDisplayId?.identifier ?? firstString(requirementInfo, ["projectIdentifier", "project_identifier"]) ?? raw.project?.identifier ?? null;
2477
+ if (!projectIdentifier && raw.project?.uuid) projectIdentifier = await this.options.resolveProjectIdentifier(raw.project.uuid);
2478
+ projectIdentifier = projectIdentifier?.toUpperCase() ?? null;
2249
2479
  const displayId = firstString(requirementInfo, ["displayId", "display_id"]) ?? (projectIdentifier ? `${projectIdentifier}-${raw.number}` : `#${raw.number}`);
2250
2480
  const relatedTasks = (raw.relatedTasks ?? []).filter((task) => classifyOnesWorkItem(task.issueType, task.subIssueType) === "task");
2251
2481
  const relatedInfos = await Promise.all(relatedTasks.map((task) => this.options.fetchTaskInfo(task.uuid)));
@@ -2574,6 +2804,12 @@ var OnesTestcaseReader = class {
2574
2804
  const JSON0_URI = "http://sharejs.org/types/JSONv0";
2575
2805
  const JSON1_URI = "http://sharejs.org/types/JSONv1";
2576
2806
  const DEFAULT_TIMEOUT_MS$1 = 15e3;
2807
+ function getSetCookies(response) {
2808
+ const headers = response.headers;
2809
+ if (headers.getSetCookie) return headers.getSetCookie();
2810
+ const raw = headers.get("set-cookie");
2811
+ return raw ? [raw] : [];
2812
+ }
2577
2813
  function isRecord(value) {
2578
2814
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
2579
2815
  }
@@ -2586,28 +2822,44 @@ function asDoc(value) {
2586
2822
  }
2587
2823
  function createTopLevelJson1Operation(current, next) {
2588
2824
  let operation = null;
2825
+ const appendComponent = (component) => {
2826
+ operation = operation === null ? component : type.compose(operation, component);
2827
+ };
2589
2828
  const keys = [.../* @__PURE__ */ new Set([...Object.keys(current), ...Object.keys(next)])].sort();
2590
2829
  for (const key of keys) {
2830
+ if (key === "blocks") continue;
2591
2831
  const hasCurrent = Object.hasOwn(current, key);
2592
2832
  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
- }
2833
+ if (!hasCurrent && hasNext) appendComponent(insertOp([key], asDoc(next[key])));
2834
+ else if (hasCurrent && !hasNext) appendComponent(removeOp([key]));
2835
+ else if (hasCurrent && hasNext && !isDeepStrictEqual(current[key], next[key])) appendComponent(replaceOp([key], asDoc(current[key]), asDoc(next[key])));
2836
+ }
2837
+ const currentBlocks = Array.isArray(current.blocks) ? current.blocks : [];
2838
+ const nextBlocks = Array.isArray(next.blocks) ? next.blocks : [];
2839
+ const sharedBlockCount = Math.min(currentBlocks.length, nextBlocks.length);
2840
+ for (let index = 0; index < sharedBlockCount; index += 1) if (!isDeepStrictEqual(currentBlocks[index], nextBlocks[index])) {
2841
+ appendComponent(removeOp(["blocks", index]));
2842
+ appendComponent(insertOp(["blocks", index], asDoc(nextBlocks[index])));
2843
+ }
2844
+ for (let index = currentBlocks.length - 1; index >= nextBlocks.length; index -= 1) appendComponent(removeOp(["blocks", index]));
2845
+ for (let index = currentBlocks.length; index < nextBlocks.length; index += 1) appendComponent(insertOp(["blocks", index], asDoc(nextBlocks[index])));
2600
2846
  if (operation !== null) type.checkValidOp(operation);
2601
2847
  return operation;
2602
2848
  }
2849
+ function createTopLevelJson1Operations(current, next) {
2850
+ const operation = createTopLevelJson1Operation(current, next);
2851
+ return operation === null ? [] : [operation];
2852
+ }
2603
2853
  function wikiEditorUrls(baseUrl, teamId, documentId) {
2604
2854
  const url = new URL(baseUrl);
2605
2855
  if (url.protocol !== "https:" && url.protocol !== "http:") throw new Error("ONES Wiki collaboration requires an HTTP(S) source URL");
2606
2856
  const path = `/wiki/api/wiki/editor/${encodeURIComponent(teamId)}/${encodeURIComponent(documentId)}`;
2607
- const socketUrl = new URL(path, url);
2857
+ const editorBaseUrl = new URL(path, url);
2858
+ const socketUrl = new URL(editorBaseUrl);
2608
2859
  socketUrl.protocol = url.protocol === "https:" ? "wss:" : "ws:";
2609
2860
  return {
2610
- authUrl: new URL(`${path}/auth`, url).toString(),
2861
+ authUrl: `${editorBaseUrl.toString()}/auth`,
2862
+ editorBaseUrl: editorBaseUrl.toString(),
2611
2863
  socketUrl: socketUrl.toString()
2612
2864
  };
2613
2865
  }
@@ -2624,39 +2876,52 @@ function closeSocket(socket) {
2624
2876
  }
2625
2877
  async function replaceOnesWikiDocument(options, update, dependencies = {
2626
2878
  fetch,
2627
- openWebSocket: (url) => new WebSocket(url)
2879
+ openWebSocket: (url, headers) => new WebSocket(url, { headers })
2628
2880
  }) {
2629
- const { authUrl, socketUrl } = wikiEditorUrls(options.baseUrl, options.teamId, options.documentId);
2881
+ const { authUrl, editorBaseUrl, socketUrl } = wikiEditorUrls(options.baseUrl, options.teamId, options.documentId);
2630
2882
  const authResponse = await dependencies.fetch(authUrl, {
2631
2883
  method: "GET",
2632
2884
  headers: {
2633
2885
  "Authorization": `Bearer ${options.accessToken}`,
2634
2886
  "x-live-editor-token": options.editorToken,
2635
- "x-live-editor-base-url": Buffer.from(socketUrl).toString("base64url")
2887
+ "x-live-editor-base-url": Buffer.from(editorBaseUrl).toString("base64url"),
2888
+ ...options.cookieHeader ? { Cookie: options.cookieHeader } : {}
2636
2889
  }
2637
2890
  });
2638
2891
  if (!authResponse.ok) throw new Error(`ONES Wiki collaboration auth failed with status ${authResponse.status}`);
2639
2892
  const editorAuth = await authResponse.json();
2640
2893
  if (typeof editorAuth.read !== "string" || !editorAuth.read) throw new Error("ONES Wiki collaboration auth response did not include a read token");
2894
+ const cookies = [options.cookieHeader, ...getSetCookies(authResponse).map((cookie) => cookie.split(";")[0])].filter((cookie) => Boolean(cookie)).join("; ");
2895
+ const socketHeaders = {
2896
+ "Accept-Language": "zh-CN,zh;q=0.9",
2897
+ "Cache-Control": "no-cache",
2898
+ "Pragma": "no-cache",
2899
+ "User-Agent": "Mozilla/5.0 AppleWebKit/537.36 Chrome/128.0.0.0 Safari/537.36",
2900
+ "Origin": new URL(options.baseUrl).origin,
2901
+ ...cookies ? { Cookie: cookies } : {}
2902
+ };
2641
2903
  return new Promise((resolve, reject) => {
2642
- const socket = dependencies.openWebSocket(socketUrl);
2643
- const sequence = 1;
2644
- let clientId = "";
2904
+ const socket = dependencies.openWebSocket(socketUrl, socketHeaders);
2905
+ let sequence = 1;
2645
2906
  let state = "init";
2646
2907
  let settled = false;
2647
2908
  let snapshotVersion = 0;
2909
+ let currentVersion = 0;
2910
+ let pendingOperations = [];
2911
+ const presenceChannel = `${options.teamId}:${options.documentId}`;
2912
+ const presenceId = randomBytes(7).toString("base64url").slice(0, 9);
2648
2913
  let timeout;
2649
2914
  const finish = (result) => {
2650
2915
  if (settled) return;
2651
2916
  settled = true;
2652
- if (timeout) clearTimeout(timeout);
2917
+ clearTimeout(timeout);
2653
2918
  resolve(result);
2654
2919
  closeSocket(socket);
2655
2920
  };
2656
2921
  const fail = (error) => {
2657
2922
  if (settled) return;
2658
2923
  settled = true;
2659
- if (timeout) clearTimeout(timeout);
2924
+ clearTimeout(timeout);
2660
2925
  reject(error);
2661
2926
  closeSocket(socket);
2662
2927
  };
@@ -2665,9 +2930,38 @@ async function replaceOnesWikiDocument(options, update, dependencies = {
2665
2930
  if (error) fail(/* @__PURE__ */ new Error(`ONES Wiki collaboration send failed: ${error.message}`));
2666
2931
  });
2667
2932
  };
2933
+ const sendHandshake = () => {
2934
+ send({
2935
+ a: "hs",
2936
+ id: null,
2937
+ auth: {
2938
+ appId: options.teamId,
2939
+ docId: options.documentId,
2940
+ userId: options.userId,
2941
+ permission: "w",
2942
+ token: options.editorToken,
2943
+ displayName: options.displayName ?? "",
2944
+ avatarUrl: options.avatarUrl ?? ""
2945
+ }
2946
+ });
2947
+ };
2948
+ const sendNextOperation = () => {
2949
+ const operation = pendingOperations[0];
2950
+ if (operation === void 0) return;
2951
+ send({
2952
+ a: "op",
2953
+ c: options.teamId,
2954
+ d: options.documentId,
2955
+ v: currentVersion,
2956
+ seq: sequence,
2957
+ x: {},
2958
+ op: operation
2959
+ });
2960
+ };
2668
2961
  timeout = setTimeout(() => {
2669
2962
  fail(/* @__PURE__ */ new Error(`ONES Wiki collaboration timed out while waiting for ${state}`));
2670
2963
  }, options.timeoutMs ?? DEFAULT_TIMEOUT_MS$1);
2964
+ socket.on("open", sendHandshake);
2671
2965
  socket.on("error", () => fail(/* @__PURE__ */ new Error("ONES Wiki collaboration WebSocket failed")));
2672
2966
  socket.on("close", (code) => {
2673
2967
  if (!settled) fail(/* @__PURE__ */ new Error(`ONES Wiki collaboration WebSocket closed before ${state} (code ${code})`));
@@ -2694,58 +2988,64 @@ async function replaceOnesWikiDocument(options, update, dependencies = {
2694
2988
  fail(/* @__PURE__ */ new Error("ONES Wiki collaboration returned an unsupported init frame"));
2695
2989
  return;
2696
2990
  }
2697
- clientId = message.id;
2698
2991
  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
- });
2992
+ sendHandshake();
2716
2993
  return;
2717
2994
  }
2718
2995
  if (state === "handshake") {
2719
- if (message.a !== "hs" || message.id !== clientId || message.protocol !== 1 || message.protocolMinor !== 1 || message.type !== JSON0_URI) {
2996
+ if (message.a !== "hs" || typeof message.id !== "string" || message.protocol !== 1 || message.protocolMinor !== 1 || message.type !== JSON0_URI) {
2720
2997
  fail(/* @__PURE__ */ new Error("ONES Wiki collaboration returned an unsupported handshake frame"));
2721
2998
  return;
2722
2999
  }
2723
- state = "snapshot";
3000
+ state = "fetch";
2724
3001
  send({
2725
- a: "s",
3002
+ a: "f",
2726
3003
  c: options.teamId,
2727
3004
  d: options.documentId
2728
3005
  });
2729
3006
  return;
2730
3007
  }
2731
- if (state === "snapshot") {
3008
+ if (state === "fetch") {
2732
3009
  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
- }
3010
+ if (message.a !== "f" || message.c !== options.teamId || message.d !== options.documentId || typeof snapshot?.v !== "number" || snapshot.type !== JSON1_URI) return;
2737
3011
  const current = asWikiSnapshot(snapshot.data);
2738
- let next;
2739
- let operation;
2740
3012
  try {
2741
- next = asJsonDocument(update(structuredClone(current)), "update");
2742
- operation = createTopLevelJson1Operation(current, next);
3013
+ pendingOperations = createTopLevelJson1Operations(current, asJsonDocument(update(structuredClone(current)), "update"));
2743
3014
  } catch (error) {
2744
3015
  fail(error instanceof Error ? error : /* @__PURE__ */ new Error("ONES Wiki collaboration update failed"));
2745
3016
  return;
2746
3017
  }
2747
3018
  snapshotVersion = snapshot.v;
2748
- if (operation === null) {
3019
+ currentVersion = snapshotVersion;
3020
+ state = "presence";
3021
+ send({
3022
+ a: "p",
3023
+ ch: presenceChannel,
3024
+ id: presenceId,
3025
+ p: null,
3026
+ pv: 2
3027
+ });
3028
+ send({
3029
+ a: "ps",
3030
+ ch: presenceChannel,
3031
+ seq: 1
3032
+ });
3033
+ return;
3034
+ }
3035
+ if (state === "presence") {
3036
+ if (message.a !== "ps" || message.ch !== presenceChannel || message.seq !== 1) return;
3037
+ state = "subscribe";
3038
+ send({
3039
+ a: "s",
3040
+ c: options.teamId,
3041
+ d: options.documentId,
3042
+ v: snapshotVersion
3043
+ });
3044
+ return;
3045
+ }
3046
+ if (state === "subscribe") {
3047
+ if (message.a !== "s" || message.c !== options.teamId || message.d !== options.documentId) return;
3048
+ if (!pendingOperations.length) {
2749
3049
  finish({
2750
3050
  snapshotVersion,
2751
3051
  version: snapshotVersion,
@@ -2754,21 +3054,23 @@ async function replaceOnesWikiDocument(options, update, dependencies = {
2754
3054
  return;
2755
3055
  }
2756
3056
  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
- });
3057
+ sendNextOperation();
2765
3058
  return;
2766
3059
  }
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
- });
3060
+ if (state === "ack" && message.a === "op" && message.c === options.teamId && message.d === options.documentId && message.seq === sequence) {
3061
+ currentVersion = typeof message.v === "number" ? message.v : currentVersion + 1;
3062
+ pendingOperations.shift();
3063
+ if (!pendingOperations.length) {
3064
+ finish({
3065
+ snapshotVersion,
3066
+ version: currentVersion,
3067
+ changed: true
3068
+ });
3069
+ return;
3070
+ }
3071
+ sequence += 1;
3072
+ sendNextOperation();
3073
+ }
2772
3074
  });
2773
3075
  });
2774
3076
  }
@@ -2806,6 +3108,10 @@ var OnesWikiProductWriter = class {
2806
3108
  const encodedTeamId = encodeIdentifier(teamId, "team UUID");
2807
3109
  const headers = new Headers(init.headers);
2808
3110
  headers.set("Authorization", `Bearer ${session.accessToken}`);
3111
+ headers.set("Ones-Auth-Token", session.legacyAuthToken);
3112
+ headers.set("Ones-User-Id", session.legacyUserId);
3113
+ headers.set("Referer", this.options.apiBase);
3114
+ if (session.cookieHeader) headers.set("Cookie", session.cookieHeader);
2809
3115
  if (typeof init.body === "string" && !headers.has("Content-Type")) headers.set("Content-Type", "application/json;charset=UTF-8");
2810
3116
  const response = await fetch(`${this.options.apiBase}/wiki/api/wiki/team/${encodedTeamId}${apiPath}`, {
2811
3117
  ...init,
@@ -2828,9 +3134,9 @@ var OnesWikiProductWriter = class {
2828
3134
  }
2829
3135
  throw new Error("ONES Wiki editor token endpoint was not found");
2830
3136
  }
2831
- async replaceDocument(teamId, resourceId, documentId, update, preferDraft = false) {
3137
+ async replaceDocument(teamId, resourceId, documentId, update, preferDraft = false, editorTokenOverride) {
2832
3138
  const session = await this.options.getSession();
2833
- const editorToken = await this.fetchEditorToken(teamId, resourceId, preferDraft);
3139
+ const editorToken = editorTokenOverride ?? await this.fetchEditorToken(teamId, resourceId, preferDraft);
2834
3140
  return { version: (await replaceOnesWikiDocument({
2835
3141
  baseUrl: this.options.apiBase,
2836
3142
  teamId,
@@ -2838,6 +3144,7 @@ var OnesWikiProductWriter = class {
2838
3144
  accessToken: session.accessToken,
2839
3145
  editorToken,
2840
3146
  userId: session.userUuid,
3147
+ cookieHeader: session.cookieHeader,
2841
3148
  displayName: session.userName
2842
3149
  }, update)).version };
2843
3150
  }
@@ -2891,20 +3198,120 @@ var OnesWikiProductWriter = class {
2891
3198
  };
2892
3199
  }
2893
3200
  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) => {
3201
+ const encodedPageId = encodeIdentifier(params.pageId, "Wiki page UUID");
3202
+ const encodedSpaceId = params.spaceId ? encodeIdentifier(params.spaceId, "Wiki space UUID") : null;
3203
+ let detail = await this.request(params.teamId, `/page/${encodedPageId}?action=edit`);
3204
+ const title = firstNonEmptyString(detail.title, detail.name, detail.page_title) ?? `Wiki ${params.pageId}`;
3205
+ if (detail.ref_type === 6 && detail.ref_uuid) {
3206
+ const editorToken = await this.fetchEditorToken(params.teamId, params.pageId);
3207
+ const write = await this.replaceDocument(params.teamId, params.pageId, detail.ref_uuid, (snapshot) => {
3208
+ return parseWikiDocument(applyWikiUpdateOperation(JSON.stringify(snapshot), params.operation));
3209
+ }, false, editorToken);
3210
+ const published = await this.request(params.teamId, `/online_page/${encodedPageId}/publish`, {
3211
+ method: "POST",
3212
+ body: JSON.stringify({ title })
3213
+ });
3214
+ if (params.spaceId) this.options.invalidateTree(params.teamId, params.spaceId);
3215
+ return {
3216
+ pageId: params.pageId,
3217
+ title,
3218
+ version: String(published.version ?? published.updated_time ?? write.version),
3219
+ url: params.spaceId ? `${this.options.apiBase}/wiki/#/team/${encodeURIComponent(params.teamId)}/space/${encodeURIComponent(params.spaceId)}/page/${encodeURIComponent(params.pageId)}` : null
3220
+ };
3221
+ }
3222
+ let draftId = firstNonEmptyString(detail.online_draft_uuid, detail.onlineDraftUuid, detail.online_draft?.uuid, detail.draft_uuid, detail.draftUuid, detail.draft?.uuid);
3223
+ 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);
3224
+ if (!draftId) {
3225
+ if (!encodedSpaceId) throw new Error("ONES Wiki page space is required to create an edit draft");
3226
+ try {
3227
+ const createdDraft = await this.request(params.teamId, `/space/${encodedSpaceId}/drafts/add`, {
3228
+ method: "POST",
3229
+ body: JSON.stringify({
3230
+ page_uuid: params.pageId,
3231
+ status: 2,
3232
+ title
3233
+ })
3234
+ });
3235
+ draftId = firstNonEmptyString(createdDraft.draft_uuid, createdDraft.draftUuid, createdDraft.uuid, createdDraft.id);
3236
+ documentId = firstNonEmptyString(createdDraft.ref_uuid, createdDraft.refUuid, documentId);
3237
+ } catch (error) {
3238
+ const refreshedDetail = await this.request(params.teamId, `/space/${encodedSpaceId}/page/${encodedPageId}`);
3239
+ draftId = firstNonEmptyString(refreshedDetail.draft_uuid, refreshedDetail.draftUuid);
3240
+ if (!draftId) throw error;
3241
+ detail = refreshedDetail;
3242
+ documentId = firstNonEmptyString(refreshedDetail.draft_ref_uuid, refreshedDetail.draftRefUuid, refreshedDetail.ref_uuid, documentId);
3243
+ }
3244
+ }
3245
+ if (!draftId || !encodedSpaceId) throw new Error("ONES Wiki edit draft did not include the required resource and space IDs");
3246
+ const encodedDraftId = encodeIdentifier(draftId, "Wiki draft UUID");
3247
+ const draftDetail = await this.request(params.teamId, `/space/${encodedSpaceId}/draft/${encodedDraftId}`);
3248
+ documentId = firstNonEmptyString(draftDetail.ref_uuid, draftDetail.refUuid, documentId);
3249
+ if (!documentId) throw new Error("ONES Wiki edit draft did not include a collaborative document ID");
3250
+ if (typeof draftDetail.content === "string") {
3251
+ let content;
3252
+ if (params.operation.type === "replace_document") content = markdownToWikiHtml(params.operation.markdown);
3253
+ else if (params.operation.type === "append_blocks") content = `${draftDetail.content}\n${markdownToWikiHtml(params.operation.markdown)}`;
3254
+ else if (params.operation.type === "replace_text") {
3255
+ const occurrences = draftDetail.content.split(params.operation.find).length - 1;
3256
+ if (occurrences !== 1) throw new Error(`ONES Wiki draft replace_text requires exactly one match; found ${occurrences}`);
3257
+ content = draftDetail.content.replace(params.operation.find, params.operation.replace);
3258
+ } else throw new Error("ONES Wiki append_table_row is not supported for legacy page drafts");
3259
+ const published = await this.request(params.teamId, `/space/${encodedSpaceId}/draft/${encodedDraftId}/update`, {
3260
+ method: "POST",
3261
+ body: JSON.stringify({
3262
+ ...draftDetail,
3263
+ content,
3264
+ title,
3265
+ page_uuid: params.pageId,
3266
+ space_uuid: params.spaceId,
3267
+ from_version: detail.version ?? draftDetail.from_version,
3268
+ is_published: true,
3269
+ is_forced: true
3270
+ })
3271
+ });
3272
+ this.options.invalidateTree(params.teamId, params.spaceId);
3273
+ const publishedVersion = published.version ?? published.updated_time ?? draftDetail.version ?? draftDetail.updated_time;
3274
+ return {
3275
+ pageId: params.pageId,
3276
+ title,
3277
+ version: publishedVersion === void 0 ? null : String(publishedVersion),
3278
+ url: `${this.options.apiBase}/wiki/#/team/${encodeURIComponent(params.teamId)}/space/${encodeURIComponent(params.spaceId)}/page/${encodeURIComponent(params.pageId)}`
3279
+ };
3280
+ }
3281
+ const write = await this.replaceDocument(params.teamId, draftId, documentId, (snapshot) => {
2898
3282
  return parseWikiDocument(applyWikiUpdateOperation(JSON.stringify(snapshot), params.operation));
3283
+ }, true);
3284
+ const refreshedDraft = await this.request(params.teamId, `/space/${encodedSpaceId}/draft/${encodedDraftId}`);
3285
+ await this.request(params.teamId, `/space/${encodedSpaceId}/draft/${encodedDraftId}/update`, {
3286
+ method: "POST",
3287
+ body: JSON.stringify({
3288
+ ...refreshedDraft,
3289
+ title,
3290
+ page_uuid: params.pageId,
3291
+ space_uuid: params.spaceId,
3292
+ from_version: detail.version ?? refreshedDraft.from_version,
3293
+ is_published: true,
3294
+ is_forced: true
3295
+ })
2899
3296
  });
2900
3297
  if (params.spaceId) this.options.invalidateTree(params.teamId, params.spaceId);
2901
3298
  return {
2902
3299
  pageId: params.pageId,
2903
- title: firstNonEmptyString(detail.title, detail.name, detail.page_title) ?? `Wiki ${params.pageId}`,
3300
+ title,
2904
3301
  version: String(write.version),
2905
3302
  url: params.spaceId ? `${this.options.apiBase}/wiki/#/team/${encodeURIComponent(params.teamId)}/space/${encodeURIComponent(params.spaceId)}/page/${encodeURIComponent(params.pageId)}` : null
2906
3303
  };
2907
3304
  }
3305
+ async delete(params) {
3306
+ const encodedSpaceId = encodeIdentifier(params.spaceId, "Wiki space UUID");
3307
+ const encodedPageId = encodeIdentifier(params.pageId, "Wiki page UUID");
3308
+ await this.request(params.teamId, `/space/${encodedSpaceId}/page/${encodedPageId}/delete`, { method: "POST" });
3309
+ this.options.invalidateTree(params.teamId, params.spaceId);
3310
+ return {
3311
+ pageId: params.pageId,
3312
+ deleted: true
3313
+ };
3314
+ }
2908
3315
  };
2909
3316
  //#endregion
2910
3317
  //#region ../../src/adapters/ones/task-query.ts
@@ -2991,7 +3398,6 @@ var OnesTaskAdapter = class extends BaseAdapter {
2991
3398
  this.wikiProductWriter = new OnesWikiProductWriter({
2992
3399
  apiBase: config.apiBase,
2993
3400
  getSession: () => this.login(),
2994
- fetchPageDetail: (pageId, teamId) => this.wikiReader.fetchPageDetail(pageId, teamId, true),
2995
3401
  invalidateTree: (teamId, spaceId) => this.wikiReader.invalidateTree(teamId, spaceId)
2996
3402
  });
2997
3403
  this.content = new OnesTaskContent({
@@ -3007,7 +3413,10 @@ var OnesTaskAdapter = class extends BaseAdapter {
3007
3413
  this.planning = new OnesTaskPlanning({
3008
3414
  api: this.api,
3009
3415
  getRequirement: (id) => this.getRequirement({ id }),
3010
- fetchTaskInfo: (taskUuid) => this.fetchTaskInfo(taskUuid)
3416
+ fetchTaskInfo: (taskUuid) => this.fetchTaskInfo(taskUuid),
3417
+ resolveProjectIdentifier: async (projectUuid) => {
3418
+ return (await this.fetchProjects()).find((candidate) => candidate.uuid === projectUuid)?.identifier?.toUpperCase() ?? null;
3419
+ }
3011
3420
  });
3012
3421
  this.issueReader = new OnesIssueReader({
3013
3422
  api: this.api,
@@ -3225,6 +3634,9 @@ var OnesTaskAdapter = class extends BaseAdapter {
3225
3634
  async updateWikiPage(params) {
3226
3635
  return this.wikiProductWriter.update(params);
3227
3636
  }
3637
+ async deleteWikiPage(params) {
3638
+ return this.wikiProductWriter.delete(params);
3639
+ }
3228
3640
  /**
3229
3641
  * Fetch a work item by UUID, number, display id, or wiki URL.
3230
3642
  * Routes by issueType.detailType: requirements (1 and 5) load wiki docs;
@@ -3619,7 +4031,7 @@ async function handleGetGrillingBrief(input, adapters, defaultSource) {
3619
4031
  }
3620
4032
  //#endregion
3621
4033
  //#region ../../src/utils/safe-image.ts
3622
- const DEFAULT_MAX_BYTES = 8 * 1024 * 1024;
4034
+ const DEFAULT_MAX_BYTES = 8388608;
3623
4035
  const DEFAULT_MAX_REDIRECTS = 3;
3624
4036
  const DEFAULT_TIMEOUT_MS = 1e4;
3625
4037
  const MAX_IMAGES = 8;
@@ -3967,7 +4379,7 @@ function formatWorkItem(req) {
3967
4379
  //#endregion
3968
4380
  //#region ../../src/tools/list-pending-work-items.ts
3969
4381
  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) {
4382
+ function resolveAdapter$4(source, adapters, defaultSource) {
3971
4383
  const sourceType = source ?? defaultSource;
3972
4384
  if (!sourceType) throw new Error("No source specified and no default source configured");
3973
4385
  const adapter = adapters.get(sourceType);
@@ -4011,7 +4423,7 @@ function formatResult(result) {
4011
4423
  return lines.join("\n");
4012
4424
  }
4013
4425
  async function handleListPendingWorkItems(input, adapters, defaultSource) {
4014
- const result = await resolveAdapter$3(input.source, adapters, defaultSource).listPendingWorkItems();
4426
+ const result = await resolveAdapter$4(input.source, adapters, defaultSource).listPendingWorkItems();
4015
4427
  const safeResult = {
4016
4428
  ...result,
4017
4429
  items: result.items.map(sanitizeItem)
@@ -4049,7 +4461,7 @@ async function handleListSources(adapters, config) {
4049
4461
  }
4050
4462
  //#endregion
4051
4463
  //#region ../../src/tools/requirement-decomposition.ts
4052
- const APPROVAL_TTL_MS$1 = 1800 * 1e3;
4464
+ const APPROVAL_TTL_MS$1 = 18e5;
4053
4465
  const MAX_CREATE_OPERATIONS = 10;
4054
4466
  function isValidDate(value) {
4055
4467
  if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) return false;
@@ -4125,7 +4537,7 @@ var RequirementDecompositionApprovalStore = class {
4125
4537
  return record;
4126
4538
  }
4127
4539
  };
4128
- function resolveAdapter$2(source, adapters, defaultSource) {
4540
+ function resolveAdapter$3(source, adapters, defaultSource) {
4129
4541
  const sourceType = source ?? defaultSource;
4130
4542
  if (!sourceType) throw new Error("No source specified and no default source configured");
4131
4543
  const adapter = adapters.get(sourceType);
@@ -4228,7 +4640,7 @@ function buildOperations(displayId, tasks) {
4228
4640
  });
4229
4641
  }
4230
4642
  async function handleInspectRequirementDecomposition(input, adapters, defaultSource) {
4231
- const { adapter } = resolveAdapter$2(input.source, adapters, defaultSource);
4643
+ const { adapter } = resolveAdapter$3(input.source, adapters, defaultSource);
4232
4644
  const context = sanitizedContext(await adapter.getRequirementDecompositionContext({ requirementId: input.requirementId }));
4233
4645
  return {
4234
4646
  content: [{
@@ -4239,7 +4651,7 @@ async function handleInspectRequirementDecomposition(input, adapters, defaultSou
4239
4651
  };
4240
4652
  }
4241
4653
  async function handlePrepareRequirementDecomposition(input, adapters, approvals, defaultSource) {
4242
- const { sourceType, adapter } = resolveAdapter$2(input.source, adapters, defaultSource);
4654
+ const { sourceType, adapter } = resolveAdapter$3(input.source, adapters, defaultSource);
4243
4655
  const context = await adapter.getRequirementDecompositionContext({ requirementId: input.requirementId });
4244
4656
  if (context.requirement.workItemKind !== "requirement") throw new Error("Only requirements can be decomposed");
4245
4657
  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 +4701,7 @@ async function handleApplyRequirementDecomposition(input, adapters, approvals, o
4289
4701
  if (!record) throw new Error("Approval token is invalid, expired, or already used. Prepare the decomposition again.");
4290
4702
  if ((input.source ?? options.defaultSource) !== record.source) throw new Error("Approval token source does not match the requested source");
4291
4703
  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);
4704
+ const { adapter } = resolveAdapter$3(record.source, adapters, options.defaultSource);
4293
4705
  const current = await adapter.getRequirementDecompositionContext({ requirementId: record.requirementId });
4294
4706
  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
4707
  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 +4807,82 @@ function formatUpdateTaskPlanDatesResult(result) {
4395
4807
  return lines.join("\n");
4396
4808
  }
4397
4809
  //#endregion
4810
+ //#region ../../src/tools/wiki-delete.ts
4811
+ const SourceSchema$1 = z.string().trim().min(1).optional();
4812
+ const DeleteEmptyWikiDuplicatesSchema = z.object({
4813
+ keepPageId: z.string().trim().min(1),
4814
+ duplicatePageIds: z.array(z.string().trim().min(1)).min(1).max(20),
4815
+ expectedTitle: z.string().trim().min(1),
4816
+ confirmed: z.literal(true).describe("Set true only after the user confirms the exact keep and delete page IDs immediately before submission."),
4817
+ source: SourceSchema$1
4818
+ });
4819
+ const DeleteEmptyWikiDuplicatesOutputSchema = z.object({
4820
+ keptPageId: z.string(),
4821
+ deletedPageIds: z.array(z.string())
4822
+ });
4823
+ function resolveAdapter$2(source, adapters, defaultSource) {
4824
+ const sourceType = source ?? defaultSource;
4825
+ if (!sourceType) throw new Error("No source specified and no default source configured");
4826
+ const adapter = adapters.get(sourceType);
4827
+ if (!adapter) throw new Error(`Source "${sourceType}" is not configured`);
4828
+ return {
4829
+ sourceType,
4830
+ adapter
4831
+ };
4832
+ }
4833
+ function assertCleanupTarget(keep, duplicates, expectedTitle) {
4834
+ if (keep.title !== expectedTitle) throw new Error("The retained Wiki page title no longer matches the confirmed title");
4835
+ if (keep.content.trim() === `# ${keep.title}`) throw new Error("The retained Wiki page does not contain a body");
4836
+ for (const duplicate of duplicates) {
4837
+ if (duplicate.title !== expectedTitle) throw new Error(`Wiki duplicate ${duplicate.pageId} title no longer matches the confirmed title`);
4838
+ 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`);
4839
+ if (duplicate.content.trim() !== `# ${duplicate.title}`) throw new Error(`Wiki duplicate ${duplicate.pageId} is not empty and will not be deleted`);
4840
+ }
4841
+ }
4842
+ async function handleDeleteEmptyWikiDuplicates(input, adapters, options) {
4843
+ if (!options.writesEnabled) throw new Error("Wiki writes are disabled. Enable both ONES_WIKI_ENABLE_WRITES=true and source option wikiWrites=true.");
4844
+ const uniqueDuplicateIds = [...new Set(input.duplicatePageIds)];
4845
+ if (uniqueDuplicateIds.length !== input.duplicatePageIds.length) throw new Error("duplicatePageIds contains repeated page IDs");
4846
+ if (uniqueDuplicateIds.includes(input.keepPageId)) throw new Error("The retained Wiki page cannot also be deleted");
4847
+ const { adapter } = resolveAdapter$2(input.source, adapters, options.defaultSource);
4848
+ const [keep, ...duplicates] = await Promise.all([adapter.getWikiPage({ pageId: input.keepPageId }), ...uniqueDuplicateIds.map((pageId) => adapter.getWikiPage({ pageId }))]);
4849
+ assertCleanupTarget(keep, duplicates, input.expectedTitle);
4850
+ const spaceId = keep.spaceId;
4851
+ if (!spaceId) throw new Error("The retained Wiki page space could not be verified");
4852
+ const operationHash = crypto.createHash("sha256").update(JSON.stringify({
4853
+ keepPageId: input.keepPageId,
4854
+ duplicatePageIds: uniqueDuplicateIds,
4855
+ expectedTitle: input.expectedTitle,
4856
+ baselines: [keep, ...duplicates].map((page) => ({
4857
+ pageId: page.pageId,
4858
+ version: page.version,
4859
+ contentHash: page.contentHash
4860
+ }))
4861
+ })).digest("hex");
4862
+ for (const duplicate of duplicates) await adapter.deleteWikiPage({
4863
+ teamId: duplicate.teamId,
4864
+ spaceId,
4865
+ pageId: duplicate.pageId
4866
+ });
4867
+ const result = {
4868
+ keptPageId: keep.pageId,
4869
+ deletedPageIds: duplicates.map((page) => page.pageId)
4870
+ };
4871
+ return {
4872
+ content: [{
4873
+ type: "text",
4874
+ text: `Kept Wiki page ${result.keptPageId} and deleted empty duplicates: ${result.deletedPageIds.join(", ")}.\noperationHash: ${operationHash}`
4875
+ }],
4876
+ structuredContent: result
4877
+ };
4878
+ }
4879
+ //#endregion
4398
4880
  //#region ../../src/tools/wiki-read.ts
4399
4881
  const WikiSourceSchema = z.string().trim().min(1).optional();
4400
4882
  const GetOnesWikiPageSchema = z.object({
4401
4883
  pageId: z.string().trim().min(1).optional(),
4402
4884
  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."),
4885
+ 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
4886
  teamId: z.string().trim().min(1).optional(),
4405
4887
  spaceId: z.string().trim().min(1).optional(),
4406
4888
  revealSensitiveSecrets: z.boolean().default(false).describe("Default false. Set true only when the user explicitly asks to reveal secrets."),
@@ -4502,6 +4984,19 @@ async function handleGetOnesWikiPage(input, adapters, defaultSource) {
4502
4984
  teamId: resolved?.teamId ?? input.teamId,
4503
4985
  spaceId: resolved?.spaceId ?? input.spaceId
4504
4986
  }), input.revealSensitiveSecrets);
4987
+ if (resolved) {
4988
+ page.title = resolved.title;
4989
+ page.breadcrumb = resolved.breadcrumb;
4990
+ const redactPrivateValues = resolved.redactPrivateValues;
4991
+ if (redactPrivateValues) {
4992
+ page.content = redactPrivateValues(page.content);
4993
+ page.attachments = page.attachments.map((attachment) => ({
4994
+ ...attachment,
4995
+ name: redactPrivateValues(attachment.name),
4996
+ url: redactPrivateValues(attachment.url)
4997
+ }));
4998
+ }
4999
+ }
4505
5000
  return {
4506
5001
  content: [{
4507
5002
  type: "text",
@@ -4647,9 +5142,9 @@ async function handleLookupEnvironmentAccess(input, adapters, defaultSource) {
4647
5142
  }
4648
5143
  //#endregion
4649
5144
  //#region ../../src/tools/wiki-write.ts
4650
- const APPROVAL_TTL_MS = 1800 * 1e3;
5145
+ const APPROVAL_TTL_MS = 18e5;
4651
5146
  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)]);
5147
+ 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
5148
  const PrepareWikiCreateSchema = z.object({
4654
5149
  parentPageId: z.string().trim().min(1).optional(),
4655
5150
  parentPath: PathSchema.optional(),
@@ -4673,6 +5168,10 @@ const ReplaceTextSchema = z.object({
4673
5168
  find: z.string().min(1),
4674
5169
  replace: z.string()
4675
5170
  });
5171
+ const ReplaceDocumentSchema = z.object({
5172
+ type: z.literal("replace_document"),
5173
+ markdown: z.string().trim().min(1)
5174
+ });
4676
5175
  const PrepareWikiUpdateSchema = z.object({
4677
5176
  pageId: z.string().trim().min(1).optional(),
4678
5177
  url: z.string().url().optional(),
@@ -4682,7 +5181,8 @@ const PrepareWikiUpdateSchema = z.object({
4682
5181
  operation: z.discriminatedUnion("type", [
4683
5182
  AppendBlocksSchema,
4684
5183
  AppendTableRowSchema,
4685
- ReplaceTextSchema
5184
+ ReplaceTextSchema,
5185
+ ReplaceDocumentSchema
4686
5186
  ]),
4687
5187
  source: SourceSchema
4688
5188
  }).refine((value) => [
@@ -4723,6 +5223,10 @@ const WikiUpdateOperationSchema = z.discriminatedUnion("type", [
4723
5223
  type: z.literal("replace_text"),
4724
5224
  find: z.string(),
4725
5225
  replace: z.string()
5226
+ }),
5227
+ z.object({
5228
+ type: z.literal("replace_document"),
5229
+ markdown: z.string()
4726
5230
  })
4727
5231
  ]);
4728
5232
  const WikiUpdateRequestSchema = z.object({
@@ -4852,11 +5356,24 @@ async function handlePrepareWikiCreate(input, adapters, approvals, defaultSource
4852
5356
  const teamId = resolved?.teamId ?? parent.teamId;
4853
5357
  const spaceId = resolved?.spaceId ?? parent.spaceId;
4854
5358
  if (!spaceId) throw new Error("The target space could not be verified");
5359
+ const title = input.title.trim();
5360
+ const titleCandidates = await adapter.searchWikiPages({
5361
+ query: title,
5362
+ teamId,
5363
+ spaceId,
5364
+ limit: 50
5365
+ });
5366
+ const siblingConflicts = (await Promise.all(titleCandidates.filter((candidate) => candidate.title === title).map((candidate) => adapter.getWikiPage({
5367
+ pageId: candidate.pageId,
5368
+ teamId: candidate.teamId,
5369
+ spaceId: candidate.spaceId ?? spaceId
5370
+ })))).filter((page) => page.parentPageId === parent.pageId);
5371
+ 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
5372
  const requestWithoutKey = {
4856
5373
  teamId,
4857
5374
  spaceId,
4858
5375
  parentPageId: parent.pageId,
4859
- title: input.title.trim(),
5376
+ title,
4860
5377
  markdown: input.markdown
4861
5378
  };
4862
5379
  const operationHash = hashOperation({
@@ -4878,7 +5395,7 @@ async function handlePrepareWikiCreate(input, adapters, approvals, defaultSource
4878
5395
  });
4879
5396
  const plan = {
4880
5397
  kind: "create",
4881
- targetBreadcrumb: [...parent.breadcrumb, input.title.trim()],
5398
+ targetBreadcrumb: [...resolved?.breadcrumb ?? parent.breadcrumb, input.title.trim()],
4882
5399
  request,
4883
5400
  parentBaseline: baseline(parent),
4884
5401
  operationHash,
@@ -4935,7 +5452,7 @@ async function handlePrepareWikiUpdate(input, adapters, approvals, defaultSource
4935
5452
  });
4936
5453
  const plan = {
4937
5454
  kind: "update",
4938
- targetBreadcrumb: page.breadcrumb,
5455
+ targetBreadcrumb: resolved?.breadcrumb ?? page.breadcrumb,
4939
5456
  request,
4940
5457
  operationHash,
4941
5458
  approvalToken: approval.token,
@@ -4945,7 +5462,7 @@ async function handlePrepareWikiUpdate(input, adapters, approvals, defaultSource
4945
5462
  content: [{
4946
5463
  type: "text",
4947
5464
  text: [
4948
- `Prepared Wiki update for ${page.breadcrumb.join(" / ") || page.title}.`,
5465
+ `Prepared Wiki update for ${plan.targetBreadcrumb.join(" / ") || page.title}.`,
4949
5466
  "No write was performed. Ask the user to confirm this exact operation immediately before apply.",
4950
5467
  `operationHash: ${plan.operationHash}`,
4951
5468
  `approvalToken: ${plan.approvalToken}`,
@@ -5058,7 +5575,7 @@ function createRequirementsServer(config, adapterOverrides) {
5058
5575
  });
5059
5576
  server.registerTool("get_ones_wiki_page", {
5060
5577
  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.",
5578
+ 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
5579
  inputSchema: GetOnesWikiPageSchema,
5063
5580
  annotations: {
5064
5581
  readOnlyHint: true,
@@ -5120,7 +5637,7 @@ function createRequirementsServer(config, adapterOverrides) {
5120
5637
  });
5121
5638
  server.registerTool("prepare_wiki_create", {
5122
5639
  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.",
5640
+ 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
5641
  inputSchema: PrepareWikiCreateSchema,
5125
5642
  outputSchema: PrepareWikiCreateOutputSchema,
5126
5643
  annotations: {
@@ -5159,7 +5676,7 @@ function createRequirementsServer(config, adapterOverrides) {
5159
5676
  });
5160
5677
  server.registerTool("prepare_wiki_update", {
5161
5678
  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.",
5679
+ 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
5680
  inputSchema: PrepareWikiUpdateSchema,
5164
5681
  outputSchema: PrepareWikiUpdateOutputSchema,
5165
5682
  annotations: {
@@ -5196,6 +5713,28 @@ function createRequirementsServer(config, adapterOverrides) {
5196
5713
  return toolError(err);
5197
5714
  }
5198
5715
  });
5716
+ server.registerTool("delete_empty_wiki_duplicates", {
5717
+ title: "Delete Empty ONES Wiki Duplicates",
5718
+ 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.",
5719
+ inputSchema: DeleteEmptyWikiDuplicatesSchema,
5720
+ outputSchema: DeleteEmptyWikiDuplicatesOutputSchema,
5721
+ annotations: {
5722
+ readOnlyHint: false,
5723
+ destructiveHint: true,
5724
+ idempotentHint: false,
5725
+ openWorldHint: true
5726
+ }
5727
+ }, async (params) => {
5728
+ try {
5729
+ const sourceType = params.source ?? defaultSource;
5730
+ return await handleDeleteEmptyWikiDuplicates(params, adapters, {
5731
+ defaultSource,
5732
+ writesEnabled: wikiWritesEnabled(sourceType)
5733
+ });
5734
+ } catch (err) {
5735
+ return toolError(err);
5736
+ }
5737
+ });
5199
5738
  server.registerTool("list_sources", {
5200
5739
  title: "List Sources",
5201
5740
  description: "List all configured requirement sources and their status",