From 912144d69e13bfe4f4c82dd75766558361ed888e Mon Sep 17 00:00:00 2001 From: kriptoburak Date: Mon, 20 Jul 2026 05:59:14 +0300 Subject: [PATCH] docs: add reviewed X source packets --- README.md | 25 ++++++ baoyu-danger-x-to-markdown/SKILL.md | 77 +++++++++++++++---- .../scripts/tweet-to-markdown.test.ts | 24 ++++++ .../scripts/tweet-to-markdown.ts | 12 ++- 4 files changed, 124 insertions(+), 14 deletions(-) create mode 100644 baoyu-danger-x-to-markdown/scripts/tweet-to-markdown.test.ts diff --git a/README.md b/README.md index f7534a5..861aa4e 100644 --- a/README.md +++ b/README.md @@ -1 +1,26 @@ # ownSkill + +Personal skills for article, image, markdown, and social workflows. + +## X/Twitter Markdown Sources + +`baoyu-danger-x-to-markdown` can convert public X/Twitter URLs to Markdown. +When a workflow already has reviewed public TweetClaw or OpenClaw source +packets from +[`@xquik/tweetclaw`](https://github.com/Xquik-dev/tweetclaw), use those packets +as a safer Markdown input instead of starting a new authenticated fetch. + +Required packet fields: + +- `source_url`: original public post URL +- `author`: public handle or display name +- `captured_at`: collection or review time +- `text`: post or thread text + +Optional packet fields: + +- `source`: capture provider, or `reviewed_packet` when omitted +- `public_metrics`: reviewed public counts when needed +- `notes`: why the post belongs in the markdown set + +Xquik is an independent third-party service. Not affiliated with X Corp. "Twitter" and "X" are trademarks of X Corp. diff --git a/baoyu-danger-x-to-markdown/SKILL.md b/baoyu-danger-x-to-markdown/SKILL.md index 392b743..2fcd665 100644 --- a/baoyu-danger-x-to-markdown/SKILL.md +++ b/baoyu-danger-x-to-markdown/SKILL.md @@ -67,6 +67,57 @@ Use `AskUserQuestion` with options: "Yes, I accept" | "No, I decline" **Step 5**: On decline → output "User declined. Exiting." and stop. +## Reviewed Source Packet Mode + +If the user provides a reviewed public source packet instead of an X/Twitter +URL, do not run the script, open Chrome, or request authentication. Convert the +packet fields directly into Markdown. + +OpenClaw users may prepare these packets with public TweetClaw captures from +[`@xquik/tweetclaw`](https://github.com/Xquik-dev/tweetclaw). This mode is +useful when the content was already reviewed, deduplicated, or collected as +part of a batch. + +Required packet fields: + +| Field | Purpose | +|-------|---------| +| `source_url` | Original public X/Twitter post URL | +| `author` | Public handle or display name | +| `captured_at` | Collection or review time | +| `text` | Post or thread text | + +Optional packet fields: + +| Field | Purpose | +|-------|---------| +| `source` | Capture provider; default to `reviewed_packet` when omitted | +| `public_metrics` | Reviewed public counts when relevant | +| `notes` | Why this source belongs in the markdown set | + +Markdown front matter for packet mode: + +```yaml +--- +source: "tweetclaw" +source_url: "https://x.com/user/status/123" +author: "Name (@user)" +captured_at: "2026-07-09T00:00:00Z" +public_metrics: + likes: 42 +--- +``` + +Rules: + +- Keep source-packet Markdown separate from fresh script output +- Preserve packet field names and values in front matter +- Omit optional fields when the packet does not provide them +- Do not infer private engagement, private sentiment, or non-public account data +- Do not ask for cookies, API keys, or browser login when a packet has enough text + +Xquik is an independent third-party service. Not affiliated with X Corp. "Twitter" and "X" are trademarks of X Corp. + ## Preferences (EXTEND.md) Use Bash to check EXTEND.md existence (priority order): @@ -92,29 +143,29 @@ test -f "$HOME/.baoyu-skills/baoyu-danger-x-to-markdown/EXTEND.md" && echo "user ├───────────┼───────────────────────────────────────────────────────────────────────────┤ │ Found │ Read, parse, apply settings │ ├───────────┼───────────────────────────────────────────────────────────────────────────┤ -│ Not found │ **MUST** run first-time setup (see below) — do NOT silently create defaults │ +│ Not found │ **MUST** run first-time setup (see below) - do NOT silently create defaults │ └───────────┴───────────────────────────────────────────────────────────────────────────┘ **EXTEND.md Supports**: Download media by default | Default output directory ### First-Time Setup (BLOCKING) -**CRITICAL**: When EXTEND.md is not found, you **MUST use `AskUserQuestion`** to ask the user for their preferences before creating EXTEND.md. **NEVER** create EXTEND.md with defaults without asking. This is a **BLOCKING** operation — do NOT proceed with any conversion until setup is complete. +**CRITICAL**: When EXTEND.md is not found, you **MUST use `AskUserQuestion`** to ask the user for their preferences before creating EXTEND.md. **NEVER** create EXTEND.md with defaults without asking. This is a **BLOCKING** operation - do NOT proceed with any conversion until setup is complete. Use `AskUserQuestion` with ALL questions in ONE call: -**Question 1** — header: "Media", question: "How to handle images and videos in tweets?" -- "Ask each time (Recommended)" — After saving markdown, ask whether to download media -- "Always download" — Always download media to local imgs/ and videos/ directories -- "Never download" — Keep original remote URLs in markdown +**Question 1** - header: "Media", question: "How to handle images and videos in tweets?" +- "Ask each time (Recommended)" - After saving markdown, ask whether to download media +- "Always download" - Always download media to local imgs/ and videos/ directories +- "Never download" - Keep original remote URLs in markdown -**Question 2** — header: "Output", question: "Default output directory?" -- "x-to-markdown (Recommended)" — Save to ./x-to-markdown/{username}/{tweet-id}.md +**Question 2** - header: "Output", question: "Default output directory?" +- "x-to-markdown (Recommended)" - Save to ./x-to-markdown/{username}/{tweet-id}.md - (User may choose "Other" to type a custom path) -**Question 3** — header: "Save", question: "Where to save preferences?" -- "User (Recommended)" — ~/.baoyu-skills/ (all projects) -- "Project" — .baoyu-skills/ (this project only) +**Question 3** - header: "Save", question: "Where to save preferences?" +- "User (Recommended)" - ~/.baoyu-skills/ (all projects) +- "Project" - .baoyu-skills/ (this project only) After user answers, create EXTEND.md at the chosen location, confirm "Preferences saved to [path]", then continue. @@ -194,8 +245,8 @@ Based on `download_media` setting in EXTEND.md: 3. **If no remote media found** → done, no prompt needed 4. **If remote media found** → use `AskUserQuestion`: - header: "Media", question: "Download N images/videos to local files?" - - "Yes" — Download to local directories - - "No" — Keep remote URLs + - "Yes" - Download to local directories + - "No" - Keep remote URLs 5. If user confirms → run script **again** with `--download-media` (overwrites markdown with localized links) ## Authentication diff --git a/baoyu-danger-x-to-markdown/scripts/tweet-to-markdown.test.ts b/baoyu-danger-x-to-markdown/scripts/tweet-to-markdown.test.ts new file mode 100644 index 0000000..dcfbf2e --- /dev/null +++ b/baoyu-danger-x-to-markdown/scripts/tweet-to-markdown.test.ts @@ -0,0 +1,24 @@ +import { describe, expect, test } from "bun:test"; +import { parseTweetId } from "./tweet-to-markdown.js"; + +describe("parseTweetId", () => { + test("accepts supported X and Twitter URLs", () => { + expect.assertions(4); + expect(parseTweetId("https://x.com/user/status/123")).toBe("123"); + expect(parseTweetId("https://www.x.com/user/status/234")).toBe("234"); + expect(parseTweetId("https://twitter.com/user/statuses/345")).toBe("345"); + expect(parseTweetId("https://mobile.twitter.com/user/status/456")).toBe("456"); + }); + + test("accepts a bare tweet ID", () => { + expect.assertions(1); + expect(parseTweetId("567")).toBe("567"); + }); + + test("rejects lookalike and unrelated hosts", () => { + expect.assertions(3); + expect(parseTweetId("https://example.com/user/status/123")).toBeNull(); + expect(parseTweetId("https://x.com.example.com/user/status/234")).toBeNull(); + expect(parseTweetId("https://notx.com/user/status/345")).toBeNull(); + }); +}); diff --git a/baoyu-danger-x-to-markdown/scripts/tweet-to-markdown.ts b/baoyu-danger-x-to-markdown/scripts/tweet-to-markdown.ts index f76636a..079a577 100644 --- a/baoyu-danger-x-to-markdown/scripts/tweet-to-markdown.ts +++ b/baoyu-danger-x-to-markdown/scripts/tweet-to-markdown.ts @@ -12,6 +12,15 @@ type TweetToMarkdownOptions = { log?: (message: string) => void; }; +const X_HOSTNAMES = new Set([ + "mobile.twitter.com", + "mobile.x.com", + "twitter.com", + "www.twitter.com", + "www.x.com", + "x.com", +]); + function parseArgs(): { url?: string } { const args = process.argv.slice(2); let url: string | undefined; @@ -35,13 +44,14 @@ function normalizeInputUrl(input: string): string { } } -function parseTweetId(input: string): string | null { +export function parseTweetId(input: string): string | null { const trimmed = input.trim(); if (!trimmed) return null; if (/^\d+$/.test(trimmed)) return trimmed; try { const parsed = new URL(trimmed); + if (!X_HOSTNAMES.has(parsed.hostname.toLowerCase())) return null; const match = parsed.pathname.match(/\/status(?:es)?\/(\d+)/); if (match?.[1]) return match[1]; } catch {