From tdoc
Converts prompts into self-contained HTML documents with inline commenting, local server preview, and free Cloudflare Worker sharing. Supports RFCs, explainers, product specs, post-mortems, and research write-ups.
How this skill is triggered — by the user, by Claude, or both
Slash command
/tdoc:tdocThis skill is limited to the following tools:
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
Open-source, collaborative take on Jesse Pollak's bdocs. Docs are HTML build
Open-source, collaborative take on Jesse Pollak's bdocs. Docs are HTML build artifacts, not files the user maintains. Authoring interface is a prompt. Every edit creates a new version. Comments anchor to highlighted text or to artifacts (images, SVG, canvas, video) and are used to regenerate the next version. Each user publishes to their own Cloudflare Worker for free always-on sharing, with GitHub auth gating comments.
~/tdocs/
<slug>/
meta.json # { title, created, versions: [...] }
v1/index.html
v2/index.html
comments.json # [{ id, version, anchor, text, status }]
Server runs at http://localhost:7878 and serves:
/ — index of all docs/d/<slug>/v/<n> — a specific version (injects comment overlay)/api/comments GET/POST — comment persistenceTDOC_DIR="${TDOC_DIR:-$HOME/tdocs}"
# Resolve the skill dir for whichever host installed it: Claude Code
# (~/.claude/skills/tdoc) or Codex (~/.codex/skills/tdoc). Honor an explicit
# TDOC_SKILL_DIR override if set. Claude's location is checked first, so its
# behavior is unchanged.
SKILL_DIR="${TDOC_SKILL_DIR:-}"
[ -z "$SKILL_DIR" ] && for d in "$HOME/.claude/skills/tdoc" "$HOME/.codex/skills/tdoc"; do
[ -f "$d/SKILL.md" ] && SKILL_DIR="$d" && break
done
SKILL_DIR="${SKILL_DIR:-$HOME/.claude/skills/tdoc}"
mkdir -p "$TDOC_DIR"
# Check server is running
if curl -sf http://localhost:7878/api/ping >/dev/null 2>&1; then
echo "SERVER_OK"
else
echo "SERVER_DOWN"
fi
If server is down, start it:
nohup node "$SKILL_DIR/server/server.js" > "$TDOC_DIR/.server.log" 2>&1 &
sleep 1
/tdoc new <prompt> — create a new doc~/tdocs/<slug>/v1/index.html — a fully self-contained HTML file:
<style>, all JS inline in <script>.meta.json:
{ "title": "...", "slug": "...", "created": "<iso>", "versions": [{ "n": 1, "created": "<iso>", "prompt": "..." }] }
comments.json as [].http://localhost:7878/d/<slug>/v/1 in the browser:
open "http://localhost:7878/d/<slug>/v/1"
bin/tdoc-new — programmatic entry for agents in other skillsThis is the contract OTHER skills (/document-release, /retro,
/investigate, /cso, /qa-only, /office-hours, /plan-*, etc.)
use when an agent inside them is about to emit a doc-shaped artifact.
The human-facing /tdoc new flow is a chat-driven prompt → HTML
generation. bin/tdoc-new is the other direction: the calling agent
already has the finished HTML and just wants tdoc to scaffold storage,
serve it locally, and (optionally) publish.
When to use it: any time inside another skill you would otherwise
have written cat > some-report.md <<EOF ... with more than a couple
paragraphs of structured content. Generate the doc as HTML (use the
template + styling rules from the /tdoc new section above), then
hand it off:
HTML_FILE=$(mktemp -t tdoc-handoff.XXXXXX.html)
cat > "$HTML_FILE" <<'HTML'
<!doctype html>
<html lang="en">
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<title>...</title></head>
<body><div class="wrap">
<h1>...</h1>
<!-- sections; tag author-composed wrappers data-tdoc-artifact
wherever you want a comment surface -->
</div></body>
</html>
HTML
TDOC_NEW_CALLER=document-release \
~/.claude/skills/tdoc/bin/tdoc-new \
--slug "release-notes-$(date +%Y%m%d)" \
--title "Release notes — $(date +%Y-%m-%d)" \
--html-file "$HTML_FILE" \
--publish
Args:
--slug <kebab-case> (required) — slug for ~/tdocs/<slug>/.--title "<title>" (required) — recorded in meta.json.--html-file <path> OR --html-stdin (required) — full HTML for v1.--prompt "<one-line>" — prompt-of-record in meta.json (defaults
to Imported via tdoc-new by <caller>).--publish — also run tdoc-publish so a shareable URL is returned.--open — open the resulting URL in the default browser.--quiet — suppress informational output (the URL is still printed
on the last line so callers can capture it).--force — overwrite an existing slug. Without this, an existing
slug is a hard error (no silent clobber).Output contract: the local URL is always the last line on stdout.
If --publish succeeded, the published URL appears on a second line.
This is what callers should tail -n 1 (or tail -n 2) to capture.
Guards built in: refuses to clobber existing slugs without --force;
validates that input contains a <body> tag (catches markdown handed
in by mistake); restarts the local server if it's down so the URL is
immediately reachable.
Set TDOC_NEW_CALLER (or rely on CLAUDE_SKILL_NAME) so meta.json
records which skill scaffolded the doc — useful for later auditing or
for /tdoc list to show provenance.
/tdoc edit <slug> [<extra prompt>] — new version from commentsYou MUST report back on every open comment — applied, partial, or unclear. This is a hard requirement, not a suggestion. The user can't tell which comments you handled unless you reply on each one. Skipping comments silently is the #1 source of regression complaints.
Read ~/tdocs/<slug>/comments.json — filter to status: "open".
Read latest version's index.html.
For EACH open comment, decide one of three outcomes BEFORE writing:
Regenerate as v<n+1>/index.html incorporating every applied and
partial comment. A comment's anchor has:
anchor.text — the exact text the user highlighted (may span across
paragraphs and inline elements)anchor.context_before / anchor.context_after — surrounding text
(~60 chars each side) for disambiguation when the same text appears
multiple timesAppend to meta.json versions array.
For each comment, post an agent reply so the user sees the outcome in the doc UI. This is mandatory.
For published docs — POST to https://<your-worker>/api/agent/reply
with the upload token from ~/.tdoc/published.json:
TOKEN=$(jq -r .upload_token ~/.tdoc/published.json)
WORKER=$(jq -r '.worker + "." + .subdomain' ~/.tdoc/published.json)
curl -sS -X POST "https://${WORKER}.workers.dev/api/agent/reply" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "{\"slug\":\"<slug>\",\"parent_id\":\"<comment_id>\",\"text\":\"<one or two sentences>\",\"status\":\"applied\",\"applied_in\":<n+1>}"
For local-only docs — POST to http://localhost:7878/api/agent/reply
(no token needed).
The reply text should be specific:
Update comments.json: set status: "applied" (or leave "open" for
partial/question) and applied_in: n+1. The agent-reply endpoint
already flips the status server-side AND drops a status emoji on the
parent comment (✅ applied, 🟡 partial, ❓ question), clearing any
previous agent emoji first. You don't need to send a separate reaction
request — the reply endpoint does it. Users see the verdict at a
glance from the comment cards without expanding replies.
If a comment is later re-anchored by the user (anchor moved to new
text), the server automatically clears the agent's emoji and resets
status: "open". Re-running /tdoc edit will pick it up again.
Open http://localhost:7878/d/<slug>/v/<n+1>.
If there are zero open comments AND no extra prompt, ask the user what to change before doing anything.
/tdoc fork <slug> [<new-slug>] — copy a doccp -R "$TDOC_DIR/<slug>" "$TDOC_DIR/<new-slug>"
Reset comments.json to []. Update meta.json title to include (fork).
/tdoc list — show all docsRead each meta.json and print: slug, title, latest version, # open comments.
/tdoc serve — (re)start the serverpkill -f "$SKILL_DIR/server/server.js" 2>/dev/null
nohup node "$SKILL_DIR/server/server.js" > "$TDOC_DIR/.server.log" 2>&1 &
echo "tdoc server: http://localhost:7878"
/tdoc stop — stop the serverpkill -f "$SKILL_DIR/server/server.js"
/tdoc publish <slug> — publish to your Cloudflare WorkerPublishes the latest version of <slug> to a public URL.
Local always stays $0/anonymous; publishing is opt-in. First run does a one-time
setup: prompts wrangler login, creates an R2 bucket (tdoc-docs) and KV
namespace (META) in your Cloudflare account, generates an upload token, and
deploys your own Worker. Config is saved to ~/.tdoc/published.json.
Subsequent runs upload the latest version of <slug>. The script also detects
when server/overlay.js or worker/worker.js is newer than the bundled file
and redeploys the Worker automatically so users get the latest overlay code.
Set TDOC_SKIP_WORKER_DEPLOY=1 to skip the redeploy (useful for batch uploads).
On published docs, viewers sign in with GitHub (Device Flow, shared OAuth App
Ov23liZ1UAGOchvKPmlS, scope read:user) before commenting.
Requires wrangler (npm i -g wrangler) and jq.
"$SKILL_DIR/bin/tdoc-publish" <slug>
Prints the published URL: https://<worker>.<subdomain>.workers.dev/d/<slug>/v/<N>.
/tdoc pull <slug> — pull comments from the published docOverwrites local ~/tdocs/<slug>/comments.json with comments collected on the
published Worker. Run before /tdoc edit to regenerate using community feedback.
"$SKILL_DIR/bin/tdoc-pull" <slug>
/tdoc unpublish <slug> — remove from your WorkerDeletes all versions, meta, and comments for <slug> from R2/KV. Local files
are untouched.
"$SKILL_DIR/bin/tdoc-unpublish" <slug>
/tdoc onboard — guided first-time setupYou are walking a user through tdoc onboarding. The user might have nothing
installed, or might be partway through. You must drive the flow from
bin/tdoc-doctor JSON output, not assume state.
Algorithm:
"$SKILL_DIR/bin/tdoc-doctor" and parse the JSON. This is non-destructive..ready_to_publish == true AND .published.ok == true → tell the user
they are fully set up, and offer to run /tdoc new <prompt> or to test
publishing with a sample doc..ready_to_publish == true AND .published.ok == false → they have all
deps but haven't published yet. Offer to create a quick sample doc with
/tdoc new and then /tdoc publish it..missing_steps in order. For each step:
cmd for them via Bash (e.g. npm i -g wrangler,
brew install jq). After install, re-run tdoc-doctor to confirm.cmd.
wrangler login is interactive — print clear instructions and wait.tdoc-doctor to verify.tdoc-doctor and continue from the new state..ready_to_publish == true, congratulate and offer to create + publish
a sample doc.Important behavioral rules:
Ov23liZ1UAGOchvKPmlS) is already baked
into the Worker — users do NOT register their own./tdoc update — check for updates and pull the latestWraps bin/tdoc-update. Runs git fetch + git merge --ff-only against
origin/main of serenakeyitan/tdoc.
tdoc-update --check → report-only, prints incoming commits without changing anythingtdoc-update → apply, with auto-stash of local edits, auto-restarts the running local server so new routes / overlay code take effecttdoc-update --yes → also redeploy the Worker so users see new overlay code"$SKILL_DIR/bin/tdoc-update" --check # see what's new
"$SKILL_DIR/bin/tdoc-update" # apply
"$SKILL_DIR/bin/tdoc-update" --yes # apply + redeploy worker
If the user has not yet git clone'd (the skill dir is not a git checkout),
the script prints a clean instruction to re-clone.
/tdoc doctor — health check, no changesPrints the doctor JSON. Use this when the user reports a problem to localize which dep / Cloudflare resource is missing.
"$SKILL_DIR/bin/tdoc-doctor" | jq .
When the user reports a problem, check these first:
/api/publish 404, or "string did not match the expected pattern" in the Publish modal → the running server is stale (old process, doesn't have current routes). Restart it: pkill -f "$SKILL_DIR/server/server.js" && nohup node "$SKILL_DIR/server/server.js" > "$TDOC_DIR/.server.log" 2>&1 &. /tdoc update now auto-restarts, but a server that was started before the update is still running stale code until restarted.ui.test.js "Drag-to-select TEXT in a <p> opens the comment popup"). If the test fails, check overlay.js mouseup handler: the if (dragged) { ... return; } block must only return when an artifact was actually hit.~/tdocs/.server.log; usually wrangler login is waiting for browser auth or R2 isn't enabled.system-ui, -apple-system, "Segoe UI", Roboto, sans-serif. Mono: ui-monospace, "SF Mono", Menlo, monospace.The overlay injects a complete default template modeled after the conway-life doc ("What if a doc could think?"): tight, readable, system fonts only.
system-ui, -apple-system, "Segoe UI", Roboto, sans-serif)#111 on white#111 left rule, #f5f6f8 background-ish quoted block (mono pre)Don't write your own CSS for these unless the doc genuinely needs a different aesthetic (a presentation, a landing page, a doc with custom widgets). Reading docs, essays, and reports should not override the template.
What to write:
<!doctype html>
<html lang="en"><head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{title}</title>
</head><body>
<div class="wrap">
<h1>{title}</h1>
<p class="meta">{subtitle or attribution}</p>
<!-- content here using plain <h2>, <h3>, <p>, <ul>, <pre>, <table>, etc. -->
</div>
<script>
/* any interactivity, inline */
</script>
</body></html>
The overlay's :where() defaults handle:
max-width: 720px, padded)Only add CSS for doc-specific content (a custom widget, a simulation, a chart). When you do, scope it tightly (e.g. .my-slider { ... }), not body p { ... }.
Wrap the doc content in a single container element with one of these selectors: .wrap (preferred), main, article, .content, or .container. The overlay relies on this to:
Note: the container should not have margin: 0 auto. The overlay sets its margins itself based on comment state (overrides with !important if you write it).
Always set body { background: #fff; } (or your chosen color) so the page doesn't render as transparent over the browser default. Light mode only; the overlay does not currently support dark mode.
Every doc must work on mobile out of the box. The overlay injects defensive CSS for artifacts, but the doc itself should also be authored responsively:
<meta name="viewport" content="width=device-width, initial-scale=1"> in <head>. (The overlay injects this if you forget, but include it.)max-width: 720px; padding: 0 24px; (no margin: 0 auto — overlay handles it).width="100%" + CSS aspect-ratio (aspect-ratio: 16/9), ormax-width: 100% and let the artifact scale.width and height attributes for the drawing buffer but ALSO style="max-width:100%;height:auto" so it scales down on narrow screens. Recompute the drawing buffer on resize if needed.<div style="overflow-x:auto"> so they scroll instead of overflowing.<pre>): max-width: 100%; overflow-x: auto;.The overlay applies these as :where() defensive defaults so old docs degrade gracefully, but new docs should bake responsiveness in.
button:hover { background: ... } globally — it will override the overlay's Comment pill on artifacts. Scope hover rules to your own buttons (e.g. .my-btn:hover, or .wrap button:hover).tdoc-*, #tdoc-*, and any class starting with tdoc-./tdoc edit)The system handles this for you. Element anchors are identity-based, not path-based: at publish time, the Worker stamps every commentable artifact with a content-hashed data-tdoc-aid attribute. The set of commentable artifacts:
img, svg, canvas, video, pre, figure, iframe[src]section, aside, blockquote, table, details (article is intentionally excluded — it's a content-root pattern; using it would make the whole doc one artifact)data-tdoc-artifact or with class containing tdoc-artifactThe same artifact in any future version gets the same aid, regardless of how the HTML around it is restructured. Comments anchor by aid; resolution is identity-first. If an aid disappears from the new version, the Worker marks the comment kind: "lost" so it renders unanchored — it will never silently re-attach to a different artifact.
If your doc has a "card" or composite widget built from <div>s (a transcript panel, a comparison card, a custom interactive widget), it won't be commentable as a unit by default — the overlay sees its inner text, not the card. Two ways to fix:
<div class="my-card"> to <section class="my-card"> (or <aside>, <details> if appropriate). Automatic — no other change needed.data-tdoc-artifact:
<div class="my-card" data-tdoc-artifact>…composite content…</div>
Or use a class containing tdoc-artifact. Works on any tag.Both paths give the block a stable aid and the full hover-to-comment affordance, identical to the media-leaf experience.
You generally don't need to do anything special when regenerating — the aid stamping is automatic on /tdoc publish. But it's still polite to:
viewBox, src, alt, aria-label, title) + normalized inner content. Trivial whitespace changes don't matter; replacing an SVG with an entirely different one does (and that's the right behavior — the comments were about the old artifact).Comments are persisted with one of two anchor shapes:
// text anchor
{ "id": "c_<ts>", "version": 1, "text": "what the user wrote",
"status": "open", "created": "<iso>",
"anchor": { "kind": "text", "text": "exact highlighted text",
"context_before": "...", "context_after": "..." } }
// element (artifact) anchor — IDENTITY-BASED
{ "id": "c_<ts>", "version": 1, "text": "what the user wrote",
"status": "open", "created": "<iso>",
"anchor": { "kind": "element",
"aid": "<content-hash>", // ← primary key: the worker-stamped
// data-tdoc-aid on the artifact.
// Same artifact across versions = same aid.
"selector": "[data-tdoc-aid=\"...\"]", // mirror of aid; legacy
// comments may still have
// a positional selector.
"label": "svg", // tag hint
"fingerprint": { ... }, // legacy content fingerprint
"fallback": { "ratio": ..., "nearestHeading": ... } } }
// lost-anchor — the Worker's publish-time reconciliation marks an element
// comment lost when its aid disappears or can't be resolved unambiguously.
// Renders as "unanchored" in the margin; never silently re-attached.
{ ..., "anchor": { "kind": "lost", "reason": "aid not found in version" } }
Text anchors: find the anchor text in the current HTML and apply the change. If the text no longer exists, apply as a general directive.
Element anchors: identity is the aid — the Worker auto-stamps
data-tdoc-aid="<content-hash>" on every commentable artifact at publish
time, and reconciles existing anchors against the new artifact set on every
upload. You don't have to preserve ids manually; just regenerate the doc
naturally. Comments on unchanged artifacts stay anchored; comments on
artifacts you genuinely replaced go kind: "lost" automatically.
Treat this skill file as executable instructions, not reference
material. Every tdoc invocation runs as a bracketed sequence:
Step 0 (telemetry preamble) → the tdoc work (pull / edit / publish /
new / fix comments) → Final Step (telemetry record). All three run,
in order, every single time.
Skipping either is a regression. A tdoc run is NOT complete until the Final Step telemetry event is logged. This applies even in plan mode — the telemetry commands are local writes and ALWAYS run.
Your default efficiency habit is to read a skill file, find the part relevant to the user's request, do that, and stop. For tdoc, suppress that habit. Step 0 and the Final Step are not "relevant or not" — they are unconditional brackets around the work. A request for "just a small edit" or "fix the comment" does NOT exempt the run from the bracket. A run that publishes the doc but skips Step 0 or the Final Step is an incomplete, broken run — even though the user sees a finished doc. Publishing the doc is necessary but not sufficient; logging the Final Step telemetry event is what makes the run actually complete. Do not declare the task done before the Final Step has run.
This block records anonymous usage data so the tdoc maintainer can see which features people actually use and what breaks. Default-on with first-run consent prompt. Three opt-out paths described at the bottom.
TEL_HOME="$HOME/.tdoc"
TEL_PROMPTED_FLAG="$TEL_HOME/.telemetry-prompted"
TEL_CONFIG_FILE="$TEL_HOME/.telemetry-mode"
mkdir -p "$TEL_HOME"
# Read existing choice. Priority: env var > persisted file > default "on"
TEL_MODE_PERSISTED="$(cat "$TEL_CONFIG_FILE" 2>/dev/null | tr -d ' \n\r')"
TEL_PROMPTED="no"
[ -f "$TEL_PROMPTED_FLAG" ] && TEL_PROMPTED="yes"
if [ -n "${SKILL_TELEMETRY:-}" ]; then
TEL_EFFECTIVE="$SKILL_TELEMETRY"
elif [ -n "$TEL_MODE_PERSISTED" ]; then
TEL_EFFECTIVE="$TEL_MODE_PERSISTED"
else
TEL_EFFECTIVE="on"
fi
# Session ID — Claude Code sets $CLAUDE_SESSION_ID in newer versions;
# fall back to a stable per-shell id so concurrent sessions don't
# overwrite each other's sentinel.
TEL_SESSION_ID="${CLAUDE_SESSION_ID:-shell-$$-$(date +%s)}"
# Write per-session sentinel (not one global file)
if [ "$TEL_EFFECTIVE" != "off" ]; then
mkdir -p "$TEL_HOME/sentinels"
date +%s > "$TEL_HOME/sentinels/$TEL_SESSION_ID"
find "$TEL_HOME/sentinels" -type f -mtime +1 -delete 2>/dev/null || true
# ── Self-healing pending marker (gstack pattern) ──
# Write a .pending marker for THIS session. The Final Step deletes it.
# If Claude skips the Final Step, this marker is left behind — and the
# reaper below records it as outcome=unknown on the next tdoc run, so
# a skipped run still produces a (degraded) event instead of vanishing.
PENDING_DIR="$TEL_HOME/telemetry/pending"
mkdir -p "$PENDING_DIR"
TEL_TS="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
printf '{"skill":"tdoc","ts":"%s","session_id":"%s"}\n' \
"$TEL_TS" "$TEL_SESSION_ID" > "$PENDING_DIR/.pending-$TEL_SESSION_ID"
# Reap stale markers from prior skipped runs (any session but ours)
for _PF in "$PENDING_DIR"/.pending-*; do
[ -f "$_PF" ] || continue
_PF_SID="$(basename "$_PF")"; _PF_SID="${_PF_SID#.pending-}"
[ "$_PF_SID" = "$TEL_SESSION_ID" ] && continue
_PDATA="$(cat "$_PF" 2>/dev/null || true)"
rm -f "$_PF" 2>/dev/null || true
[ -z "$_PDATA" ] && continue
_P_SKILL="$(echo "$_PDATA" | grep -o '"skill":"[^"]*"' | head -1 | cut -d'"' -f4)"
_P_SID="$(echo "$_PDATA" | grep -o '"session_id":"[^"]*"' | head -1 | cut -d'"' -f4)"
[ -z "$_P_SKILL" ] && continue
if [ -x "__TDOC_DIR__/telemetry/bin/telemetry-log" ]; then
"__TDOC_DIR__/telemetry/bin/telemetry-log" \
--skill "$_P_SKILL" --outcome unknown \
--step "reaped-incomplete-run" --session-id "$_P_SID" 2>/dev/null || true
fi
done
fi
# ─── Upgrade check (gstack-style lifecycle event) ───────────
# Check installed version against latest release. If stale, record
# upgrade_prompted event and tell the user (once per day, not nag).
# TDOC_DIR is substituted at install time by postinstall-telemetry.sh
# so this works no matter where tdoc is cloned.
TDOC_DIR="__TDOC_DIR__"
# Resolve installed version, trying multiple sources in order:
# 1. VERSION file (if maintained, like gstack)
# 2. git describe --tags (most recent reachable tag)
# 3. fallback "0.0.0" (skip the check)
INSTALLED_VERSION="$(cat "$TDOC_DIR/VERSION" 2>/dev/null)"
if [ -z "$INSTALLED_VERSION" ] && [ -d "$TDOC_DIR/.git" ]; then
INSTALLED_VERSION="$(cd "$TDOC_DIR" && git describe --tags --abbrev=0 2>/dev/null | sed 's/^v//')"
fi
[ -z "$INSTALLED_VERSION" ] && INSTALLED_VERSION="0.0.0"
UPGRADE_CHECK_FLAG="$TEL_HOME/.upgrade-checked-$(date +%Y-%m-%d)"
if [ "$TEL_EFFECTIVE" != "off" ] && [ ! -f "$UPGRADE_CHECK_FLAG" ] && [ "$INSTALLED_VERSION" != "0.0.0" ]; then
LATEST=$(curl -s --max-time 3 https://api.github.com/repos/serenakeyitan/tdoc/releases/latest 2>/dev/null | grep -oE '"tag_name"[[:space:]]*:[[:space:]]*"[^"]*"' | cut -d'"' -f4 | sed 's/^v//')
# Only fire upgrade prompt if installed is STRICTLY OLDER than latest.
# Use sort -V (version sort): if installed sorts first, installed < latest.
# If installed == latest or installed > latest (dev build), skip silently.
if [ -n "$LATEST" ] && [ "$LATEST" != "$INSTALLED_VERSION" ]; then
FIRST_VERSION=$(printf '%s\n%s\n' "$INSTALLED_VERSION" "$LATEST" | sort -V | head -1)
if [ "$FIRST_VERSION" = "$INSTALLED_VERSION" ]; then
"$TDOC_DIR/telemetry/bin/telemetry-log" \
--skill tdoc \
--event-type upgrade_prompted \
--outcome unknown \
--skill-version "$INSTALLED_VERSION" \
--step "v$INSTALLED_VERSION→v$LATEST" \
--session-id "$TEL_SESSION_ID" 2>/dev/null || true
echo "TDOC_UPGRADE_AVAILABLE: $INSTALLED_VERSION → $LATEST (cd $TDOC_DIR && git pull && bin/postinstall-telemetry.sh)"
fi
fi
touch "$UPGRADE_CHECK_FLAG" 2>/dev/null || true
fi
echo "TEL_PROMPTED: $TEL_PROMPTED"
echo "TEL_EFFECTIVE: $TEL_EFFECTIVE"
echo "TEL_SESSION_ID: $TEL_SESSION_ID"
echo "TDOC_VERSION: $INSTALLED_VERSION"
If TEL_PROMPTED is no (first time the user runs tdoc with
telemetry), ask the user ONCE with this text and two options:
tdoc can record when it runs, how it went (success/error/abandoned), how long it took, and a random ID for your machine, and send it to the tdoc maintainer's database. It does NOT record your tdoc content, your prompts, or anything else. Nothing is sent to Anthropic.
A) Help tdoc improve — sends events (recommended) B) Off — nothing leaves your machine
Change anytime: edit
~/.tdoc/.telemetry-mode, or setSKILL_TELEMETRY=off.
How to ask depends on your host: if the AskUserQuestion tool is
available (Claude Code), use it with the two options above — this is
the normal path and is unchanged. If it is NOT available (e.g. Codex
or any other host without that tool), present the same text as plain
prose and wait for the user's typed reply (A/B). Either way, record
their choice the same.
After they pick, record the choice:
echo "MODE_FROM_USER" > "$TEL_CONFIG_FILE" # "on" or "off"
touch "$TEL_PROMPTED_FLAG"
TEL_EFFECTIVE="$(cat "$TEL_CONFIG_FILE")"
If TEL_PROMPTED is yes, do NOT ask again. Proceed silently.
This is the last required step of every tdoc run. When the work is done — success, error, or abandoned — you MUST record one telemetry event before telling the user the task is complete.
PLAN MODE EXCEPTION — ALWAYS RUN: local writes + background sync; these always run, even in plan mode.
"The doc is published" is NOT the end of the skill. The end of the skill is after this telemetry event fires. A tdoc run that publishes a doc but logs no Final Step event is an incomplete run — a regression.
TEL_HOME="$HOME/.tdoc"
TEL_SESSION_ID="${CLAUDE_SESSION_ID:-${TEL_SESSION_ID:-shell-$$-$(date +%s)}}"
END=$(date +%s)
START=$(cat "$TEL_HOME/sentinels/$TEL_SESSION_ID" 2>/dev/null || echo "$END")
DURATION=$(( END - START ))
rm -f "$TEL_HOME/sentinels/$TEL_SESSION_ID"
# Clear THIS session's pending marker — we're about to log the real
# event, so the self-healing reaper must not later treat it as orphaned.
rm -f "$TEL_HOME/telemetry/pending/.pending-$TEL_SESSION_ID" 2>/dev/null
TEL_EFFECTIVE="${SKILL_TELEMETRY:-$(cat "$TEL_HOME/.telemetry-mode" 2>/dev/null || echo on)}"
If TEL_EFFECTIVE is off, stop here — do not call telemetry-log.
Otherwise pick the block matching the outcome.
For --step, pick the tdoc subcommand: new, edit, publish,
list, pull, unpublish, onboard, doctor, update. If the user
invoked multiple in one session, use the last one. If it was an ad-hoc
mention (not a /tdoc command), use chat or freeform.
On success:
"__TDOC_DIR__/telemetry/bin/telemetry-log" \
--skill tdoc \
--outcome success \
--duration "$DURATION" \
--step "<subcommand: new|edit|publish|list|pull|unpublish|onboard|doctor|update|chat>" \
--skill-version "$INSTALLED_VERSION" \
--session-id "$TEL_SESSION_ID"
On error:
"__TDOC_DIR__/telemetry/bin/telemetry-log" \
--skill tdoc \
--outcome error \
--duration "$DURATION" \
--error-class "<short tag, e.g. 'publish_timeout' / 'auth_failed' / 'malformed_input'>" \
--error-message "<full debug context, ≤400 chars>" \
--step "<which subcommand was running and what phase failed>" \
--skill-version "$INSTALLED_VERSION" \
--session-id "$TEL_SESSION_ID"
On abandoned (user asked to stop):
"__TDOC_DIR__/telemetry/bin/telemetry-log" \
--skill tdoc \
--outcome abandoned \
--duration "$DURATION" \
--step "<subcommand + phase you were on>" \
--skill-version "$INSTALLED_VERSION" \
--session-id "$TEL_SESSION_ID"
The script is fire-and-forget. It returns instantly and syncs to Supabase in the background.
echo off > ~/.tdoc/.telemetry-modeexport SKILL_TELEMETRY=offSee telemetry/PRIVACY.md for the full list of recorded fields.
npx claudepluginhub serenakeyitan/tdocDrafts READMEs, API docs, tutorials, release notes, and reviews technical docs for clarity and structure. Activates on docs/ .md files and READMEs.
Writes, rewrites, reviews, and organizes developer-facing documentation for web software projects. Use for READMEs, quickstarts, tutorials, how-to guides, API references, and more.