jackwener/OpenCLI · error · CommandExecutionError

Could not parse Twitter bookmarks resume file

Error message

Could not parse Twitter bookmarks resume file ${filePath}: ${error instanceof Error ? error.message : String(error)}

What it means

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.

Solutions

  1. 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.
  2. Validate the file with `JSON.parse(fs.readFileSync(path,'utf8'))` in node to see the exact parse error before retrying.
  3. Restore the file from a backup taken mid-sync.
  4. Check file permissions (readable by the current user) and encoding (UTF-8, no BOM).

Example fix

// before: resuming with a corrupt file
$ cli twitter bookmarks --all --resume-file ./bookmarks-resume.json
// CommandExecutionError: Could not parse ... Unexpected end of JSON input
// after: inspect, then restart clean
$ node -e "JSON.parse(require('fs').readFileSync('./bookmarks-resume.json','utf8'))"
$ rm ./bookmarks-resume.json && cli twitter bookmarks --all --resume-file ./bookmarks-resume.json
Defensive patterns

Strategy: validation

Validate before calling

function isParsableJsonResumeFile(p) {
  try {
    JSON.parse(require('node:fs').readFileSync(p, 'utf8'));
    return true;
  } catch {
    return false;
  }
}
if (resumeFile && fs.existsSync(resumeFile) && !isParsableJsonResumeFile(resumeFile)) {
  fs.rmSync(resumeFile, { force: true }); // start fresh
}

Type guard

function isValidResumeJson(value) {
  return typeof value === 'object' && value !== null && !Array.isArray(value);
}

Try / catch

try {
  await run(['twitter', 'bookmarks', '--all', '--resume-file', resumePath]);
} catch (e) {
  if (String(e.message).includes('Could not parse Twitter bookmarks resume file')) {
    fs.rmSync(resumePath, { force: true });
    await run(['twitter', 'bookmarks', '--all', '--resume-file', resumePath]);
  } else throw e;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of jackwener/OpenCLI@49907e53dc (2026-08-29). Data as JSON: /api/errors/d7de56c20ef16aef. Report an issue: GitHub.

Appendix: source

Thrown at clis/twitter/bookmarks.js:113

            }
            for (const item of content?.items || []) {
                const nested = extractBookmarkTweet(item.item?.itemContent?.tweet_results?.result, seen);
                if (nested)
                    tweets.push(nested);
            }
        }
    }
    return { tweets, nextCursor };
}
function readResumeFile(filePath, expected = null) {
    if (!filePath || !fs.existsSync(filePath))
        return null;
    let parsed;
    try {
        parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));
    }
    catch (error) {
        throw new CommandExecutionError(`Could not parse Twitter bookmarks resume file ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
    }
    const count = parsed?.count;
    const cursor = parsed?.cursor == null ? null : String(parsed.cursor);
    const outputFile = parsed?.outputFile ? path.resolve(String(parsed.outputFile)) : null;
    if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)
        || !Number.isInteger(count) || count < 0
        || (parsed.cursor != null && typeof parsed.cursor !== 'string')
        || (cursor !== null && !cursor.trim())) {
        throw new CommandExecutionError(`Twitter bookmarks resume file ${filePath} has an invalid shape`);
    }
    if (expected) {
        if (parsed.source !== expected.source)
            throw new ArgumentError(`Resume file source mismatch: expected ${expected.source}, found ${parsed.source || 'unknown'}`);
        if (outputFile !== expected.outputFile)
            throw new ArgumentError(`Resume file output mismatch: expected ${expected.outputFile || 'in-memory mode'}, found ${outputFile || 'in-memory mode'}`);
        if (!expected.outputFile && !Array.isArray(parsed.tweets))
            throw new CommandExecutionError(`Twitter bookmarks resume file ${filePath} is missing in-memory tweets`);
        if (!expected.outputFile && parsed.tweets.length !== count)

View on GitHub (pinned to 49907e53dc)