Initial commit

This commit is contained in:
Sean Betts committed 2026-04-26 10:26:58 +01:00
commit fbb8c3da72
24 files changed
+2476

No files matched your search

+38
View File
@@ -0,0 +1,38 @@
#!/usr/bin/env python3
import argparse
import json
from pathlib import Path
def main() -> None:
parser = argparse.ArgumentParser(description="Append one evidence record to the skill JSONL ledger.")
parser.add_argument("--file", required=True, help="Path to evidence.jsonl")
parser.add_argument("--date-checked", required=True)
parser.add_argument("--source", required=True)
parser.add_argument("--entity", required=True)
parser.add_argument("--claim", required=True)
parser.add_argument("--evidence-type", required=True)
parser.add_argument("--confidence", required=True, choices=["high", "medium", "low"])
parser.add_argument("--ref", required=True, help="URL or local path")
parser.add_argument("--notes", default="")
args = parser.parse_args()
record = {
"date_checked": args.date_checked,
"source": args.source,
"entity": args.entity,
"claim": args.claim,
"evidence_type": args.evidence_type,
"confidence": args.confidence,
"ref": args.ref,
"notes": args.notes,
}
target = Path(args.file)
target.parent.mkdir(parents=True, exist_ok=True)
with target.open("a", encoding="utf-8") as fh:
fh.write(json.dumps(record, ensure_ascii=True) + "\n")
if __name__ == "__main__":
main()
+62
View File
@@ -0,0 +1,62 @@
#!/bin/sh
set -eu
SCRIPT_DIR="$(CDPATH= cd -- "$(dirname "$0")" && pwd)"
REPO_DIR="$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd)"
LOCAL_DIR="$REPO_DIR/.local"
PROFILE_DIR="${1:-${KOMODO_PROFILE_DIR:-$LOCAL_DIR/playwright-komodo-profile}}"
PORT="${2:-${KOMODO_CDP_PORT:-55684}}"
URL="${KOMODO_BOOTSTRAP_URL:-https://komodostation.com/product/steam-controller_jpy/}"
ENV_FILE="${KOMODO_ENV_FILE:-$LOCAL_DIR/komodo-env.sh}"
mkdir -p "$PROFILE_DIR" "$LOCAL_DIR"
existing_pid="$(ps aux | awk -v profile="$PROFILE_DIR" -v port="$PORT" '
index($0, "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome") &&
index($0, profile) &&
index($0, "--remote-debugging-port=" port) {
print $2
exit
}
')"
if [ -z "$existing_pid" ]; then
open -g -na "Google Chrome" --args \
"--user-data-dir=$PROFILE_DIR" \
"--remote-debugging-port=$PORT" \
--no-first-run \
--disable-sync \
--new-window \
"$URL"
attempts=0
until curl -fsS "http://127.0.0.1:$PORT/json/version" >/dev/null 2>&1; do
attempts=$((attempts + 1))
if [ "$attempts" -ge 30 ]; then
echo "failed to observe a Chrome CDP endpoint on port $PORT" >&2
exit 1
fi
sleep 1
done
else
echo "Komodo bootstrap browser already running on port $PORT (pid $existing_pid)."
fi
cat > "$ENV_FILE" <<EOF
export KOMODO_PLAYWRIGHT_FALLBACK=1
export PLAYWRIGHT_PROFILE_DIR="$PROFILE_DIR"
export PLAYWRIGHT_CDP_ENDPOINT="http://127.0.0.1:$PORT"
export PLAYWRIGHT_BROWSER_CHANNEL="chrome"
export PLAYWRIGHT_REFERER="https://komodostation.com/product/steam-controller_jpy/"
EOF
printf '%s\n' "Komodo browser bootstrap ready."
printf '%s\n' "Profile: $PROFILE_DIR"
printf '%s\n' "CDP endpoint: http://127.0.0.1:$PORT"
printf '%s\n' "Env file: $ENV_FILE"
printf '%s\n' ""
printf '%s\n' "Next steps:"
printf '%s\n' "1. If Komodo needs any manual interaction, switch to the dedicated Chrome window when convenient."
printf '%s\n' "2. Leave that dedicated browser running in the background."
printf '%s\n' "3. Source the env file before a run, or let check_komodo.sh pick it up automatically."
+152
View File
@@ -0,0 +1,152 @@
#!/bin/sh
set -eu
if [ "$#" -lt 1 ]; then
echo "usage: $0 RUN_DIR" >&2
exit 1
fi
RUN_DIR="$1"
OUT_DIR="$RUN_DIR/api/komodo"
REPORT_DIR="$RUN_DIR/reports"
SCRIPT_DIR="$(CDPATH= cd -- "$(dirname "$0")" && pwd)"
REPO_DIR="$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd)"
mkdir -p "$OUT_DIR" "$REPORT_DIR"
ERROR_FILE="$REPORT_DIR/komodo-errors.txt"
: > "$REPORT_DIR/komodo-product-modified.tsv"
: > "$REPORT_DIR/komodo-controller-sections.tsv"
: > "$REPORT_DIR/komodo-machine-sections.tsv"
: > "$REPORT_DIR/komodo-frame-sections.tsv"
: > "$REPORT_DIR/komodo-controller-media-interesting.tsv"
: > "$REPORT_DIR/komodo-machine-media.tsv"
: > "$REPORT_DIR/komodo-frame-media.tsv"
: > "$REPORT_DIR/komodo-visual-assets.tsv"
: > "$ERROR_FILE"
load_local_env() {
env_file="${KOMODO_ENV_FILE:-$REPO_DIR/.local/komodo-env.sh}"
if [ -f "$env_file" ]; then
# shellcheck disable=SC1090
. "$env_file"
fi
}
maybe_set_cdp_endpoint() {
if [ -n "${PLAYWRIGHT_CDP_ENDPOINT:-}" ] || [ -z "${PLAYWRIGHT_PROFILE_DIR:-}" ]; then
return 0
fi
port="$(ps aux | awk -v profile="$PLAYWRIGHT_PROFILE_DIR" '
index($0, profile) && match($0, /--remote-debugging-port=([0-9]+)/) {
value = substr($0, RSTART, RLENGTH)
sub(/^--remote-debugging-port=/, "", value)
print value
exit
}
' 2>/dev/null || true)"
if [ -n "$port" ]; then
PLAYWRIGHT_CDP_ENDPOINT="http://127.0.0.1:$port"
export PLAYWRIGHT_CDP_ENDPOINT
fi
}
load_local_env
fetch_json() {
name="$1"
url="$2"
allow_fallback="${3:-1}"
tmp="$OUT_DIR/$name.json.tmp"
if curl -A 'Mozilla/5.0' --retry 1 --retry-delay 1 --max-time 12 -fsSL "$url" > "$tmp" 2>/dev/null \
&& jq empty "$tmp" >/dev/null 2>&1; then
mv "$tmp" "$OUT_DIR/$name.json"
return 0
fi
rm -f "$tmp"
if [ "$allow_fallback" = "1" ] && [ "${KOMODO_PLAYWRIGHT_FALLBACK:-0}" = "1" ] && command -v playwright-cli >/dev/null 2>&1; then
maybe_set_cdp_endpoint
PLAYWRIGHT_CLI_BIN="$(command -v playwright-cli)"
PLAYWRIGHT_CORE_PATH="$(CDPATH= cd -- "$(dirname "$PLAYWRIGHT_CLI_BIN")/../lib/node_modules/@playwright/cli/node_modules/playwright-core" && pwd 2>/dev/null || true)"
PLAYWRIGHT_CORE_PATH="$PLAYWRIGHT_CORE_PATH" node "$SCRIPT_DIR/fetch_with_playwright.js" "$url" "$tmp"
if jq empty "$tmp" >/dev/null 2>&1; then
mv "$tmp" "$OUT_DIR/$name.json"
return 0
fi
fi
rm -f "$tmp"
printf '%s\n' "failed to fetch JSON from $url" >> "$ERROR_FILE"
return 1
}
if ! fetch_json "wp-json-root" "https://komodostation.com/wp-json/"; then
printf '%s\n' "Komodo API unavailable or blocked; leaving Komodo reports empty." >> "$ERROR_FILE"
printf '%s\n' "Komodo fetch blocked; see $ERROR_FILE"
exit 0
fi
if ! fetch_json "product-controller-jpy" "https://komodostation.com/wp-json/wp/v2/product/413763"; then
printf '%s\n' "Komodo product fetch blocked; leaving Komodo reports empty." >> "$ERROR_FILE"
printf '%s\n' "Komodo fetch blocked; see $ERROR_FILE"
exit 0
fi
fetch_json "product-machine-jpy" "https://komodostation.com/wp-json/wp/v2/product/413772" 0 || true
fetch_json "product-frame-jpy" "https://komodostation.com/wp-json/wp/v2/product/413776" 0 || true
fetch_json "sections-controller-search" "https://komodostation.com/wp-json/wp/v2/sections?search=Steam%20Controller&per_page=100" || true
fetch_json "sections-machine-search" "https://komodostation.com/wp-json/wp/v2/sections?search=Steam%20Machine&per_page=100" 0 || true
fetch_json "sections-frame-search" "https://komodostation.com/wp-json/wp/v2/sections?search=Steam%20Frame&per_page=100" 0 || true
fetch_json "media-controller-search" "https://komodostation.com/wp-json/wp/v2/media?search=Steam%20Controller&per_page=100" || true
fetch_json "media-parent-product-controller" "https://komodostation.com/wp-json/wp/v2/media?parent=413763&per_page=100" || true
for id in $(jq -r '.[].id' "$OUT_DIR/sections-controller-search.json" 2>/dev/null || true); do
fetch_json "media-parent-section-$id" "https://komodostation.com/wp-json/wp/v2/media?parent=$id&per_page=100" || true
done
{
printf 'controller\t%s\n' "$(jq -r '.modified // "n/a"' "$OUT_DIR/product-controller-jpy.json" 2>/dev/null || printf 'n/a')"
printf 'machine\t%s\n' "$(jq -r '.modified // "n/a"' "$OUT_DIR/product-machine-jpy.json" 2>/dev/null || printf 'n/a')"
printf 'frame\t%s\n' "$(jq -r '.modified // "n/a"' "$OUT_DIR/product-frame-jpy.json" 2>/dev/null || printf 'n/a')"
} > "$REPORT_DIR/komodo-product-modified.tsv"
jq -r '.[]? | [.id,.date,.modified,.slug,.title.rendered] | @tsv' \
"$OUT_DIR/sections-controller-search.json" \
> "$REPORT_DIR/komodo-controller-sections.tsv"
jq -r '.[]? | [.id,.date,.modified,.slug,.title.rendered] | @tsv' \
"$OUT_DIR/sections-machine-search.json" \
> "$REPORT_DIR/komodo-machine-sections.tsv"
jq -r '.[]? | [.id,.date,.modified,.slug,.title.rendered] | @tsv' \
"$OUT_DIR/sections-frame-search.json" \
> "$REPORT_DIR/komodo-frame-sections.tsv"
jq -r '
.[]?
| select((.source_url + " " + .slug + " " + .title.rendered) | test("2026/|webm|mp4|avif|spec|manual"; "i"))
| [.id,.date,.modified,.slug,.mime_type,.post,.source_url]
| @tsv
' "$OUT_DIR/media-controller-search.json" > "$REPORT_DIR/komodo-controller-media-interesting.tsv"
: > "$REPORT_DIR/komodo-machine-media.tsv"
: > "$REPORT_DIR/komodo-frame-media.tsv"
jq -r '
.[]?
| select((.mime_type | startswith("image/")) or (.mime_type | startswith("video/")))
| [.id,.mime_type,.source_url]
| @tsv
' "$OUT_DIR"/media-*.json "$OUT_DIR"/media-parent-*.json 2>/dev/null \
| sort -u \
> "$REPORT_DIR/komodo-visual-assets.tsv"
printf '%s\n' \
"Saved Komodo API responses to $OUT_DIR" \
"Saved Komodo summaries to $REPORT_DIR"
+39
View File
@@ -0,0 +1,39 @@
#!/bin/sh
set -eu
if [ "$#" -lt 1 ]; then
echo "usage: $0 RUN_DIR" >&2
exit 1
fi
RUN_DIR="$1"
OUT_DIR="$RUN_DIR/api/steamdb"
REPORT_DIR="$RUN_DIR/reports"
mkdir -p "$OUT_DIR" "$REPORT_DIR"
ERROR_FILE="$REPORT_DIR/steamdb-errors.txt"
: > "$ERROR_FILE"
: > "$REPORT_DIR/steamdb-key-lines.txt"
fetch_html() {
name="$1"
url="$2"
if ! curl -A 'Mozilla/5.0' --retry 2 --retry-delay 2 --max-time 45 -fsSL "$url" > "$OUT_DIR/$name.html" 2>/dev/null; then
printf '%s\n' "failed to fetch $url" >> "$ERROR_FILE"
rm -f "$OUT_DIR/$name.html"
return 1
fi
}
fetch_html "controller-app" "https://steamdb.info/app/4165870/" || true
fetch_html "controller-history" "https://steamdb.info/app/4165870/history/" || true
fetch_html "controller-unboxing-app" "https://steamdb.info/app/4653940/" || true
fetch_html "controller-unboxing-package" "https://steamdb.info/sub/1620489/" || true
rg -n "Coming soon|released|ownersonly|free on demand|unboxing|package|depot|video" \
"$OUT_DIR" \
> "$REPORT_DIR/steamdb-key-lines.txt" || true
printf '%s\n' \
"Saved SteamDB pages to $OUT_DIR" \
"Saved matching lines to $REPORT_DIR/steamdb-key-lines.txt"
+29
View File
@@ -0,0 +1,29 @@
#!/bin/sh
set -eu
if [ "$#" -lt 1 ]; then
echo "usage: $0 RUN_DIR [TRACKING_DIR]" >&2
exit 1
fi
RUN_DIR="$1"
TRACKING_DIR="${2:-/tmp/SteamTracking-master}"
OUT_DIR="$RUN_DIR/reports"
mkdir -p "$OUT_DIR"
if [ ! -d "$TRACKING_DIR" ]; then
echo "missing tracking dir: $TRACKING_DIR" >&2
exit 1
fi
rg -n -i "Triton|Ibex|Puck|Steam Controller|firmware|pair|dock|dongle" \
"$TRACKING_DIR" \
> "$OUT_DIR/steamtracking-hits.txt" || true
rg -n -i "ShouldTritonPairInOobe|PairDongleTriton|UnpairedTriton|ibex_internal|ibex_external" \
"$TRACKING_DIR" \
> "$OUT_DIR/steamtracking-pairing-focus.txt" || true
printf '%s\n' \
"Saved SteamTracking grep output to $OUT_DIR/steamtracking-hits.txt" \
"Saved pairing-focused grep output to $OUT_DIR/steamtracking-pairing-focus.txt"
+39
View File
@@ -0,0 +1,39 @@
#!/bin/sh
set -eu
if [ "$#" -lt 1 ]; then
echo "usage: $0 RUN_DIR" >&2
exit 1
fi
RUN_DIR="$1"
OUT_DIR="$RUN_DIR/api/valve"
REPORT_DIR="$RUN_DIR/reports"
mkdir -p "$OUT_DIR" "$REPORT_DIR"
ERROR_FILE="$REPORT_DIR/valve-errors.txt"
: > "$ERROR_FILE"
: > "$REPORT_DIR/valve-key-lines.txt"
fetch_page() {
name="$1"
url="$2"
if ! curl -A 'Mozilla/5.0' --retry 2 --retry-delay 2 --max-time 45 -fsSL "$url" > "$OUT_DIR/$name.html" 2>/dev/null; then
printf '%s\n' "failed to fetch $url" >> "$ERROR_FILE"
rm -f "$OUT_DIR/$name.html"
return 1
fi
}
fetch_page "steamdeck-site" "https://www.steamdeck.com/" || true
fetch_page "steam-support-deck" "https://help.steampowered.com/en/wizard/HelpWithSteamDeck" || true
fetch_page "steam-controller-store" "https://store.steampowered.com/app/4165870/" || true
fetch_page "steam-hardware-root" "https://steampowered.com/hardware" || true
rg -n "Steam Controller|Steam Machine|Steam Frame|coming soon|manual|safety|warranty|preorder|release" \
"$OUT_DIR" \
> "$REPORT_DIR/valve-key-lines.txt" || true
printf '%s\n' \
"Saved Valve pages to $OUT_DIR" \
"Saved matching lines to $REPORT_DIR/valve-key-lines.txt"
+34
View File
@@ -0,0 +1,34 @@
#!/bin/sh
set -eu
SCRIPT_DIR="$(CDPATH= cd -- "$(dirname "$0")" && pwd)"
REPO_DIR="$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd)"
LOCAL_DIR="$REPO_DIR/.local"
PROFILE_DIR="${1:-${KOMODO_PROFILE_DIR:-$LOCAL_DIR/playwright-komodo-profile}}"
ENV_FILE="${KOMODO_ENV_FILE:-$LOCAL_DIR/komodo-env.sh}"
pids="$(ps aux | awk -v profile="$PROFILE_DIR" '
index($0, profile) && !index($0, "awk -v profile=") {
print $2
}
')"
if [ -n "$pids" ]; then
for pid in $pids; do
kill "$pid" 2>/dev/null || true
done
printf '%s\n' "Closed dedicated Komodo browser/profile processes for $PROFILE_DIR."
else
printf '%s\n' "No dedicated Komodo browser/profile processes found for $PROFILE_DIR."
fi
rm -f \
"$PROFILE_DIR/SingletonLock" \
"$PROFILE_DIR/SingletonSocket" \
"$PROFILE_DIR/SingletonCookie"
if [ -f "$ENV_FILE" ]; then
rm -f "$ENV_FILE"
printf '%s\n' "Removed $ENV_FILE"
fi
+162
View File
@@ -0,0 +1,162 @@
#!/usr/bin/env python3
import argparse
import json
from pathlib import Path
def read_json(path: Path):
if not path.exists():
return None
return json.loads(path.read_text(encoding="utf-8"))
def read_lines(path: Path):
if not path.exists():
return []
return [line.rstrip("\n") for line in path.read_text(encoding="utf-8").splitlines()]
def compare_products(prev_dir: Path, curr_dir: Path):
results = []
for label, name in [
("controller", "product-controller-jpy.json"),
("machine", "product-machine-jpy.json"),
("frame", "product-frame-jpy.json"),
]:
prev = read_json(prev_dir / "api" / "komodo" / name)
curr = read_json(curr_dir / "api" / "komodo" / name)
if not prev or not curr:
continue
prev_mod = prev.get("modified")
curr_mod = curr.get("modified")
if prev_mod != curr_mod:
results.append(f"- Komodo {label} product modified changed: `{prev_mod}` -> `{curr_mod}`")
return results
def parse_section_tsv(path: Path):
rows = {}
for line in read_lines(path):
if not line.strip():
continue
parts = line.split("\t")
if len(parts) < 5:
continue
sid, date, modified, slug, title = parts[:5]
rows[sid] = {
"date": date,
"modified": modified,
"slug": slug,
"title": title,
}
return rows
def compare_sections(prev_dir: Path, curr_dir: Path, label: str, filename: str):
prev = parse_section_tsv(prev_dir / "reports" / filename)
curr = parse_section_tsv(curr_dir / "reports" / filename)
lines = []
added_ids = sorted(set(curr) - set(prev), key=int)
removed_ids = sorted(set(prev) - set(curr), key=int)
shared_ids = sorted(set(curr) & set(prev), key=int)
for sid in added_ids:
row = curr[sid]
lines.append(f"- {label} section added: `{sid}` `{row['slug']}` modified `{row['modified']}`")
for sid in removed_ids:
row = prev[sid]
lines.append(f"- {label} section removed: `{sid}` `{row['slug']}`")
for sid in shared_ids:
if prev[sid]["modified"] != curr[sid]["modified"]:
lines.append(
f"- {label} section updated: `{sid}` `{curr[sid]['slug']}` `{prev[sid]['modified']}` -> `{curr[sid]['modified']}`"
)
return lines
def parse_tsv_set(path: Path):
return {line for line in read_lines(path) if line.strip()}
def compare_report_sets(prev_dir: Path, curr_dir: Path, label: str, filename: str):
prev = parse_tsv_set(prev_dir / "reports" / filename)
curr = parse_tsv_set(curr_dir / "reports" / filename)
lines = []
for line in sorted(curr - prev):
lines.append(f"- {label} added: `{line}`")
for line in sorted(prev - curr):
lines.append(f"- {label} removed: `{line}`")
return lines
def compare_text_lines(prev_dir: Path, curr_dir: Path, label: str, filename: str, limit: int = 20):
prev = parse_tsv_set(prev_dir / "reports" / filename)
curr = parse_tsv_set(curr_dir / "reports" / filename)
added = sorted(curr - prev)[:limit]
removed = sorted(prev - curr)[:limit]
lines = []
for line in added:
lines.append(f"- {label} added line: `{line}`")
for line in removed:
lines.append(f"- {label} removed line: `{line}`")
return lines
def build_summary(prev_dir: Path, curr_dir: Path):
lines = [
"# Run Comparison",
"",
f"Previous: `{prev_dir}`",
f"Current: `{curr_dir}`",
"",
"## Material Deltas",
"",
]
deltas = []
deltas.extend(compare_products(prev_dir, curr_dir))
deltas.extend(compare_sections(prev_dir, curr_dir, "Komodo controller", "komodo-controller-sections.tsv"))
deltas.extend(compare_sections(prev_dir, curr_dir, "Komodo machine", "komodo-machine-sections.tsv"))
deltas.extend(compare_sections(prev_dir, curr_dir, "Komodo frame", "komodo-frame-sections.tsv"))
deltas.extend(compare_report_sets(prev_dir, curr_dir, "Komodo controller media", "komodo-controller-media-interesting.tsv"))
deltas.extend(compare_report_sets(prev_dir, curr_dir, "Komodo machine media", "komodo-machine-media.tsv"))
deltas.extend(compare_report_sets(prev_dir, curr_dir, "Komodo frame media", "komodo-frame-media.tsv"))
deltas.extend(compare_text_lines(prev_dir, curr_dir, "SteamDB", "steamdb-key-lines.txt"))
deltas.extend(compare_text_lines(prev_dir, curr_dir, "Valve endpoint", "valve-key-lines.txt"))
if deltas:
lines.extend(deltas)
else:
lines.append("- No material deltas detected in the compared reports.")
lines.extend(
[
"",
"## Notes",
"",
"- This comparison is intentionally conservative.",
"- It only covers artifacts produced by the helper scripts.",
"- Review raw files when a line item looks ambiguous or noisy.",
"",
]
)
return "\n".join(lines)
def main():
parser = argparse.ArgumentParser(description="Compare two Steam hardware watch run directories.")
parser.add_argument("--previous", required=True, help="Previous run directory")
parser.add_argument("--current", required=True, help="Current run directory")
parser.add_argument("--output", required=True, help="Path to write markdown summary")
args = parser.parse_args()
prev_dir = Path(args.previous)
curr_dir = Path(args.current)
output = Path(args.output)
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(build_summary(prev_dir, curr_dir), encoding="utf-8")
if __name__ == "__main__":
main()
+165
View File
@@ -0,0 +1,165 @@
#!/usr/bin/env node
const fs = require("fs");
const { URL } = require("url");
function loadPlaywright() {
const candidates = [];
if (process.env.PLAYWRIGHT_CORE_PATH) candidates.push(process.env.PLAYWRIGHT_CORE_PATH);
candidates.push("playwright-core");
for (const candidate of candidates) {
try {
return require(candidate);
} catch (err) {
}
}
throw new Error("Unable to load playwright-core. Set PLAYWRIGHT_CORE_PATH or install playwright-core.");
}
function launchOptions(headless) {
const options = { headless };
if (process.env.PLAYWRIGHT_BROWSER_CHANNEL) {
options.channel = process.env.PLAYWRIGHT_BROWSER_CHANNEL;
}
return options;
}
function findKomodoPage(context, origin) {
const pages = context.pages();
return pages.find((candidate) => candidate.url().startsWith(origin)) || pages[0] || null;
}
async function main() {
const [, , targetUrl, outputPath, metaPath] = process.argv;
if (!targetUrl || !outputPath || !metaPath) {
console.error("usage: download_with_playwright.js URL OUTPUT_PATH META_PATH");
process.exit(1);
}
const { chromium } = loadPlaywright();
const headless = process.env.PLAYWRIGHT_HEADLESS !== "0";
const profileDir = process.env.PLAYWRIGHT_PROFILE_DIR;
const storageState = process.env.PLAYWRIGHT_STORAGE_STATE;
const referer = process.env.PLAYWRIGHT_REFERER;
const cdpEndpoint = process.env.PLAYWRIGHT_CDP_ENDPOINT;
const fetchTimeoutMs = Number(process.env.PLAYWRIGHT_FETCH_TIMEOUT_MS || 20000);
let browser = null;
let context = null;
let page = null;
let shouldCloseBrowser = false;
let shouldCloseContext = false;
let shouldClosePage = false;
if (cdpEndpoint) {
browser = await chromium.connectOverCDP(cdpEndpoint);
shouldCloseBrowser = true;
context = browser.contexts()[0];
if (!context) {
throw new Error(`No browser context available via CDP endpoint ${cdpEndpoint}`);
}
const origin = new URL(targetUrl).origin + "/";
page = findKomodoPage(context, origin);
if (!page) {
page = await context.newPage();
shouldClosePage = true;
}
} else if (profileDir) {
context = await chromium.launchPersistentContext(profileDir, {
...launchOptions(headless),
userAgent: "Mozilla/5.0",
});
shouldCloseContext = true;
page = context.pages()[0] || await context.newPage();
} else {
browser = await chromium.launch(launchOptions(headless));
shouldCloseBrowser = true;
context = await browser.newContext({
userAgent: "Mozilla/5.0",
storageState: storageState || undefined,
extraHTTPHeaders: referer ? { Referer: referer } : undefined,
});
shouldCloseContext = true;
page = await context.newPage();
}
try {
let result;
if (cdpEndpoint) {
const origin = new URL(targetUrl).origin + "/";
if (!page.url().startsWith(origin)) {
await page.goto(origin, {
waitUntil: "domcontentloaded",
timeout: 45000,
});
}
result = await page.evaluate(async ({ url, timeoutMs }) => {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), timeoutMs);
const response = await fetch(url, { credentials: "include", signal: controller.signal });
clearTimeout(timeout);
const buffer = await response.arrayBuffer();
const bytes = new Uint8Array(buffer);
let binary = "";
const chunkSize = 0x8000;
for (let i = 0; i < bytes.length; i += chunkSize) {
binary += String.fromCharCode(...bytes.subarray(i, i + chunkSize));
}
const headers = {};
response.headers.forEach((value, key) => {
headers[key] = value;
});
return {
ok: response.ok,
status: response.status,
url: response.url,
headers,
bodyBase64: btoa(binary),
};
}, { url: targetUrl, timeoutMs: fetchTimeoutMs });
fs.writeFileSync(outputPath, Buffer.from(result.bodyBase64, "base64"));
} else {
const response = await page.goto(targetUrl, {
waitUntil: "domcontentloaded",
timeout: 45000,
});
if (!response) {
throw new Error("No response received from navigation");
}
const body = await response.body();
fs.writeFileSync(outputPath, body);
result = {
ok: response.ok(),
status: response.status(),
url: response.url(),
headers: response.headers(),
};
}
fs.writeFileSync(
metaPath,
JSON.stringify(
result,
null,
2
)
);
} finally {
if (shouldClosePage && page && !page.isClosed()) {
await page.close();
}
if (shouldCloseContext && context) {
await context.close();
}
if (shouldCloseBrowser && browser) {
await browser.close();
}
}
}
main().catch((err) => {
console.error(err && err.stack ? err.stack : String(err));
process.exit(1);
});
+166
View File
@@ -0,0 +1,166 @@
#!/usr/bin/env python3
import argparse
from pathlib import Path
def read_lines(path: Path):
if not path.exists():
return []
return [line.rstrip("\n") for line in path.read_text(encoding="utf-8", errors="ignore").splitlines()]
def first_n_nonempty(path: Path, n: int):
return [line for line in read_lines(path) if line.strip()][:n]
def count_lines(path: Path):
return len([line for line in read_lines(path) if line.strip()])
def has_nonempty(path: Path):
return count_lines(path) > 0
def blocked_asset_urls(path: Path, n: int):
urls = []
if not path.exists():
return urls
for line in read_lines(path):
parts = line.split("\t")
if len(parts) > 4 and parts[4].startswith("http"):
urls.append(parts[4])
if len(urls) >= n:
break
return urls
def build_draft(run_dir: Path):
reports = run_dir / "reports"
run_date = run_dir.name
compare_files = sorted(reports.glob("compare-vs-*.md"))
compare_file = compare_files[-1] if compare_files else None
compare_lines = []
if compare_file:
compare_lines = [line for line in read_lines(compare_file) if line.startswith("- ")][:12]
komodo_mods = first_n_nonempty(reports / "komodo-product-modified.tsv", 10)
steamdb_lines = first_n_nonempty(reports / "steamdb-key-lines.txt", 8)
valve_lines = first_n_nonempty(reports / "valve-key-lines.txt", 8)
tracking_lines = first_n_nonempty(reports / "steamtracking-pairing-focus.txt", 8)
discovered_count = count_lines(reports / "discovered-visual-assets.tsv")
retrieved_count = count_lines(reports / "retrieved-visual-assets.tsv")
blocked_count = count_lines(reports / "blocked-visual-assets.tsv")
manual_urls = blocked_asset_urls(reports / "blocked-visual-assets.tsv", 10)
komodo_blocked = has_nonempty(reports / "komodo-errors.txt")
steamdb_blocked = has_nonempty(reports / "steamdb-errors.txt")
valve_blocked = has_nonempty(reports / "valve-errors.txt")
lines = [
f"# Status Draft: {run_date}",
"",
"## Suggested `status/current.md` Update",
"",
f"Last updated: `{run_date}`",
"",
"### Observed Changes Since Previous Run",
"",
]
if compare_lines:
lines.extend(compare_lines)
else:
lines.append("- No comparison report was available.")
lines.extend(
[
"",
"### Source Snapshot Draft",
"",
"#### Komodo",
]
)
if komodo_blocked:
lines.append("- Komodo was blocked or partially unavailable in this run. See `komodo-errors.txt`.")
elif komodo_mods:
lines.extend([f"- `{line}`" for line in komodo_mods])
else:
lines.append("- No Komodo product summary found.")
lines.append(f"- Discovered visual assets: `{discovered_count}`")
lines.append(f"- Retrieved visual assets: `{retrieved_count}`")
lines.append(f"- Blocked visual assets: `{blocked_count}`")
lines.extend(["", "#### SteamDB"])
if steamdb_blocked:
lines.append("- SteamDB had blocked or failed fetches in this run. See `steamdb-errors.txt`.")
elif steamdb_lines:
lines.extend([f"- `{line}`" for line in steamdb_lines])
else:
lines.append("- No SteamDB key-line report found.")
lines.extend(["", "#### SteamTracking / GameTracking"])
if tracking_lines:
lines.extend([f"- `{line}`" for line in tracking_lines])
else:
lines.append("- No focused SteamTracking report found.")
lines.extend(["", "#### Valve Support / CDN"])
if valve_blocked:
lines.append("- Some Valve endpoint fetches failed in this run. See `valve-errors.txt`.")
elif valve_lines:
lines.extend([f"- `{line}`" for line in valve_lines])
else:
lines.append("- No Valve key-line report found.")
lines.extend(
[
"",
"### Analyst Judgment Needed",
"",
"- Price confidence:",
"- Release date confidence:",
"- Same-time launch confidence:",
"",
"## Suggested Run Note Additions",
"",
f"- Artifacts live in `{run_dir}`",
f"- Discovered visual assets: `{reports / 'discovered-visual-assets.tsv'}`",
f"- Retrieved visual assets: `{reports / 'retrieved-visual-assets.tsv'}`",
f"- Blocked visual assets: `{reports / 'blocked-visual-assets.tsv'}`",
f"- Manual retry URLs: `{reports / 'manual-asset-urls.txt'}`",
]
)
if compare_file:
lines.append(f"- Comparison report: `{compare_file}`")
if manual_urls:
lines.extend(["", "- Manual retry candidates:"])
lines.extend([f"- `{url}`" for url in manual_urls])
lines.extend(
[
"",
"- Summarize only material deltas from the comparison report.",
"- If new visual assets were downloaded, call out the most meaningful ones explicitly.",
"",
]
)
return "\n".join(lines)
def main():
parser = argparse.ArgumentParser(description="Draft a status update from a run directory.")
parser.add_argument("--run-dir", required=True)
parser.add_argument("--output", help="Optional output path")
args = parser.parse_args()
run_dir = Path(args.run_dir)
output = Path(args.output) if args.output else run_dir / "reports" / "status-draft.md"
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(build_draft(run_dir), encoding="utf-8")
if __name__ == "__main__":
main()
+116
View File
@@ -0,0 +1,116 @@
#!/usr/bin/env node
const fs = require("fs");
const { URL } = require("url");
function loadPlaywright() {
const candidates = [];
if (process.env.PLAYWRIGHT_CORE_PATH) candidates.push(process.env.PLAYWRIGHT_CORE_PATH);
candidates.push("playwright-core");
for (const candidate of candidates) {
try {
return require(candidate);
} catch (err) {
}
}
throw new Error(
"Unable to load playwright-core. Set PLAYWRIGHT_CORE_PATH or install playwright-core."
);
}
function launchOptions(headless) {
const options = { headless };
if (process.env.PLAYWRIGHT_BROWSER_CHANNEL) {
options.channel = process.env.PLAYWRIGHT_BROWSER_CHANNEL;
}
return options;
}
function findKomodoPage(context, origin) {
const pages = context.pages();
return pages.find((candidate) => candidate.url().startsWith(origin)) || pages[0] || null;
}
async function main() {
const [, , targetUrl, outputPath] = process.argv;
if (!targetUrl || !outputPath) {
console.error("usage: fetch_with_playwright.js URL OUTPUT_PATH");
process.exit(1);
}
const { chromium } = loadPlaywright();
const headless = process.env.PLAYWRIGHT_HEADLESS !== "0";
const profileDir = process.env.PLAYWRIGHT_PROFILE_DIR;
const storageState = process.env.PLAYWRIGHT_STORAGE_STATE;
const referer = process.env.PLAYWRIGHT_REFERER;
const cdpEndpoint = process.env.PLAYWRIGHT_CDP_ENDPOINT;
const fetchTimeoutMs = Number(process.env.PLAYWRIGHT_FETCH_TIMEOUT_MS || 20000);
let browser = null;
let context = null;
let page = null;
let shouldCloseBrowser = false;
let shouldCloseContext = false;
let shouldClosePage = false;
if (cdpEndpoint) {
browser = await chromium.connectOverCDP(cdpEndpoint);
shouldCloseBrowser = true;
context = browser.contexts()[0];
if (!context) {
throw new Error(`No browser context available via CDP endpoint ${cdpEndpoint}`);
}
const origin = new URL(targetUrl).origin + "/";
page = findKomodoPage(context, origin);
if (!page) {
page = await context.newPage();
shouldClosePage = true;
}
} else if (profileDir) {
context = await chromium.launchPersistentContext(profileDir, {
...launchOptions(headless),
userAgent: "Mozilla/5.0",
});
shouldCloseContext = true;
page = context.pages()[0] || await context.newPage();
} else {
browser = await chromium.launch(launchOptions(headless));
shouldCloseBrowser = true;
context = await browser.newContext({
userAgent: "Mozilla/5.0",
storageState: storageState || undefined,
extraHTTPHeaders: referer ? { Referer: referer } : undefined,
});
shouldCloseContext = true;
page = await context.newPage();
}
try {
const origin = new URL(targetUrl).origin + "/";
if (!page.url().startsWith(origin)) {
await page.goto(origin, { waitUntil: "domcontentloaded", timeout: 45000 });
}
const body = await page.evaluate(async ({ url, timeoutMs }) => {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), timeoutMs);
const response = await fetch(url, { credentials: "include", signal: controller.signal });
clearTimeout(timeout);
return await response.text();
}, { url: targetUrl, timeoutMs: fetchTimeoutMs });
fs.writeFileSync(outputPath, body);
} finally {
if (shouldClosePage && page && !page.isClosed()) {
await page.close();
}
if (shouldCloseContext && context) {
await context.close();
}
if (shouldCloseBrowser && browser) {
await browser.close();
}
}
}
main().catch((err) => {
console.error(err && err.stack ? err.stack : String(err));
process.exit(1);
});
+60
View File
@@ -0,0 +1,60 @@
#!/bin/sh
set -eu
if [ "$#" -lt 1 ]; then
echo "usage: $0 YYYY-MM-DD [base_dir]" >&2
exit 1
fi
RUN_DATE="$1"
BASE_DIR="${2:-$HOME/steam_hardware_watch}"
RUN_DIR="$BASE_DIR/$RUN_DATE"
mkdir -p "$RUN_DIR/api" "$RUN_DIR/assets" "$RUN_DIR/frames" "$RUN_DIR/reports"
RUN_NOTE_DIR="$(CDPATH= cd -- "$(dirname "$0")/../status/runs" && pwd)"
RUN_NOTE="$RUN_NOTE_DIR/$RUN_DATE.md"
if [ ! -f "$RUN_NOTE" ]; then
cat >"$RUN_NOTE" <<EOF
# Run Note: $RUN_DATE
## Summary
-
## Source Checks
### Komodo
-
### SteamDB
-
### SteamTracking / GameTracking
-
### Valve Support / CDN
-
### Customs / Regulatory
-
## Effect On Key Questions
- Price:
- Release date:
- Same-time launch:
## Next Checks
-
EOF
fi
printf 'RUN_DIR=%s\nRUN_NOTE=%s\n' "$RUN_DIR" "$RUN_NOTE"
+84
View File
@@ -0,0 +1,84 @@
#!/bin/sh
set -eu
if [ "$#" -lt 1 ]; then
echo "usage: $0 YYYY-MM-DD [base_dir] [tracking_dir]" >&2
exit 1
fi
RUN_DATE="$1"
BASE_DIR="${2:-$HOME/steam_hardware_watch}"
TRACKING_DIR="${3:-/tmp/SteamTracking-master}"
SCRIPT_DIR="$(CDPATH= cd -- "$(dirname "$0")" && pwd)"
REPO_DIR="$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd)"
KOMODO_ENV_FILE="${KOMODO_ENV_FILE:-$REPO_DIR/.local/komodo-env.sh}"
cleanup() {
if [ "${KOMODO_KEEP_BROWSER_OPEN:-0}" = "1" ]; then
return 0
fi
if [ -f "$KOMODO_ENV_FILE" ] && [ -x "$SCRIPT_DIR/close_komodo.sh" ]; then
"$SCRIPT_DIR/close_komodo.sh" >/dev/null 2>&1 || true
fi
}
trap cleanup EXIT INT TERM
INIT_OUTPUT="$("$SCRIPT_DIR/init_run.sh" "$RUN_DATE" "$BASE_DIR")"
RUN_DIR="$(printf '%s\n' "$INIT_OUTPUT" | awk -F= '/^RUN_DIR=/{print $2}')"
RUN_NOTE="$(printf '%s\n' "$INIT_OUTPUT" | awk -F= '/^RUN_NOTE=/{print $2}')"
if [ -z "$RUN_DIR" ] || [ -z "$RUN_NOTE" ]; then
echo "failed to initialize run folder" >&2
exit 1
fi
printf '%s\n' "Run dir: $RUN_DIR"
printf '%s\n' "Run note: $RUN_NOTE"
"$SCRIPT_DIR/check_komodo.sh" "$RUN_DIR"
"$SCRIPT_DIR/check_steamdb.sh" "$RUN_DIR"
"$SCRIPT_DIR/check_steamtracking.sh" "$RUN_DIR" "$TRACKING_DIR"
"$SCRIPT_DIR/check_valve_endpoints.sh" "$RUN_DIR"
python3 "$SCRIPT_DIR/save_visual_assets.py" --run-dir "$RUN_DIR" --base-dir "$BASE_DIR"
PREVIOUS_DIR=""
for candidate in $(find "$BASE_DIR" -mindepth 1 -maxdepth 1 -type d -name '????-??-??' | sort); do
if [ "$candidate" = "$RUN_DIR" ]; then
continue
fi
case "$(basename "$candidate")" in
"$RUN_DATE") ;;
*)
if [ "$(basename "$candidate")" \< "$RUN_DATE" ]; then
PREVIOUS_DIR="$candidate"
fi
;;
esac
done
if [ -n "$PREVIOUS_DIR" ]; then
COMPARE_OUT="$RUN_DIR/reports/compare-vs-$(basename "$PREVIOUS_DIR").md"
python3 "$SCRIPT_DIR/compare_runs.py" \
--previous "$PREVIOUS_DIR" \
--current "$RUN_DIR" \
--output "$COMPARE_OUT"
printf '%s\n' "Comparison report: $COMPARE_OUT"
else
printf '%s\n' "No previous run folder found under $BASE_DIR"
fi
STATUS_DRAFT_OUT="$RUN_DIR/reports/status-draft.md"
python3 "$SCRIPT_DIR/draft_status_update.py" \
--run-dir "$RUN_DIR" \
--output "$STATUS_DRAFT_OUT"
printf '%s\n' "Status draft: $STATUS_DRAFT_OUT"
RUN_SUMMARY_OUT="$RUN_DIR/reports/run-summary.md"
python3 "$SCRIPT_DIR/write_run_summary.py" \
--run-dir "$RUN_DIR" \
--output "$RUN_SUMMARY_OUT"
printf '%s\n' "Run summary: $RUN_SUMMARY_OUT"
printf '%s\n' "Run complete."
+331
View File
@@ -0,0 +1,331 @@
#!/usr/bin/env python3
import argparse
import json
import urllib.error
import urllib.request
from pathlib import Path
from urllib.parse import urlparse
VISUAL_EXTS = {
".png",
".jpg",
".jpeg",
".webp",
".avif",
".svg",
".gif",
".mp4",
".webm",
".mov",
".m4v",
}
def is_visual_url(url: str) -> bool:
path = urlparse(url).path.lower()
return any(path.endswith(ext) for ext in VISUAL_EXTS)
def walk_json(value):
if isinstance(value, dict):
yield value
for child in value.values():
yield from walk_json(child)
elif isinstance(value, list):
for item in value:
yield from walk_json(item)
def collect_from_json(path: Path):
if path.name in {"media-spec-search.json", "media-manual-search.json", "media-video.json"}:
return []
try:
data = json.loads(path.read_text(encoding="utf-8"))
except Exception:
return []
hits = []
for obj in walk_json(data):
if not isinstance(obj, dict):
continue
url = obj.get("source_url")
mime = obj.get("mime_type", "")
if isinstance(url, str) and url.startswith("http"):
if mime.startswith("image/") or mime.startswith("video/") or is_visual_url(url):
hits.append((url, mime, str(path)))
return hits
def collect_from_seed_reports(base_dir: Path, current_run_dir: Path):
if not base_dir.exists():
return []
hits = []
patterns = (
("discovered-visual-assets.tsv", 0, 2),
("retrieved-visual-assets.tsv", 0, 2),
("blocked-visual-assets.tsv", None, 4),
("downloaded-visual-assets.tsv", 0, 2),
)
for report_name, mime_index, url_index in patterns:
for report in sorted(base_dir.glob(f"????-??-??/reports/{report_name}")):
run_dir = report.parent.parent
if run_dir == current_run_dir:
continue
for raw_line in report.read_text(encoding="utf-8", errors="ignore").splitlines():
line = raw_line.strip()
if not line:
continue
parts = line.split("\t")
if len(parts) <= url_index:
continue
url = parts[url_index]
if not isinstance(url, str) or not url.startswith("http") or not is_visual_url(url):
continue
mime = parts[mime_index] if mime_index is not None and len(parts) > mime_index else ""
hits.append((url, mime, f"seed:{run_dir.name}:{report_name}"))
return hits
def destination_for(base_dir: Path, url: str) -> Path:
parsed = urlparse(url)
rel = (parsed.netloc + parsed.path).lstrip("/")
return base_dir / rel
def is_protected_komodo_asset(url: str) -> bool:
parsed = urlparse(url)
host = parsed.netloc.lower()
path = parsed.path.lower()
if "komodostation.com" not in host:
return False
return path.endswith((".mp4", ".webm", ".mov", ".m4v", ".avif"))
def detect_format(data: bytes) -> str:
head = data[:512]
lower = head.lower()
if head.startswith(b"\x89PNG\r\n\x1a\n"):
return "png"
if head.startswith(b"\xff\xd8\xff"):
return "jpeg"
if head.startswith((b"GIF87a", b"GIF89a")):
return "gif"
if head.startswith(b"RIFF") and b"WEBP" in head[:16]:
return "webp"
if head.startswith(b"\x1a\x45\xdf\xa3"):
return "webm"
if b"ftyp" in head[:32]:
if b"avif" in head[:32] or b"avis" in head[:32]:
return "avif"
return "mp4"
if b"<svg" in lower:
return "svg"
if lower.startswith(b"<!doctype html") or lower.startswith(b"<html"):
return "html"
return "unknown"
def expected_family(url: str) -> str:
path = urlparse(url).path.lower()
if path.endswith(".png"):
return "png"
if path.endswith((".jpg", ".jpeg")):
return "jpeg"
if path.endswith(".gif"):
return "gif"
if path.endswith(".webp"):
return "webp"
if path.endswith(".avif"):
return "avif"
if path.endswith(".svg"):
return "svg"
if path.endswith(".webm"):
return "webm"
if path.endswith((".mp4", ".mov", ".m4v")):
return "mp4"
return "unknown"
def is_valid_download(url: str, content_type: str, data: bytes):
detected = detect_format(data)
expected = expected_family(url)
ctype = (content_type or "").lower()
if detected == "html":
return False, detected, expected
if ctype.startswith("text/html"):
return False, detected, expected
if expected != "unknown" and detected != expected:
return False, detected, expected
if detected == "unknown":
return False, detected, expected
return True, detected, expected
def download(url: str, dest: Path):
dest.parent.mkdir(parents=True, exist_ok=True)
req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
with urllib.request.urlopen(req, timeout=15) as response:
data = response.read()
status = getattr(response, "status", 200)
content_type = response.headers.get_content_type()
ok, detected, expected = is_valid_download(url, content_type, data)
if not ok:
return {
"ok": False,
"status": str(status),
"content_type": content_type,
"detected": detected,
"expected": expected,
}
dest.write_bytes(data)
return {
"ok": True,
"status": str(status),
"content_type": content_type,
"detected": detected,
"expected": expected,
}
def main():
parser = argparse.ArgumentParser(description="Save discovered visual assets from a run directory.")
parser.add_argument("--run-dir", required=True)
parser.add_argument("--base-dir", help="Optional base directory containing prior dated runs to seed discovery from")
args = parser.parse_args()
run_dir = Path(args.run_dir)
base_dir = Path(args.base_dir) if args.base_dir else run_dir.parent
api_dir = run_dir / "api"
asset_dir = run_dir / "assets" / "discovered"
report_dir = run_dir / "reports"
report_dir.mkdir(parents=True, exist_ok=True)
asset_dir.mkdir(parents=True, exist_ok=True)
hits = []
for path in (api_dir / "komodo").rglob("*.json"):
hits.extend(collect_from_json(path))
hits.extend(collect_from_seed_reports(base_dir, run_dir))
unique = {}
for url, mime, source in hits:
unique.setdefault(url, {"mime": mime, "sources": set()})
if mime and not unique[url]["mime"]:
unique[url]["mime"] = mime
unique[url]["sources"].add(source)
discovered_rows = []
retrieved_rows = []
error_rows = []
for url in sorted(unique):
discovered_rows.append(
"\t".join(
[
unique[url]["mime"],
expected_family(url),
url,
",".join(sorted(unique[url]["sources"])),
]
)
)
dest = destination_for(asset_dir, url)
if dest.exists():
data = dest.read_bytes()[:512]
ok, detected, expected = is_valid_download(url, unique[url]["mime"], data)
if not ok:
dest.unlink(missing_ok=True)
result = None
if not dest.exists():
if is_protected_komodo_asset(url):
result = {
"ok": False,
"status": "session_required",
"content_type": unique[url]["mime"],
"detected": "protected_komodo_asset",
"expected": expected_family(url),
}
else:
try:
result = download(url, dest)
except urllib.error.HTTPError as err:
result = {
"ok": False,
"status": str(err.code),
"content_type": getattr(err, "headers", {}).get_content_type() if getattr(err, "headers", None) else "",
"detected": "http_error",
"expected": expected_family(url),
}
except Exception as err:
result = {
"ok": False,
"status": "error",
"content_type": "",
"detected": type(err).__name__,
"expected": expected_family(url),
}
else:
result = {
"ok": True,
"status": "cached",
"content_type": unique[url]["mime"],
"detected": detect_format(dest.read_bytes()[:512]),
"expected": expected_family(url),
}
if result["ok"]:
retrieved_rows.append(
"\t".join(
[
unique[url]["mime"],
result["detected"],
url,
str(dest),
",".join(sorted(unique[url]["sources"])),
]
)
)
else:
dest.unlink(missing_ok=True)
error_rows.append(
"\t".join(
[
result["status"],
result["content_type"],
result["detected"],
result["expected"],
url,
",".join(sorted(unique[url]["sources"])),
]
)
)
(report_dir / "discovered-visual-assets.tsv").write_text(
"\n".join(discovered_rows) + ("\n" if discovered_rows else ""),
encoding="utf-8",
)
(report_dir / "retrieved-visual-assets.tsv").write_text(
"\n".join(retrieved_rows) + ("\n" if retrieved_rows else ""),
encoding="utf-8",
)
(report_dir / "blocked-visual-assets.tsv").write_text(
"\n".join(error_rows) + ("\n" if error_rows else ""),
encoding="utf-8",
)
manual_urls = sorted({row.split("\t")[4] for row in error_rows if len(row.split("\t")) > 4})
(report_dir / "manual-asset-urls.txt").write_text(
"\n".join(manual_urls) + ("\n" if manual_urls else ""),
encoding="utf-8",
)
# Backward-compatible alias for earlier script versions.
(report_dir / "downloaded-visual-assets.tsv").write_text(
"\n".join(retrieved_rows) + ("\n" if retrieved_rows else ""),
encoding="utf-8",
)
if __name__ == "__main__":
main()
+160
View File
@@ -0,0 +1,160 @@
#!/usr/bin/env python3
import argparse
from pathlib import Path
MAX_LINE_LEN = 220
FILTER_TERMS = (
"controller",
"machine",
"frame",
"triton",
"puck",
"ibex",
"unboxing",
"coming",
"2026",
"pair",
"firmware",
)
REJECT_TERMS = (
"application_config",
"data-config",
"&quot;",
"localized_",
)
def read_lines(path: Path):
if not path.exists():
return []
return [line.rstrip("\n") for line in path.read_text(encoding="utf-8", errors="ignore").splitlines() if line.strip()]
def count(path: Path):
return len(read_lines(path))
def first(path: Path, n: int):
return read_lines(path)[:n]
def concise(line: str):
trimmed = " ".join(line.split())
if len(trimmed) > MAX_LINE_LEN:
return trimmed[: MAX_LINE_LEN - 3] + "..."
return trimmed
def filtered_first(path: Path, n: int):
selected = []
for line in read_lines(path):
lowered = line.lower()
if any(term in lowered for term in REJECT_TERMS):
continue
if path.name == "valve-key-lines.txt" and not any(term in lowered for term in FILTER_TERMS):
continue
selected.append(concise(line))
if len(selected) >= n:
break
return selected
def blocked_asset_urls(path: Path, n: int):
urls = []
if not path.exists():
return urls
for line in read_lines(path):
parts = line.split("\t")
if len(parts) > 4 and parts[4].startswith("http"):
urls.append(parts[4])
if len(urls) >= n:
break
return urls
def count_block_reason(path: Path, reason: str):
total = 0
if not path.exists():
return total
for line in read_lines(path):
parts = line.split("\t")
if parts and parts[0] == reason:
total += 1
return total
def build(run_dir: Path):
reports = run_dir / "reports"
compare = sorted(reports.glob("compare-vs-*.md"))
compare_file = compare[-1] if compare else None
discovered = count(reports / "discovered-visual-assets.tsv")
retrieved = count(reports / "retrieved-visual-assets.tsv")
blocked = count(reports / "blocked-visual-assets.tsv")
session_required = count_block_reason(reports / "blocked-visual-assets.tsv", "session_required")
lines = [
f"# Run Summary: {run_dir.name}",
"",
f"Artifacts: `{run_dir}`",
"",
"## At A Glance",
"",
f"- Komodo blocked: `{'yes' if count(reports / 'komodo-errors.txt') else 'no'}`",
f"- SteamDB blocked: `{'yes' if count(reports / 'steamdb-errors.txt') else 'no'}`",
f"- Valve blocked: `{'yes' if count(reports / 'valve-errors.txt') else 'no'}`",
f"- Discovered visual assets: `{discovered}`",
f"- Retrieved visual assets: `{retrieved}`",
f"- Blocked visual assets: `{blocked}`",
f"- Blocked assets requiring trusted session: `{session_required}`",
"",
"## Most Relevant Outputs",
"",
f"- Status draft: `{reports / 'status-draft.md'}`",
f"- Discovered assets: `{reports / 'discovered-visual-assets.tsv'}`",
f"- Retrieved assets: `{reports / 'retrieved-visual-assets.tsv'}`",
f"- Blocked assets: `{reports / 'blocked-visual-assets.tsv'}`",
f"- Manual retry URLs: `{reports / 'manual-asset-urls.txt'}`",
]
if compare_file:
lines.append(f"- Comparison report: `{compare_file}`")
lines.extend(["", "## Notable Lines", ""])
notable = []
notable.extend([f"- SteamDB: `{line}`" for line in filtered_first(reports / "steamdb-key-lines.txt", 5)])
notable.extend([f"- SteamTracking: `{line}`" for line in filtered_first(reports / "steamtracking-pairing-focus.txt", 5)])
notable.extend([f"- Valve: `{line}`" for line in filtered_first(reports / "valve-key-lines.txt", 5)])
notable.extend([f"- Asset blocked: `{line}`" for line in filtered_first(reports / "blocked-visual-assets.tsv", 5)])
if notable:
lines.extend(notable)
else:
lines.append("- No notable lines were extracted.")
manual_urls = blocked_asset_urls(reports / "blocked-visual-assets.tsv", 10)
lines.extend(["", "## Manual Follow-Up URLs", ""])
if manual_urls:
lines.extend([f"- `{url}`" for url in manual_urls])
else:
lines.append("- No blocked asset URLs were recorded in this run.")
lines.extend(["", "## Next Human Step", "", "- Review this summary first, then open the detailed reports only if something changed or looks important.", ""])
return "\n".join(lines)
def main():
parser = argparse.ArgumentParser(description="Write a concise human-readable run summary.")
parser.add_argument("--run-dir", required=True)
parser.add_argument("--output", help="Optional output path")
args = parser.parse_args()
run_dir = Path(args.run_dir)
output = Path(args.output) if args.output else run_dir / "reports" / "run-summary.md"
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(build(run_dir), encoding="utf-8")
if __name__ == "__main__":
main()