{"record":{"id":"d7de56c20ef16aef","repo":"jackwener/OpenCLI","slug":"could-not-parse-twitter-bookmarks-resume-file-fi","errorCode":null,"errorMessage":"Could not parse Twitter bookmarks resume file ${filePath}: ${error instanceof Error ? error.message : String(error)}","messagePattern":"Could not parse Twitter bookmarks resume file (.+?): (.+?)","errorType":"exception","errorClass":"CommandExecutionError","httpStatus":null,"severity":"error","filePath":"clis/twitter/bookmarks.js","lineNumber":113,"sourceCode":"            }\n            for (const item of content?.items || []) {\n                const nested = extractBookmarkTweet(item.item?.itemContent?.tweet_results?.result, seen);\n                if (nested)\n                    tweets.push(nested);\n            }\n        }\n    }\n    return { tweets, nextCursor };\n}\nfunction readResumeFile(filePath, expected = null) {\n    if (!filePath || !fs.existsSync(filePath))\n        return null;\n    let parsed;\n    try {\n        parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));\n    }\n    catch (error) {\n        throw new CommandExecutionError(`Could not parse Twitter bookmarks resume file ${filePath}: ${error instanceof Error ? error.message : String(error)}`);\n    }\n    const count = parsed?.count;\n    const cursor = parsed?.cursor == null ? null : String(parsed.cursor);\n    const outputFile = parsed?.outputFile ? path.resolve(String(parsed.outputFile)) : null;\n    if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)\n        || !Number.isInteger(count) || count < 0\n        || (parsed.cursor != null && typeof parsed.cursor !== 'string')\n        || (cursor !== null && !cursor.trim())) {\n        throw new CommandExecutionError(`Twitter bookmarks resume file ${filePath} has an invalid shape`);\n    }\n    if (expected) {\n        if (parsed.source !== expected.source)\n            throw new ArgumentError(`Resume file source mismatch: expected ${expected.source}, found ${parsed.source || 'unknown'}`);\n        if (outputFile !== expected.outputFile)\n            throw new ArgumentError(`Resume file output mismatch: expected ${expected.outputFile || 'in-memory mode'}, found ${outputFile || 'in-memory mode'}`);\n        if (!expected.outputFile && !Array.isArray(parsed.tweets))\n            throw new CommandExecutionError(`Twitter bookmarks resume file ${filePath} is missing in-memory tweets`);\n        if (!expected.outputFile && parsed.tweets.length !== count)","sourceCodeStart":95,"sourceCodeEnd":131,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/twitter/bookmarks.js#L95-L131","documentation":"readResumeFile JSON-parses the resume file pointed to by --resume-file; if JSON.parse or the read throws, it wraps the underlying parse/read message in a CommandExecutionError. This means the resume file exists but is not valid JSON (or is unreadable), so the bookmarks sync cannot restore its prior state.","triggerScenarios":"Running `twitter bookmarks --all --resume-file <path>` where the file exists but contains truncated JSON (e.g. an interrupted non-atomic write), garbage/binary content, BOM corruption, or a read-permission/encoding failure.","commonSituations":"Manually editing the resume file and leaving invalid JSON; a previous crash corrupted it; a tool wrote partial content; the file was replaced by a symlink or a file with no read permission.","solutions":["Delete or rename the corrupt resume file (and its .tmp-* sibling) and restart the --all sync from scratch, or remove --resume-file to run in-memory.","Validate the file with `JSON.parse(fs.readFileSync(path,'utf8'))` in node to see the exact parse error before retrying.","Restore the file from a backup taken mid-sync.","Check file permissions (readable by the current user) and encoding (UTF-8, no BOM)."],"exampleFix":"// before: resuming with a corrupt file\n$ cli twitter bookmarks --all --resume-file ./bookmarks-resume.json\n// CommandExecutionError: Could not parse ... Unexpected end of JSON input\n// after: inspect, then restart clean\n$ node -e \"JSON.parse(require('fs').readFileSync('./bookmarks-resume.json','utf8'))\"\n$ rm ./bookmarks-resume.json && cli twitter bookmarks --all --resume-file ./bookmarks-resume.json","handlingStrategy":"validation","validationCode":"function isParsableJsonResumeFile(p) {\n  try {\n    JSON.parse(require('node:fs').readFileSync(p, 'utf8'));\n    return true;\n  } catch {\n    return false;\n  }\n}\nif (resumeFile && fs.existsSync(resumeFile) && !isParsableJsonResumeFile(resumeFile)) {\n  fs.rmSync(resumeFile, { force: true }); // start fresh\n}","typeGuard":"function isValidResumeJson(value) {\n  return typeof value === 'object' && value !== null && !Array.isArray(value);\n}","tryCatchPattern":"try {\n  await run(['twitter', 'bookmarks', '--all', '--resume-file', resumePath]);\n} catch (e) {\n  if (String(e.message).includes('Could not parse Twitter bookmarks resume file')) {\n    fs.rmSync(resumePath, { force: true });\n    await run(['twitter', 'bookmarks', '--all', '--resume-file', resumePath]);\n  } else throw e;\n}","preventionTips":["Never hand-edit resume files; let the tool write them atomically.","Keep resume files on the same filesystem as the tool so rename-based writes stay atomic.","Validate JSON before resuming an unattended sync.","Keep backups of long-running sync state."],"tags":["json-parse","resume-file","cli"],"backgroundTag":"invalid-json-resume-state","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}