jackwener/OpenCLI · error · CommandExecutionError

Twitter bookmarks resume file ${filePath} has an invalid sha

Error message

Twitter bookmarks resume file ${filePath} has an invalid shape

What it means

After parsing, readResumeFile validates the resume file's shape: it must be a non-null, non-array object with an integer count >= 0, a cursor that is null or a non-empty string, and no blank-string cursor. A file that parses as JSON but fails this contract throws this CommandExecutionError.

Source

Thrown at clis/twitter/bookmarks.js:122

}
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)
            throw new CommandExecutionError(`Twitter bookmarks resume file ${filePath} count does not match its in-memory tweets`);
        if (parsed.complete)
            throw new CommandExecutionError(`Twitter bookmarks resume file ${filePath} is already marked complete`);
    }
    return {
        cursor,
        count,
        tweets: Array.isArray(parsed.tweets) ? parsed.tweets : [],
        complete: Boolean(parsed.complete),

View on GitHub (pinned to 49907e53dc)

Solutions

  1. Inspect the file and ensure it is an object with integer `count` (>=0) and optional string non-blank `cursor`.
  2. Delete the malformed file and restart the --all sync so the tool writes a fresh resume file.
  3. Verify you passed the resume file path, not the JSONL output-file path, to --resume-file.

Example fix

// before: resume.json = {"count": "5", "cursor": 123}
// after (valid shape)
{"count": 5, "cursor": "ABCDEF", "source": "bookmarks", "tweets": [], "complete": false}
Defensive patterns

Strategy: validation

Validate before calling

function resumeShapeOk(p) {
  const o = JSON.parse(fs.readFileSync(p, 'utf8'));
  return o && typeof o === 'object' && !Array.isArray(o)
    && Number.isInteger(o.count) && o.count >= 0
    && (o.cursor == null || (typeof o.cursor === 'string' && o.cursor.trim() !== ''));
}

Type guard

function looksLikeResumeState(v) {
  return typeof v === 'object' && v !== null && !Array.isArray(v)
    && Number.isInteger(v.count) && v.count >= 0;
}

Try / catch

try {
  await runBookmarks({ resumeFile });
} catch (e) {
  if (String(e.message).endsWith('has an invalid shape')) {
    fs.rmSync(resumeFile, { force: true });
    await runBookmarks({ resumeFile }); // fresh start
  } else throw e;
}

Prevention

When it happens

Trigger: Passing a --resume-file that contains valid JSON of the wrong structure — e.g. `{}`, an array `[]`, `"text"`, `"{"count":"5"}` (count not an integer), a negative count, a numeric cursor, or a cursor of `""` or whitespace.

Common situations: Pointing --resume-file at the JSONL output-file instead of the resume JSON; hand-editing the resume file and changing field types; an older/other command wrote a different resume schema to the same path.

Related errors


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