| 483 | const normalizeTag = (v: string): string => v.trim(); |
| 484 | |
| 485 | const parseRepoFromUrl = (raw: string): { repo: string; canonicalUrl: string } | null => { |
| 486 | try { |
| 487 | const input = raw.trim(); |
| 488 | if (!input) return null; |
| 489 | const url = new URL(input); |
| 490 | |
| 491 | if (url.hostname === "github.com") { |
| 492 | const parts = url.pathname.split("/").filter(Boolean); |
| 493 | if (parts.length < 2) return null; |
| 494 | const owner = parts[0]; |
| 495 | const repoName = parts[1].replace(/\.git$/i, ""); |
| 496 | return { |
| 497 | repo: `${owner}/${repoName}`, |
| 498 | canonicalUrl: `https://github.com/${owner}/${repoName}`, |
| 499 | }; |
| 500 | } |
| 501 | |
| 502 | if (url.hostname === "deepwiki.com") { |
| 503 | const parts = url.pathname.split("/").filter(Boolean); |
| 504 | if (parts.length >= 3 && parts[0] === "browse" && parts[1] === "github.com") { |
| 505 | const owner = parts[2]; |
| 506 | const repoName = parts[3]?.replace(/\.git$/i, ""); |
| 507 | if (!owner || !repoName) return null; |
| 508 | return { |
| 509 | repo: `${owner}/${repoName}`, |
| 510 | canonicalUrl: `https://deepwiki.com/browse/github.com/${owner}/${repoName}`, |
| 511 | }; |
| 512 | } |
| 513 | if (parts.length >= 2) { |
| 514 | const owner = parts[0]; |
| 515 | const repoName = parts[1].replace(/\.git$/i, ""); |
| 516 | return { |
| 517 | repo: `${owner}/${repoName}`, |
| 518 | canonicalUrl: `https://deepwiki.com/${owner}/${repoName}`, |
| 519 | }; |
| 520 | } |
| 521 | } |
| 522 | } catch { |
| 523 | return null; |
| 524 | } |
| 525 | return null; |
| 526 | }; |
| 527 | |
| 528 | const splitMarkdownSections = (text: string): string[] => { |
| 529 | const sections = text.split(/\n(?=#+\s)/).map((s) => s.trim()).filter(Boolean); |