← back to Tiktok Architect

scripts/tt-list.js

81 lines

#!/usr/bin/env node
/**
 * tt-list.js — list a TikTok creator's most-recent videos and flag the ones
 * that are actually about Claude Code / AI-coding (the "what's new" scan).
 *
 * @tiktok_architect posts a MIXED feed — motorcycles, hobbies, privacy, AND
 * Claude Code / vibecoding tips. This enumerates recent uploads via yt-dlp's
 * flat playlist (id + caption, no download, $0) and marks each `claude_relevant`
 * by hashtag/keyword so the scout only spends effort on the on-topic ones.
 *
 * Usage:
 *   node tt-list.js [handle] [count]
 *     handle  TikTok handle without @  (default: tiktok_architect)
 *     count   how many newest videos to list (default: 12)
 *
 * Requires yt-dlp on PATH. TikTok may throttle; on failure this exits non-zero
 * with a hint to fall back to the mirrors in references/retrieval-playbook.md.
 * yt-dlp prints an "attempting impersonation" WARNING even on success — that is
 * NOT an error. If downloads later fail, install a curl_cffi impersonate target.
 *
 * Output: JSON array on stdout, newest first:
 *   [{ id, url, caption, hashtags[], claude_relevant }]
 */

'use strict';

const { spawnSync } = require('child_process');

const RELEVANCE_RE =
  /\b(claude|claudecode|claude-code|anthropic|vibecod|cursor|codex|copilot|chatgpt|openai|\bllm\b|\bai\b|aitools|aiagent|agentic|mcp|prompt|coding|developer|softwareengineer|programming)\b/i;

function hashtagsFrom(caption) {
  const tags = new Set();
  const re = /#([\p{L}\p{N}_]+)/gu;
  let m;
  while ((m = re.exec(caption || ''))) tags.add(m[1].toLowerCase());
  return [...tags];
}

function main() {
  const handle = (process.argv[2] || 'tiktok_architect').replace(/^@/, '');
  const count = Math.max(1, parseInt(process.argv[3] || '12', 10) || 12);
  const url = `https://www.tiktok.com/@${handle}`;

  const r = spawnSync(
    'yt-dlp',
    ['--flat-playlist', '--playlist-end', String(count), '--dump-json', url],
    { encoding: 'utf8', maxBuffer: 64 * 1024 * 1024, timeout: 90_000 }
  );

  if (r.error && r.error.code === 'ENOENT') {
    console.error('yt-dlp not found on PATH. brew install yt-dlp — or use the mirror path in references/retrieval-playbook.md.');
    process.exit(2);
  }

  const rows = [];
  for (const line of (r.stdout || '').split('\n')) {
    if (!line.trim()) continue;
    let d;
    try { d = JSON.parse(line); } catch { continue; }
    const caption = d.title || d.description || '';
    rows.push({
      id: d.id || null,
      url: d.url || (d.id ? `https://www.tiktok.com/@${handle}/video/${d.id}` : null),
      caption,
      hashtags: hashtagsFrom(caption),
      claude_relevant: RELEVANCE_RE.test(caption),
    });
  }

  if (!rows.length) {
    console.error('yt-dlp returned 0 videos (throttle/block or empty feed). Fall back to mirrors — do NOT conclude "no new tips".');
    if (r.stderr) console.error(r.stderr.split('\n').slice(-3).join('\n'));
    process.exit(3);
  }

  console.log(JSON.stringify(rows, null, 2));
}

main();