jackwener/OpenCLI · error · ArgumentError

twitter collection --until must be an RFC3339 timestamp

Error message

twitter collection --until must be an RFC3339 timestamp

What it means

normalizeUntil validates the --until flag against an RFC3339 timestamp regex before any numeric or Date checks; a string that doesn't match the RFC3339 shape fails immediately with usage guidance. This ensures the flag is parseable as an exact instant before further validation.

Source

Thrown at clis/twitter/collection.js:28

    DEFAULT_USER_TWEETS_PAGE_DELAY_SECONDS,
    MAX_USER_TWEETS_LIMIT,
    MAX_USER_TWEETS_PAGES,
    USER_TWEETS_PAGE_SIZE,
    fetchUserTimelinePage,
    resolveUserTimelineContext,
} from './user-timeline.js';

const RFC3339_TIMESTAMP = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d+))?(Z|([+-])(\d{2}):(\d{2}))$/;

function isLeapYear(year) {
    return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
}

function normalizeUntil(raw) {
    const value = String(raw ?? '').trim();
    const match = value.match(RFC3339_TIMESTAMP);
    if (!match) {
        throw new ArgumentError(
            'twitter collection --until must be an RFC3339 timestamp',
            'Example: opencli twitter collection @jack --until 2026-07-23T00:00:00Z',
        );
    }
    const [, yearRaw, monthRaw, dayRaw, hourRaw, minuteRaw, secondRaw, , zone, , offsetHourRaw, offsetMinuteRaw] = match;
    const year = Number(yearRaw);
    const month = Number(monthRaw);
    const day = Number(dayRaw);
    const hour = Number(hourRaw);
    const minute = Number(minuteRaw);
    const second = Number(secondRaw);
    const offsetHour = zone === 'Z' ? 0 : Number(offsetHourRaw);
    const offsetMinute = zone === 'Z' ? 0 : Number(offsetMinuteRaw);
    const daysInMonth = month === 2
        ? (isLeapYear(year) ? 29 : 28)
        : ([4, 6, 9, 11].includes(month) ? 30 : 31);
    if (
        month < 1 || month > 12

View on GitHub (pinned to 49907e53dc)

Solutions

  1. Pass a full RFC3339 timestamp such as 2026-07-23T00:00:00Z
  2. Include a timezone (Z or ±hh:mm offset) — date-only strings will not match
  3. Convert from your local/epoch format first, e.g. new Date(...).toISOString()
  4. Use the suggested example from the error hint verbatim as a template

Example fix

// before
--until 2026-07-23
// after
--until 2026-07-23T00:00:00Z
Defensive patterns

Strategy: validation

Validate before calling

function isRfc3339(s) {
  return typeof s === 'string' && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/.test(s.trim());
}
if (!isRfc3339(until)) throw new Error(`--until must be RFC3339, got: ${until}`);

Type guard

function isRfc3339Timestamp(v) {
  return typeof v === 'string' && !Number.isNaN(new Date(v).getTime()) && /^\d{4}-\d{2}-\d{2}T/.test(v);
}

Try / catch

try {
  await run(['opencli','twitter','collection',handle,'--until',until]);
} catch (err) {
  if (String(err.message).includes('--until must be an RFC3339 timestamp')) {
    until = new Date(until).toISOString(); // retry with canonical form
  } else throw err;
}

Prevention

When it happens

Trigger: Calling `opencli twitter collection @user --until` with a value that fails the RFC3339_TIMESTAMP regex, e.g. '2026-07-23', 'yesterday', '2026/07/23 00:00', or an empty string.

Common situations: Users typing date-only values or human phrases like 'now'/'last week'; shell scripts interpolating epoch integers; copy-pasted timestamps with locale formats or missing timezone designators.

Related errors


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