{"record":{"id":"780429613bfebf99","repo":"jackwener/OpenCLI","slug":"too-many-images-paths-length-max-max-images","errorCode":null,"errorMessage":"Too many images: ${paths.length} (max ${MAX_IMAGES})","messagePattern":"Too many images: (.+?) \\(max (.+?)\\)","errorType":"validation","errorClass":"CommandExecutionError","httpStatus":null,"severity":"error","filePath":"clis/twitter/post.js","lineNumber":22,"sourceCode":"import { CommandExecutionError, TimeoutError } from '@jackwener/opencli/errors';\nimport { unwrapBrowserResult } from './shared.js';\nimport { isRecoverableFileInputError } from './utils.js';\n\nconst MAX_IMAGES = 4;\nconst UPLOAD_POLL_MS = 500;\nconst UPLOAD_TIMEOUT_MS = 30_000;\nconst COMPOSER_POLL_MS = 250;\nconst COMPOSER_TIMEOUT_MS = 10_000;\nconst SUBMIT_POLL_MS = 500;\nconst SUBMIT_TIMEOUT_MS = 15_000;\nconst COMPOSE_URL = 'https://x.com/compose/post';\nconst FILE_INPUT_SELECTOR = 'input[type=\"file\"][data-testid=\"fileInput\"]';\nconst SUPPORTED_EXTENSIONS = new Set(['.jpg', '.jpeg', '.png', '.gif', '.webp']);\n\nfunction validateImagePaths(raw) {\n    const paths = raw.split(',').map(s => s.trim()).filter(Boolean);\n    if (paths.length > MAX_IMAGES) {\n        throw new CommandExecutionError(`Too many images: ${paths.length} (max ${MAX_IMAGES})`);\n    }\n    return paths.map(p => {\n        const absPath = path.resolve(p);\n        const ext = path.extname(absPath).toLowerCase();\n        if (!SUPPORTED_EXTENSIONS.has(ext)) {\n            throw new CommandExecutionError(`Unsupported image format \"${ext}\". Supported: jpg, png, gif, webp`);\n        }\n        const stat = fs.statSync(absPath, { throwIfNoEntry: false });\n        if (!stat || !stat.isFile()) {\n            throw new CommandExecutionError(`Not a valid file: ${absPath}`);\n        }\n        return absPath;\n    });\n}\n\nfunction isUnsupportedInsertTextError(err) {\n    const msg = err instanceof Error ? err.message : String(err);\n    const lower = msg.toLowerCase();","sourceCodeStart":4,"sourceCodeEnd":40,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/twitter/post.js#L4-L40","documentation":"CommandExecutionError thrown by validateImagePaths when a comma-separated image path list contains more than MAX_IMAGES (4) entries. X.com allows at most 4 images per post, so the CLI enforces this limit before opening the composer to avoid a guaranteed upload failure. Raised in validateImagePaths, called via absPaths before any browser interaction.","triggerScenarios":"Calling the twitter post command with an --images/-style comma-joined string containing 5+ paths, e.g. 'a.jpg,b.png,c.jpg,d.png,e.jpg' — after trim/filter the array length exceeds 4.","commonSituations":"Globbing directories into a comma list and exceeding the limit; scripts joining all files in a folder; users unaware of Twitter's 4-image cap; trailing-comma strings that leave extra entries after filtering.","solutions":["Reduce the comma-separated list to at most 4 image paths.","Split posts into multiple tweets if you need more than 4 images (each with its own set).","In scripts, slice the file list to 4 before invoking: files.slice(0, 4).join(',').","Consider a video post instead — X's video path has different (larger) limits."],"exampleFix":"// before\nconst images = allFiles.join(',');\nawait post({ text, images }); // may exceed 4\n// after\nconst images = allFiles.slice(0, 4).join(',');\nif (allFiles.length > 4) console.warn('Posting only first 4 images; Twitter allows max 4 per post');\nawait post({ text, images });","handlingStrategy":"validation","validationCode":"const paths = rawImages.split(',').map(s => s.trim()).filter(Boolean);\nif (paths.length > 4) throw new Error(`Max 4 images per post, got ${paths.length}`);","typeGuard":"function isWithinImageLimit(paths, max = 4) {\n  return Array.isArray(paths) && paths.length <= max;\n}","tryCatchPattern":"try {\n  await run(['twitter', 'post', '--images', rawImages]);\n} catch (err) {\n  if (err.message.startsWith('Too many images')) {\n    const first4 = rawImages.split(',').map(s => s.trim()).filter(Boolean).slice(0, 4).join(',');\n    await run(['twitter', 'post', '--images', first4]);\n  } else throw err;\n}","preventionTips":["Remember Twitter's hard limit of 4 images per post","Slice or chunk file lists in scripts before joining into a comma string","Watch for glob/directory expansion silently producing extra paths"],"tags":["validation","twitter","image-upload","limit-exceeded"],"backgroundTag":"limit-exceeded","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}