{"record":{"id":"6877dca218fc771d","repo":"jackwener/OpenCLI","slug":"could-not-extract-tweet-id-from-url-value","errorCode":null,"errorMessage":"Could not extract tweet ID from URL: ${value}","messagePattern":"Could not extract tweet ID from URL: (.+?)","errorType":"validation","errorClass":"ArgumentError","httpStatus":null,"severity":"error","filePath":"clis/twitter/shared.js","lineNumber":65,"sourceCode":"export function parseTweetUrl(rawUrl) {\n    const value = String(rawUrl ?? '').trim();\n    if (!value) {\n        throw new ArgumentError('twitter tweet URL cannot be empty', 'Example: opencli twitter retweet https://x.com/jack/status/20');\n    }\n    let parsed;\n    try {\n        parsed = new URL(value);\n    }\n    catch {\n        throw new ArgumentError(`Invalid tweet URL: ${value}`, 'Use a full https://x.com/<user>/status/<id> URL');\n    }\n    const hostname = parsed.hostname.toLowerCase();\n    if (parsed.protocol !== 'https:' || !isTwitterHost(hostname)) {\n        throw new ArgumentError(`Invalid tweet URL host: ${value}`, 'Use a full https://x.com/<user>/status/<id> URL');\n    }\n    const match = parsed.pathname.match(TWEET_PATH_PATTERN);\n    if (!match?.[1]) {\n        throw new ArgumentError(`Could not extract tweet ID from URL: ${value}`, 'Use a full https://x.com/<user>/status/<id> URL');\n    }\n    return {\n        id: match[1],\n        url: parsed.toString(),\n    };\n}\n\n/**\n * Build a JS source fragment that, when embedded inside a `page.evaluate(...)`\n * IIFE, declares browser-side helpers for scoping operations to a specific\n * tweet by status id. Sibling adapters historically inlined ad-hoc article\n * lookups that either (a) skipped scoping entirely (silent: act on first\n * matching button on a conversation page) or (b) used substring matches like\n * `pathname.includes('/status/' + tweetId)` (silent: `/status/123` matches\n * `/status/1234567`). This helper centralises the canonical pattern so all\n * write-actions reuse the same exact-match guard.\n *\n * Declared bindings (available to the embedding IIFE):","sourceCodeStart":47,"sourceCodeEnd":83,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/twitter/shared.js#L47-L83","documentation":"parseTweetUrl validates tweet URLs in clis/twitter/shared.js:65. After confirming the URL is https and on an x.com/twitter.com host, it matches pathname against TWEET_PATH_PATTERN (^/(?:[^/]+|i)/status/(\\d+)/?). If the path does not contain a /status/<numeric-id> segment, it throws ArgumentError with the offending URL. The library throws this because a tweet ID is required for every downstream GraphQL call.","triggerScenarios":"Calling any command that accepts a tweet URL (e.g. opencli twitter retweet/tweet actions) with a URL whose pathname is not /<user>/status/<digits>, such as https://x.com/jack (profile), https://x.com/jack/status/ (missing id), https://x.com/jack/status/abc (non-numeric id), or a non-status deep link like /jack/likes/123.","commonSituations":"Pasting a profile page or photo page URL instead of the tweet permalink; copying a mobile or shortened link with extra segments (e.g. /i/web/status/ works but /jack/status followed by query junk stripped wrong); URLs from retweet quotes like /jack/status/123/retweets; hand-built URLs with placeholder IDs.","solutions":["Open the tweet in a browser and copy the full permalink from the address bar or the tweet's share > Copy link button; it must look like https://x.com/<user>/status/<numeric-id>","If you only have the tweet ID, construct the URL yourself: `https://x.com/i/status/${id}`","Strip tracking suffixes carefully — the pattern requires the numeric ID directly after /status/, so `https://x.com/jack/status/12345?s=20` is fine but `https://x.com/jack/status/` is not","Check that you did not paste a URL for a different entity (user profile, list, or search results page)"],"exampleFix":"// before\nopencli twitter retweet https://x.com/jack\n// after\nopencli twitter retweet https://x.com/jack/status/1784239871234567","handlingStrategy":"validation","validationCode":"function looksLikeTweetUrl(u) {\n  try {\n    const p = new URL(String(u).trim());\n    if (p.protocol !== 'https:') return false;\n    if (!/(^|\\.)(x\\.com|twitter\\.com)$/.test(p.hostname)) return false;\n    return /^\\/(?:[^/]+|i)\\/status\\/(\\d+)\\/?$/.test(p.pathname);\n  } catch { return false; }\n}\nif (!looksLikeTweetUrl(input)) throw new Error(`Not a tweet permalink: ${input}`);","typeGuard":"function isTweetUrl(value) {\n  try {\n    const p = new URL(String(value).trim());\n    return p.protocol === 'https:' && /(^|\\.)(x\\.com|twitter\\.com)$/.test(p.hostname)\n      && /^\\/(?:[^/]+|i)\\/status\\/\\d+\\/?$/.test(p.pathname);\n  } catch { return false; }\n}","tryCatchPattern":"import { ArgumentError } from '@jackwener/opencli/errors';\ntry {\n  const { id } = await parseTweetUrl(url);\n} catch (e) {\n  if (e instanceof ArgumentError) {\n    console.error(`Bad tweet URL: ${url}. ${e.hint ?? 'Use https://x.com/<user>/status/<id>'}`);\n    process.exitCode = 2;\n  } else throw e;\n}","preventionTips":["Always copy the permalink via the tweet's Share > Copy link, not the address bar of a profile page","Normalize known variants (/i/web/status/<id>) to the canonical /<user>/status/<id> form before calling","Unit-test your URL-building helpers against profile, photo, and retweet-list URLs to catch regressions"],"tags":["argument-validation","url-parsing","twitter"],"backgroundTag":"invalid-url-format","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}