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