
Transcript Viewer
- 60 installs
- 70 repo stars
- Updated July 26, 2026
- rysweet/amplihack
Helps with ai & agent building tasks.
About
transcript-viewer is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- transcript-viewer
- AI & Agent Building
- AI-coding skill
Transcript Viewer by the numbers
- 60 all-time installs (skills.sh)
- +1 installs in the week ending Jul 26, 2026 (Skillselion tracking)
- Ranked #6,314 of 16,556 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/rysweet/amplihack --skill transcript-viewerAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 60 |
|---|---|
| repo stars | ★ 70 |
| Last updated | July 26, 2026 |
| Repository | rysweet/amplihack ↗ |
What it does
Helps with ai & agent building tasks.
Files
Transcript Viewer Skill
Purpose
This skill converts and browses session transcripts from two supported tools:
- Claude Code — JSONL logs auto-saved to
~/.claude/projects/ - GitHub Copilot CLI — JSONL logs auto-saved to
~/.copilot/session-state/*/events.jsonl
It provides four browsing modes:
1. Current session — View the active session's transcript 2. Specific session — View a session by its ID 3. Agent output — View background task output files produced by subagents 4. All sessions — Browse all project sessions, with optional date-range filtering
Tool Context Auto-Detection
Before browsing, detect which tool is active to set default log paths. Prefer directory-based detection (more reliable than env vars); fall back to env vars from src/amplihack/hooks/launcher_detector.py when directories don't exist:
# Primary: directory-based detection (most reliable)
if [[ -d "$HOME/.copilot/session-state" ]]; then
# Check if there are any sessions present
COPILOT_SESSIONS=$(ls -d "$HOME/.copilot/session-state/"*/ 2>/dev/null | wc -l)
CLAUDE_SESSIONS=$(ls "$HOME/.claude/projects/"*/*.jsonl 2>/dev/null | wc -l)
if [[ "$COPILOT_SESSIONS" -gt 0 && "$CLAUDE_SESSIONS" -eq 0 ]]; then
TOOL_CONTEXT="copilot"
DEFAULT_LOG_DIR="$HOME/.copilot/session-state"
elif [[ "$CLAUDE_SESSIONS" -gt 0 ]]; then
TOOL_CONTEXT="claude-code"
DEFAULT_LOG_DIR="$HOME/.claude/projects"
else
# Both dirs exist but empty — use env vars to decide
TOOL_CONTEXT="claude-code"
DEFAULT_LOG_DIR="$HOME/.claude/projects"
fi
elif [[ -d "$HOME/.claude/projects" ]]; then
TOOL_CONTEXT="claude-code"
DEFAULT_LOG_DIR="$HOME/.claude/projects"
# Fallback: env var detection (same vars as launcher_detector.py)
elif [[ -n "${CLAUDE_CODE_SESSION:-}${CLAUDE_SESSION_ID:-}${ANTHROPIC_API_KEY:-}" ]]; then
TOOL_CONTEXT="claude-code"
DEFAULT_LOG_DIR="$HOME/.claude/projects"
elif [[ -n "${GITHUB_COPILOT_TOKEN:-}${COPILOT_SESSION:-}" ]]; then
# Note: GITHUB_TOKEN is intentionally excluded — it's too generic and
# appears in non-Copilot CI contexts, causing false positives.
TOOL_CONTEXT="copilot"
DEFAULT_LOG_DIR="$HOME/.copilot/session-state"
else
# Default to claude-code — safe fallback (most users)
TOOL_CONTEXT="claude-code"
DEFAULT_LOG_DIR="$HOME/.claude/projects"
fiWhen both ~/.copilot/session-state/ and ~/.claude/projects/ exist with sessions, offer the user a choice: "Found sessions for both Claude Code and GitHub Copilot CLI. Which would you like to browse? [1] Claude Code [2] GitHub Copilot CLI"
The user can always override with an explicit path.
Log Format Auto-Detection
When given a file path, detect its format before processing:
detect_log_format() {
local file="$1"
if [[ "$file" == *.jsonl ]]; then
echo "jsonl"
elif [[ "$file" == *.md ]]; then
# Check for Copilot /share export signature — specific header only
# Note: do NOT match on "/share" alone (too generic — appears in docs, READMEs)
if grep -q "Copilot Session Export\|copilot-session" "$file" 2>/dev/null; then
echo "copilot-markdown"
else
echo "markdown"
fi
elif [[ "$file" == *.log ]]; then
# .log files may be plain text or agent JSONL; check content
local first_char
first_char=$(head -c 1 "$file" 2>/dev/null)
if [[ "$first_char" == "{" ]]; then
echo "jsonl"
else
echo "plain-log"
fi
else
# Inspect first byte for other extensions
local first_char
first_char=$(head -c 1 "$file" 2>/dev/null)
if [[ "$first_char" == "{" ]]; then
echo "jsonl"
else
echo "unknown"
fi
fi
}| Format | Handler |
|---|---|
jsonl | Pass to claude-code-log |
copilot-markdown | Display inline (already readable Markdown) |
markdown | Display inline |
unknown | Warn and display raw |
Tool Detection
Before running any command, check whether claude-code-log is available:
# Step 1: direct install check
which claude-code-log 2>/dev/null
# Step 2: npx fallback
npx --yes claude-code-log --version 2>/dev/nullSet CCL to the resolved command:
if which claude-code-log &>/dev/null; then
CCL="claude-code-log"
elif npx --yes claude-code-log --version &>/dev/null 2>&1; then
CCL="npx claude-code-log"
else
CCL=""
fiMissing Tool — Graceful Error
If CCL is empty, display this message and stop:
claude-code-log is not installed.
To install it globally:
npm install -g claude-code-log
Or run without installing (requires npx):
npx claude-code-log --help
After installing, retry your request.Do not attempt to install it automatically.
Modes
Mode 1: Current Session
Trigger phrases: "view current transcript", "show my current session", "current log"
What to do:
For Claude Code (TOOL_CONTEXT="claude-code"):
1. Find the most recently modified JSONL file under ~/.claude/projects/:
ls -t ~/.claude/projects/*/*.jsonl 2>/dev/null | head -12. Run:
$CCL <path-to-jsonl> --format markdown3. Display the Markdown output inline.
For GitHub Copilot CLI (TOOL_CONTEXT="copilot"):
1. Find the most recently modified session directory under ~/.copilot/session-state/:
ls -dt ~/.copilot/session-state/*/ 2>/dev/null | head -12. Read its events.jsonl:
LATEST_SESSION=$(ls -dt ~/.copilot/session-state/*/ 2>/dev/null | head -1)
$CCL "${LATEST_SESSION}events.jsonl" --format markdown3. Display the Markdown output inline.
Example output:
# Session: 2025-11-23 19:32
**Model**: claude-sonnet-4-6
**Messages**: 42
---
**User**: Fix the authentication bug in login.py
**Assistant**: I'll examine the file...
...Mode 2: Specific Session by ID
Trigger phrases: "view session <ID>", "show transcript <ID>", "open log <ID>"
What to do:
For Claude Code (TOOL_CONTEXT="claude-code"):
1. Search for the JSONL file matching the session ID:
find ~/.claude/projects -name "*.jsonl" | xargs grep -l "<SESSION_ID>" 2>/dev/null | head -1Or, if the ID looks like a filename fragment, use:
ls ~/.claude/projects/*/ | grep "<SESSION_ID>"2. Run:
$CCL <path-to-jsonl> --format markdown3. Display the output. If no file matches, report:
No session found with ID: <SESSION_ID>
Available sessions: run "view all sessions" to list them.For GitHub Copilot CLI (TOOL_CONTEXT="copilot"):
1. The session ID is the directory name under ~/.copilot/session-state/. Check directly:
SESSION_DIR="$HOME/.copilot/session-state/<SESSION_ID>"
if [[ -d "$SESSION_DIR" ]]; then
EVENTS_FILE="$SESSION_DIR/events.jsonl"
fiIf the full ID is not known, search for a partial match:
ls -d ~/.copilot/session-state/*/ 2>/dev/null | grep "<SESSION_ID>"2. Run:
$CCL "$EVENTS_FILE" --format markdown3. Display the output. If no directory matches, report:
No Copilot session found with ID: <SESSION_ID>
Available sessions: run "browse all sessions" to list them.Mode 3: Agent Background Task Output
Trigger phrases: "view agent output", "show background task output", "agent log"
What to do:
1. Find .log or .jsonl files created by background agent tasks. These are typically written to the current working directory or a temp path with a name matching .agent-step-*.log or similar:
ls -t .agent-step-*.log 2>/dev/null
ls -t /tmp/*.agent*.log 2>/dev/null2. For each file found, run detect_log_format <file> to classify it:
jsonl→ run$CCL <file> --format markdownplain-log→ display directly withcat
3. This ensures JSONL-formatted .log files (rare but possible) are rendered properly. 4. If no agent output files are found:
No agent background task output files found in the current directory.
Background agents write their output to files named .agent-step-<ID>.log.Mode 4: All Sessions (Browse)
Trigger phrases: "browse all sessions", "list transcripts", "view all sessions", "show session history"
What to do:
For Claude Code (TOOL_CONTEXT="claude-code"):
1. List all JSONL files under ~/.claude/projects/:
find ~/.claude/projects -name "*.jsonl" -printf "%T@ %p\n" 2>/dev/null \
| sort -rn | awk '{print $2}'2. For each file, extract the session date and first user message:
$CCL <file> --format markdown --summary
# If --summary flag is not supported, just show filename and date
head -1 <file> | python3 -c "import sys,json; d=json.loads(sys.stdin.read()); print(d.get('timestamp',''))"3. Print a summary table:
Available Sessions (Claude Code)
=================================
# Date Session ID / File
1 2025-11-23 19:32:36 ~/.claude/projects/foo/abc123.jsonl
2 2025-11-22 14:10:05 ~/.claude/projects/foo/def456.jsonl
...4. Offer to open a specific session: "Enter a number to view that session."
For GitHub Copilot CLI (TOOL_CONTEXT="copilot"):
1. List all session directories under ~/.copilot/session-state/ sorted by modification time:
ls -dt ~/.copilot/session-state/*/ 2>/dev/null2. For each session directory, read its events.jsonl to extract the timestamp:
for session_dir in $(ls -dt ~/.copilot/session-state/*/); do
session_id=$(basename "$session_dir")
events_file="$session_dir/events.jsonl"
if [[ -f "$events_file" ]]; then
timestamp=$(head -1 "$events_file" | python3 -c \
"import sys,json; d=json.loads(sys.stdin.read()); print(d.get('timestamp',''))" 2>/dev/null)
echo "$timestamp $session_id"
fi
done3. Print a summary table:
Available Sessions (GitHub Copilot CLI)
========================================
# Date Session ID
1 2025-11-23 19:32:36 abc1234567890abcdef1234567890abcd
2 2025-11-22 14:10:05 def4567890abcdef1234567890abcdef
...4. Offer to open a specific session: "Enter a number to view that session."
Date-Range Filtering
When the user specifies a date range (e.g., "last 7 days", "between 2025-11-01 and 2025-11-30"):
# Filter by modification time (last N days)
find ~/.claude/projects -name "*.jsonl" -mtime -7
# Filter by date range using find -newer
find ~/.claude/projects -name "*.jsonl" \
-newer /tmp/start_date_ref \
! -newer /tmp/end_date_refCreate the reference files with touch -d:
touch -d "2025-11-01" /tmp/start_date_ref
touch -d "2025-11-30" /tmp/end_date_refOutput Formats
Markdown (default)
Pass --format markdown to claude-code-log. The output is printed inline in the conversation. Best for quick reading in the terminal or Claude Code.
HTML
Pass --format html to claude-code-log. Write the output to a file and open it:
$CCL <file> --format html > /tmp/transcript-view.html
open /tmp/transcript-view.html 2>/dev/null \
|| xdg-open /tmp/transcript-view.html 2>/dev/null \
|| echo "HTML saved to /tmp/transcript-view.html — open it in your browser."The user can request HTML explicitly: "view transcript as HTML" or "export to HTML".
GitHub Copilot CLI Support
Automatic Log Persistence
GitHub Copilot CLI automatically saves session data to disk at:
~/.copilot/session-state/{session-id}/Each session directory contains:
events.jsonl— JSONL session history (similar format to Claude Code logs)workspace.yaml— session metadata (working directory, session ID, etc.)plan.md— implementation plan for the sessioncheckpoints/— compaction history (older event snapshots)
Legacy sessions may also exist at ~/.copilot/history-session-state/.
JSONL Format
Copilot events.jsonl contains one JSON object per line, similar to Claude Code format:
{"type":"user","message":{"role":"user","content":[{"type":"text","text":"Fix the bug"}]},"timestamp":"2025-11-23T19:32:36Z","sessionId":"abc1234567890"}
{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"I'll examine the file..."}]},"timestamp":"2025-11-23T19:32:40Z","sessionId":"abc1234567890"}claude-code-log handles parsing these files since the format is compatible.
Viewing a Copilot Session
When the user wants to view a Copilot session (by directory detection or explicit path):
1. Locate the events.jsonl for the session:
# Current session (most recent)
LATEST_SESSION=$(ls -dt ~/.copilot/session-state/*/ 2>/dev/null | head -1)
EVENTS_FILE="${LATEST_SESSION}events.jsonl"
# Specific session by ID
EVENTS_FILE="$HOME/.copilot/session-state/<SESSION_ID>/events.jsonl"2. Run:
$CCL "$EVENTS_FILE" --format markdown3. Display the output inline.
Detecting Available Sessions
To check if Copilot sessions exist:
if [[ -d "$HOME/.copilot/session-state" ]]; then
SESSION_COUNT=$(ls -d "$HOME/.copilot/session-state/"*/ 2>/dev/null | wc -l)
if [[ "$SESSION_COUNT" -gt 0 ]]; then
echo "Found $SESSION_COUNT Copilot session(s)"
fi
fiFull Workflow
When the user invokes this skill, follow this decision tree:
0. Detect tool context (directory-based first, then env var fallback):
- ~/.copilot/session-state/ exists with sessions → copilot
- ~/.claude/projects/ exists with sessions → claude-code
- Both exist with sessions → offer user a choice
- Directory detection fails, use env vars:
- CLAUDE_CODE_SESSION / CLAUDE_SESSION_ID / ANTHROPIC_API_KEY set → claude-code
- GITHUB_COPILOT_TOKEN / COPILOT_SESSION set → copilot
- Neither → default to claude-code (safe fallback)
1. Detect CCL (which claude-code-log / npx fallback)
→ If missing: show install instructions and STOP
→ Required for ALL modes (both Claude Code and Copilot use JSONL format)
2. If user provides an explicit file path:
a. Detect its format (detect_log_format function above)
b. jsonl or events.jsonl → pass to $CCL
c. unknown → warn and display raw
3. Determine mode from user message:
- mentions "current" or no session specified → Mode 1 (Current Session)
- mentions a session ID or hash → Mode 2 (Specific Session)
- mentions "agent" or "background" → Mode 3 (Agent Output)
- mentions "all", "browse", "list" → Mode 4 (All Sessions)
4. Determine output format:
- "as HTML" or "export HTML" → html
- default → markdown
5. Execute the appropriate mode (using TOOL_CONTEXT to choose correct paths) and display results.Error Handling
| Situation | Response |
|---|---|
claude-code-log not installed | Show install instructions, stop |
| JSONL file not found | "No session file found at <path>" |
| Session ID not found | "No session with ID <ID>. Run 'browse all sessions' to list available ones." |
| No agent output files | "No agent background task output found in current directory." |
| Empty JSONL file | "Session file is empty — no messages to display." |
| Date range produces no results | "No sessions found between <start> and <end>." |
claude-code-log returns non-zero exit | Display stderr and suggest --help |
~/.copilot/session-state/ exists but empty | "No Copilot sessions found. Start a session with GitHub Copilot CLI to create logs." |
| Copilot session dir exists but no events.jsonl | "Session directory found but events.jsonl is missing at <path>" |
| Unknown file format | Warn user and display raw content |
Implementation Notes
Detecting Session IDs
Claude Code session IDs are UUID-like strings. If the user writes something like "view session abc123" or "show log def456", treat the last word as the session ID and search for matching JSONL filenames.
JSONL Structure (Claude Code)
Claude Code session JSONL files contain one JSON object per line:
{"type":"user","message":{"role":"user","content":[{"type":"text","text":"..."}]},"timestamp":"...","sessionId":"..."}
{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"..."}]},"timestamp":"..."}claude-code-log handles parsing; this skill does not re-implement it.
Copilot JSONL Structure (GitHub Copilot CLI)
Copilot CLI events.jsonl files contain one JSON object per line, stored at ~/.copilot/session-state/{session-id}/events.jsonl:
{"type":"user","message":{"role":"user","content":[{"type":"text","text":"..."}]},"timestamp":"...","sessionId":"..."}
{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"..."}]},"timestamp":"...","sessionId":"..."}The format is compatible with claude-code-log. Additional per-session files:
workspace.yaml— session metadataplan.md— implementation plancheckpoints/— compaction history (older snapshots)
Legacy sessions may be stored at ~/.copilot/history-session-state/ with the same structure.
Philosophy Alignment
- Thin wrapper: Delegates to
claude-code-logfor all JSONL (both Claude Code and Copilot) - Graceful degradation: Clear error messages when tool is missing or no sessions found
- Single responsibility: Only views/converts transcripts, never modifies them
- No hidden state: All file paths are shown to the user
Limitations
- Requires
claude-code-log(npm) ornpxto convert JSONL (both Claude Code and Copilot) to HTML/Markdown - Cannot view transcripts from remote machines
- Date filtering relies on filesystem modification times, not session timestamps
--summaryflag availability depends onclaude-code-logversion- Legacy Copilot sessions in
~/.copilot/history-session-state/are not auto-discovered (must use explicit path) - When both Claude Code and Copilot sessions exist, the skill prompts the user to choose rather than merging them
Quick Reference
| User says | Tool | Mode | Command |
|---|---|---|---|
| "view current transcript" | Claude Code | Current session | $CCL <latest.jsonl> --format markdown |
| "show session abc123" | Claude Code | Specific session | $CCL ~/.claude/projects/**/*abc123*.jsonl --format markdown |
| "view agent output" | Claude Code | Agent output | cat .agent-step-*.log or $CCL *.jsonl |
| "browse all sessions" | Claude Code | All sessions | list + summarize all ~/.claude/projects/**/*.jsonl |
| "view transcript as HTML" | Claude Code | Any + HTML | $CCL <file> --format html > /tmp/view.html |
| "last 7 days" (with browse) | Claude Code | Date filter | find ... -mtime -7 |
| "view current copilot session" | Copilot | Current session | $CCL ~/.copilot/session-state/<latest>/events.jsonl --format markdown |
| "show copilot session abc123" | Copilot | Specific session | $CCL ~/.copilot/session-state/abc123/events.jsonl --format markdown |
| "browse copilot sessions" | Copilot | All sessions | list dirs in ~/.copilot/session-state/, show session IDs |
| "view copilot session as HTML" | Copilot | Any + HTML | $CCL <events.jsonl> --format html > /tmp/view.html |
#!/usr/bin/env bash
# Tests for transcript-viewer skill behaviors
# Run with: bash tests/test_transcript_viewer.sh
# All tests are self-contained and use temporary directories.
set -euo pipefail
PASS=0
FAIL=0
TMPDIR_TEST="$(mktemp -d)"
trap 'rm -rf "$TMPDIR_TEST"' EXIT
pass() { echo " PASS: $1"; PASS=$((PASS+1)); }
fail() { echo " FAIL: $1"; FAIL=$((FAIL+1)); }
# ─── Helpers ────────────────────────────────────────────────────────────────
make_jsonl() {
local path="$1"
mkdir -p "$(dirname "$path")"
cat >"$path" <<'JSONL'
{"type":"user","message":{"role":"user","content":[{"type":"text","text":"Hello"}]},"timestamp":"2025-11-23T19:32:36Z","sessionId":"abc123"}
{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"Hi there"}]},"timestamp":"2025-11-23T19:32:40Z","sessionId":"abc123"}
JSONL
}
make_copilot_events_jsonl() {
local path="$1"
mkdir -p "$(dirname "$path")"
cat >"$path" <<'JSONL'
{"type":"user","message":{"role":"user","content":[{"type":"text","text":"Fix the authentication bug"}]},"timestamp":"2025-11-23T19:32:36Z","sessionId":"copilot-session-xyz789"}
{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"I'll examine the file and fix the authentication bug..."}]},"timestamp":"2025-11-23T19:32:40Z","sessionId":"copilot-session-xyz789"}
JSONL
}
# ─── Test 1: SKILL.md exists and has valid YAML frontmatter fields ──────────
echo "Test 1: SKILL.md exists with required YAML frontmatter"
SKILL_FILE="$(dirname "$(dirname "$0")")/SKILL.md"
if [[ ! -f "$SKILL_FILE" ]]; then
fail "SKILL.md not found at $SKILL_FILE"
else
pass "SKILL.md exists"
if grep -q "^name: transcript-viewer" "$SKILL_FILE"; then
pass "frontmatter: name field present"
else
fail "frontmatter: name field missing"
fi
if grep -q "^description:" "$SKILL_FILE"; then
pass "frontmatter: description field present"
else
fail "frontmatter: description field missing"
fi
if grep -q "auto_activate_keywords:" "$SKILL_FILE"; then
pass "frontmatter: auto_activate_keywords field present"
else
fail "frontmatter: auto_activate_keywords field missing"
fi
fi
# ─── Test 2: SKILL.md includes Copilot-related keywords ──────────────────────
echo ""
echo "Test 2: SKILL.md includes Copilot-related auto_activate_keywords"
if grep -q "copilot" "$SKILL_FILE"; then
pass "SKILL.md mentions copilot (keyword support)"
else
fail "SKILL.md missing copilot keyword support"
fi
# ─── Test 3: Tool detection logic ────────────────────────────────────────────
echo ""
echo "Test 3: Tool detection — which / npx fallback"
# Simulate missing claude-code-log by using a PATH that won't find it
CCL=""
PATH_BACKUP="$PATH"
export PATH="/usr/bin:/bin" # minimal PATH, no npm binaries
if which claude-code-log &>/dev/null; then
CCL="claude-code-log"
elif npx --yes claude-code-log --version &>/dev/null 2>&1; then
CCL="npx claude-code-log"
else
CCL=""
fi
export PATH="$PATH_BACKUP"
if [[ -z "$CCL" ]]; then
pass "tool detection: correctly returns empty when not installed"
else
# Tool was found — that's also valid, but skip the "missing" path
pass "tool detection: claude-code-log found on this system (CCL=$CCL)"
fi
# ─── Test 4: Missing tool message contains install instructions ───────────────
echo ""
echo "Test 4: Missing tool error message"
if grep -q "npm install -g claude-code-log" "$SKILL_FILE"; then
pass "SKILL.md contains npm install-g install instruction"
else
fail "SKILL.md missing npm install-g install instruction"
fi
if grep -q "npx claude-code-log" "$SKILL_FILE"; then
pass "SKILL.md contains npx fallback install instruction"
else
fail "SKILL.md missing npx fallback install instruction"
fi
# ─── Test 5: Current session — find latest JSONL (Claude Code path) ──────────
echo ""
echo "Test 5: Mode 1 — Claude Code current session file detection"
FAKE_HOME="$TMPDIR_TEST/home"
mkdir -p "$FAKE_HOME/.claude/projects/myproject"
JSONL1="$FAKE_HOME/.claude/projects/myproject/session1.jsonl"
JSONL2="$FAKE_HOME/.claude/projects/myproject/session2.jsonl"
make_jsonl "$JSONL1"
sleep 0.1
make_jsonl "$JSONL2"
LATEST=$(ls -t "$FAKE_HOME/.claude/projects"/*/*.jsonl 2>/dev/null | head -1)
if [[ "$LATEST" == "$JSONL2" ]]; then
pass "mode 1 (claude-code): correctly identifies latest JSONL as session2.jsonl"
else
fail "mode 1 (claude-code): expected $JSONL2, got $LATEST"
fi
# ─── Test 6: Current session — find latest events.jsonl (Copilot path) ───────
echo ""
echo "Test 6: Mode 1 — Copilot current session detection via ~/.copilot/session-state/"
COPILOT_STATE_DIR="$FAKE_HOME/.copilot/session-state"
SESSION_ID_1="abc1234567890abcdef1234567890abc"
SESSION_ID_2="def4567890abcdef1234567890abcdef"
mkdir -p "$COPILOT_STATE_DIR/$SESSION_ID_1"
mkdir -p "$COPILOT_STATE_DIR/$SESSION_ID_2"
make_copilot_events_jsonl "$COPILOT_STATE_DIR/$SESSION_ID_1/events.jsonl"
sleep 0.1
make_copilot_events_jsonl "$COPILOT_STATE_DIR/$SESSION_ID_2/events.jsonl"
LATEST_COPILOT_SESSION=$(ls -dt "$COPILOT_STATE_DIR"/*/ 2>/dev/null | head -1)
LATEST_EVENTS="${LATEST_COPILOT_SESSION}events.jsonl"
if [[ -f "$LATEST_EVENTS" ]]; then
pass "mode 1 (copilot): latest session dir found with events.jsonl"
else
fail "mode 1 (copilot): events.jsonl not found in latest session dir ($LATEST_EVENTS)"
fi
LATEST_SESSION_ID=$(basename "$LATEST_COPILOT_SESSION")
if [[ "$LATEST_SESSION_ID" == "$SESSION_ID_2" ]]; then
pass "mode 1 (copilot): correctly identifies latest session as $SESSION_ID_2"
else
fail "mode 1 (copilot): expected $SESSION_ID_2, got $LATEST_SESSION_ID"
fi
# ─── Test 7: Specific session ID search (Claude Code) ─────────────────────────
echo ""
echo "Test 7: Mode 2 — Claude Code find session by ID"
TARGET_ID="abc123"
FOUND=$(grep -rl "\"sessionId\":\"$TARGET_ID\"" "$FAKE_HOME/.claude/projects" 2>/dev/null | head -1)
if [[ -n "$FOUND" ]]; then
pass "mode 2 (claude-code): found session file containing ID $TARGET_ID"
else
fail "mode 2 (claude-code): could not find session file for ID $TARGET_ID"
fi
MISSING_ID="zzznope"
NOT_FOUND=$(grep -rl "\"sessionId\":\"$MISSING_ID\"" "$FAKE_HOME/.claude/projects" 2>/dev/null | head -1 || true)
if [[ -z "$NOT_FOUND" ]]; then
pass "mode 2 (claude-code): correctly returns nothing for unknown ID $MISSING_ID"
else
fail "mode 2 (claude-code): unexpected match for $MISSING_ID"
fi
# ─── Test 8: Specific session ID search (Copilot path) ────────────────────────
echo ""
echo "Test 8: Mode 2 — Copilot find session by directory ID"
# Copilot sessions are found by directory name under ~/.copilot/session-state/
COPILOT_SESSION_DIR="$COPILOT_STATE_DIR/$SESSION_ID_1"
if [[ -d "$COPILOT_SESSION_DIR" ]]; then
COPILOT_EVENTS="$COPILOT_SESSION_DIR/events.jsonl"
if [[ -f "$COPILOT_EVENTS" ]]; then
pass "mode 2 (copilot): found events.jsonl for session $SESSION_ID_1"
else
fail "mode 2 (copilot): events.jsonl missing for session $SESSION_ID_1"
fi
else
fail "mode 2 (copilot): session directory $SESSION_ID_1 not found"
fi
# Test partial ID match
PARTIAL_ID="abc12345"
PARTIAL_MATCH=$(ls -d "$COPILOT_STATE_DIR"/*/ 2>/dev/null | grep "$PARTIAL_ID" | head -1 || true)
if [[ -n "$PARTIAL_MATCH" ]]; then
pass "mode 2 (copilot): partial ID '$PARTIAL_ID' matches session directory"
else
fail "mode 2 (copilot): could not find session with partial ID '$PARTIAL_ID'"
fi
MISSING_COPILOT_ID="zzznope-session-xxx"
NOT_FOUND_COPILOT=$(ls -d "$COPILOT_STATE_DIR/$MISSING_COPILOT_ID" 2>/dev/null || true)
if [[ -z "$NOT_FOUND_COPILOT" ]]; then
pass "mode 2 (copilot): correctly returns nothing for unknown session ID"
else
fail "mode 2 (copilot): unexpected match for $MISSING_COPILOT_ID"
fi
# ─── Test 9: Agent output detection ──────────────────────────────────────────
echo ""
echo "Test 9: Mode 3 — agent background task output"
AGENT_DIR="$TMPDIR_TEST/workdir"
mkdir -p "$AGENT_DIR"
echo "agent log line 1" > "$AGENT_DIR/.agent-step-1234567890.log"
AGENT_FILES=$(ls -t "$AGENT_DIR"/.agent-step-*.log 2>/dev/null)
if [[ -n "$AGENT_FILES" ]]; then
pass "mode 3: correctly detects .agent-step-*.log files"
else
fail "mode 3: no .agent-step-*.log files found"
fi
EMPTY_DIR="$TMPDIR_TEST/emptydir"
mkdir -p "$EMPTY_DIR"
NO_AGENT=$(ls -t "$EMPTY_DIR"/.agent-step-*.log 2>/dev/null || true)
if [[ -z "$NO_AGENT" ]]; then
pass "mode 3: correctly returns empty when no agent files present"
else
fail "mode 3: unexpected agent files found in empty dir"
fi
# ─── Test 10: All sessions listing (Claude Code) ──────────────────────────────
echo ""
echo "Test 10: Mode 4 — list all Claude Code sessions"
ALL_FILES=$(find "$FAKE_HOME/.claude/projects" -name "*.jsonl" 2>/dev/null | wc -l)
if [[ "$ALL_FILES" -eq 2 ]]; then
pass "mode 4 (claude-code): found correct number of sessions (2)"
else
fail "mode 4 (claude-code): expected 2 sessions, found $ALL_FILES"
fi
# ─── Test 11: All sessions listing (Copilot path) ─────────────────────────────
echo ""
echo "Test 11: Mode 4 — list all Copilot sessions from ~/.copilot/session-state/"
COPILOT_SESSION_COUNT=$(ls -d "$COPILOT_STATE_DIR"/*/ 2>/dev/null | wc -l)
if [[ "$COPILOT_SESSION_COUNT" -eq 2 ]]; then
pass "mode 4 (copilot): found correct number of Copilot sessions (2)"
else
fail "mode 4 (copilot): expected 2 sessions, found $COPILOT_SESSION_COUNT"
fi
# Each session directory should have an events.jsonl
COPILOT_EVENTS_COUNT=0
for session_dir in "$COPILOT_STATE_DIR"/*/; do
if [[ -f "${session_dir}events.jsonl" ]]; then
COPILOT_EVENTS_COUNT=$((COPILOT_EVENTS_COUNT+1))
fi
done
if [[ "$COPILOT_EVENTS_COUNT" -eq 2 ]]; then
pass "mode 4 (copilot): all Copilot sessions have events.jsonl"
else
fail "mode 4 (copilot): expected 2 events.jsonl files, found $COPILOT_EVENTS_COUNT"
fi
# ─── Test 12: Date-range filtering ────────────────────────────────────────────
echo ""
echo "Test 12: Date-range filtering"
# Create files with different timestamps
OLD="$FAKE_HOME/.claude/projects/myproject/old.jsonl"
make_jsonl "$OLD"
touch -d "2025-01-01" "$OLD"
RECENT_FILES=$(find "$FAKE_HOME/.claude/projects" -name "*.jsonl" -mtime -1 2>/dev/null | wc -l)
if [[ "$RECENT_FILES" -eq 2 ]]; then
pass "date filter: -mtime -1 correctly excludes old session"
else
fail "date filter: expected 2 recent files, found $RECENT_FILES"
fi
ALL_FILES_NOW=$(find "$FAKE_HOME/.claude/projects" -name "*.jsonl" 2>/dev/null | wc -l)
if [[ "$ALL_FILES_NOW" -eq 3 ]]; then
pass "date filter: all 3 files exist including old one"
else
fail "date filter: expected 3 total files, found $ALL_FILES_NOW"
fi
# ─── Test 13: HTML output path construction ───────────────────────────────────
echo ""
echo "Test 13: HTML output path"
HTML_OUTPUT="/tmp/transcript-view.html"
# Verify the path is mentioned in SKILL.md
if grep -q "/tmp/transcript-view.html" "$SKILL_FILE"; then
pass "SKILL.md mentions HTML output path /tmp/transcript-view.html"
else
fail "SKILL.md does not mention HTML output path"
fi
# ─── Test 14: JSONL sample parse (basic structure, Claude Code) ───────────────
echo ""
echo "Test 14: JSONL basic parse (Claude Code format)"
SAMPLE="$TMPDIR_TEST/sample.jsonl"
make_jsonl "$SAMPLE"
LINE1=$(head -1 "$SAMPLE")
TYPE=$(echo "$LINE1" | python3 -c "import sys,json; d=json.loads(sys.stdin.read()); print(d.get('type',''))" 2>/dev/null)
if [[ "$TYPE" == "user" ]]; then
pass "JSONL (claude-code): first line has type=user"
else
fail "JSONL (claude-code): expected type=user, got '$TYPE'"
fi
SESSION_ID=$(echo "$LINE1" | python3 -c "import sys,json; d=json.loads(sys.stdin.read()); print(d.get('sessionId',''))" 2>/dev/null)
if [[ "$SESSION_ID" == "abc123" ]]; then
pass "JSONL (claude-code): sessionId correctly parsed as abc123"
else
fail "JSONL (claude-code): expected sessionId=abc123, got '$SESSION_ID'"
fi
# ─── Test 15: JSONL sample parse (Copilot events.jsonl format) ────────────────
echo ""
echo "Test 15: JSONL basic parse (Copilot events.jsonl format)"
COPILOT_SAMPLE="$COPILOT_STATE_DIR/$SESSION_ID_1/events.jsonl"
COPILOT_LINE1=$(head -1 "$COPILOT_SAMPLE")
COPILOT_TYPE=$(echo "$COPILOT_LINE1" | python3 -c "import sys,json; d=json.loads(sys.stdin.read()); print(d.get('type',''))" 2>/dev/null)
if [[ "$COPILOT_TYPE" == "user" ]]; then
pass "JSONL (copilot): first line has type=user"
else
fail "JSONL (copilot): expected type=user, got '$COPILOT_TYPE'"
fi
COPILOT_SESSION_ID_VAL=$(echo "$COPILOT_LINE1" | python3 -c "import sys,json; d=json.loads(sys.stdin.read()); print(d.get('sessionId',''))" 2>/dev/null)
if [[ "$COPILOT_SESSION_ID_VAL" == "copilot-session-xyz789" ]]; then
pass "JSONL (copilot): sessionId correctly parsed as copilot-session-xyz789"
else
fail "JSONL (copilot): expected sessionId=copilot-session-xyz789, got '$COPILOT_SESSION_ID_VAL'"
fi
# ─── Test 16: Auto-detection — directory-based (Copilot vs Claude Code) ───────
echo ""
echo "Test 16: Auto-detection of tool context (directory-based)"
# Detection logic mirrors what the skill documents
detect_tool_context_dir() {
local home="$1"
local copilot_sessions=0
local claude_sessions=0
if [[ -d "$home/.copilot/session-state" ]]; then
copilot_sessions=$(ls -d "$home/.copilot/session-state/"*/ 2>/dev/null | wc -l)
fi
if [[ -d "$home/.claude/projects" ]]; then
claude_sessions=$(ls "$home/.claude/projects/"*/*.jsonl 2>/dev/null | wc -l || true)
fi
if [[ "$copilot_sessions" -gt 0 && "$claude_sessions" -eq 0 ]]; then
echo "copilot"
elif [[ "$claude_sessions" -gt 0 && "$copilot_sessions" -eq 0 ]]; then
echo "claude-code"
elif [[ "$claude_sessions" -gt 0 && "$copilot_sessions" -gt 0 ]]; then
echo "both"
else
echo "none"
fi
}
# Scenario 1: Only Copilot sessions exist
ONLY_COPILOT_HOME="$TMPDIR_TEST/only-copilot-home"
mkdir -p "$ONLY_COPILOT_HOME/.copilot/session-state/session-aaa"
make_copilot_events_jsonl "$ONLY_COPILOT_HOME/.copilot/session-state/session-aaa/events.jsonl"
CONTEXT_ONLY_COPILOT=$(detect_tool_context_dir "$ONLY_COPILOT_HOME")
if [[ "$CONTEXT_ONLY_COPILOT" == "copilot" ]]; then
pass "auto-detect (dir): only ~/.copilot/session-state/ → copilot"
else
fail "auto-detect (dir): expected copilot, got '$CONTEXT_ONLY_COPILOT'"
fi
# Scenario 2: Only Claude Code sessions exist
ONLY_CLAUDE_HOME="$TMPDIR_TEST/only-claude-home"
mkdir -p "$ONLY_CLAUDE_HOME/.claude/projects/myproject"
make_jsonl "$ONLY_CLAUDE_HOME/.claude/projects/myproject/session.jsonl"
CONTEXT_ONLY_CLAUDE=$(detect_tool_context_dir "$ONLY_CLAUDE_HOME")
if [[ "$CONTEXT_ONLY_CLAUDE" == "claude-code" ]]; then
pass "auto-detect (dir): only ~/.claude/projects/ → claude-code"
else
fail "auto-detect (dir): expected claude-code, got '$CONTEXT_ONLY_CLAUDE'"
fi
# Scenario 3: Both directories exist with sessions → offer choice
BOTH_HOME="$TMPDIR_TEST/both-home"
mkdir -p "$BOTH_HOME/.copilot/session-state/session-bbb"
make_copilot_events_jsonl "$BOTH_HOME/.copilot/session-state/session-bbb/events.jsonl"
mkdir -p "$BOTH_HOME/.claude/projects/myproject"
make_jsonl "$BOTH_HOME/.claude/projects/myproject/session.jsonl"
CONTEXT_BOTH=$(detect_tool_context_dir "$BOTH_HOME")
if [[ "$CONTEXT_BOTH" == "both" ]]; then
pass "auto-detect (dir): both directories exist → 'both' (user choice needed)"
else
fail "auto-detect (dir): expected 'both', got '$CONTEXT_BOTH'"
fi
# Scenario 4: Neither directory → return none (fallback to env vars)
EMPTY_HOME="$TMPDIR_TEST/empty-home"
mkdir -p "$EMPTY_HOME"
CONTEXT_NONE=$(detect_tool_context_dir "$EMPTY_HOME")
if [[ "$CONTEXT_NONE" == "none" ]]; then
pass "auto-detect (dir): no directories → 'none' (env var fallback applies)"
else
fail "auto-detect (dir): expected 'none', got '$CONTEXT_NONE'"
fi
# ─── Test 17: Auto-detection — env var fallback ────────────────────────────────
echo ""
echo "Test 17: Auto-detection — env var fallback (when directories not present)"
detect_tool_context_env() {
if [[ -n "${CLAUDE_CODE_SESSION:-}${CLAUDE_SESSION_ID:-}${ANTHROPIC_API_KEY:-}" ]]; then
echo "claude-code"
elif [[ -n "${GITHUB_COPILOT_TOKEN:-}${COPILOT_SESSION:-}" ]]; then
echo "copilot"
else
echo "claude-code" # safe fallback
fi
}
CONTEXT_DEFAULT=$(detect_tool_context_env)
if [[ "$CONTEXT_DEFAULT" == "claude-code" ]]; then
pass "env-var fallback: no vars set → claude-code (safe fallback)"
else
fail "env-var fallback: expected claude-code, got '$CONTEXT_DEFAULT'"
fi
CONTEXT_CC=$(CLAUDE_CODE_SESSION=test-session-id detect_tool_context_env)
if [[ "$CONTEXT_CC" == "claude-code" ]]; then
pass "env-var fallback: CLAUDE_CODE_SESSION set → claude-code"
else
fail "env-var fallback: expected claude-code, got '$CONTEXT_CC'"
fi
CONTEXT_CP=$(GITHUB_COPILOT_TOKEN=ghu_test123 detect_tool_context_env)
if [[ "$CONTEXT_CP" == "copilot" ]]; then
pass "env-var fallback: GITHUB_COPILOT_TOKEN set → copilot"
else
fail "env-var fallback: expected copilot, got '$CONTEXT_CP'"
fi
CONTEXT_CS=$(COPILOT_SESSION=copilot-session-abc detect_tool_context_env)
if [[ "$CONTEXT_CS" == "copilot" ]]; then
pass "env-var fallback: COPILOT_SESSION set → copilot"
else
fail "env-var fallback: expected copilot, got '$CONTEXT_CS'"
fi
# ─── Test 18: SKILL.md documents Copilot log location ─────────────────────────
echo ""
echo "Test 18: SKILL.md documents Copilot JSONL log location"
if grep -q "session-state" "$SKILL_FILE"; then
pass "SKILL.md documents ~/.copilot/session-state/ path"
else
fail "SKILL.md missing ~/.copilot/session-state/ documentation"
fi
if grep -q "events.jsonl" "$SKILL_FILE"; then
pass "SKILL.md documents events.jsonl file name"
else
fail "SKILL.md missing events.jsonl documentation"
fi
# ─── Test 19: Unknown format detection ────────────────────────────────────────
echo ""
echo "Test 19: Unknown format detection"
detect_format() {
local file="$1"
if [[ "$file" == *.jsonl ]]; then
echo "jsonl"
elif [[ "$file" == *.log ]]; then
local first_char
first_char=$(head -c 1 "$file" 2>/dev/null)
if [[ "$first_char" == "{" ]]; then
echo "jsonl"
else
echo "plain-log"
fi
else
local first_char
first_char=$(head -c 1 "$file" 2>/dev/null)
if [[ "$first_char" == "{" ]]; then
echo "jsonl"
else
echo "unknown"
fi
fi
}
# A file with no recognized extension and plain text content
PLAIN_FILE="$TMPDIR_TEST/somefile.txt"
echo "just some plain text" > "$PLAIN_FILE"
FORMAT_PLAIN=$(detect_format "$PLAIN_FILE")
if [[ "$FORMAT_PLAIN" == "unknown" ]]; then
pass "unknown format: plain text file returns 'unknown'"
else
fail "unknown format: expected 'unknown', got '$FORMAT_PLAIN'"
fi
# A .log file with plain text (not JSONL)
PLAIN_LOG="$TMPDIR_TEST/agent.log"
echo "2025-01-01 INFO: starting agent" > "$PLAIN_LOG"
FORMAT_PLAIN_LOG=$(detect_format "$PLAIN_LOG")
if [[ "$FORMAT_PLAIN_LOG" == "plain-log" ]]; then
pass "plain-log: non-JSONL .log file returns 'plain-log'"
else
fail "plain-log: expected 'plain-log', got '$FORMAT_PLAIN_LOG'"
fi
# A .log file that IS JSONL
JSONL_LOG="$TMPDIR_TEST/agent-jsonl.log"
make_jsonl "$JSONL_LOG"
FORMAT_JSONL_LOG=$(detect_format "$JSONL_LOG")
if [[ "$FORMAT_JSONL_LOG" == "jsonl" ]]; then
pass "jsonl .log: JSONL-format .log file returns 'jsonl'"
else
fail "jsonl .log: expected 'jsonl', got '$FORMAT_JSONL_LOG'"
fi
# events.jsonl (Copilot) detected as jsonl format
COPILOT_EVENTS_SAMPLE="$TMPDIR_TEST/events.jsonl"
make_copilot_events_jsonl "$COPILOT_EVENTS_SAMPLE"
FORMAT_COPILOT_EVENTS=$(detect_format "$COPILOT_EVENTS_SAMPLE")
if [[ "$FORMAT_COPILOT_EVENTS" == "jsonl" ]]; then
pass "copilot events.jsonl: correctly identified as jsonl format"
else
fail "copilot events.jsonl: expected 'jsonl', got '$FORMAT_COPILOT_EVENTS'"
fi
# ─── Test 20: SKILL.md documents Copilot workspace.yaml and plan.md ──────────
echo ""
echo "Test 20: SKILL.md documents Copilot session structure"
if grep -q "workspace.yaml" "$SKILL_FILE"; then
pass "SKILL.md documents workspace.yaml in Copilot session"
else
fail "SKILL.md missing workspace.yaml documentation"
fi
if grep -q "plan.md" "$SKILL_FILE"; then
pass "SKILL.md documents plan.md in Copilot session"
else
fail "SKILL.md missing plan.md documentation"
fi
# ─── Summary ──────────────────────────────────────────────────────────────────
echo ""
echo "═══════════════════════════════"
echo "Results: $PASS passed, $FAIL failed"
echo "═══════════════════════════════"
if [[ "$FAIL" -gt 0 ]]; then
exit 1
fi