abidlabs's picture
abidlabs HF Staff
Upload folder using huggingface_hub
7896de8 verified
Raw
History Blame Contribute Delete
78.2 kB
import argparse
import os
import re
import sys
from pathlib import Path
import huggingface_hub
import trackio
from trackio import freeze, show, sync
from trackio.cli_helpers import (
error_exit,
format_alerts,
format_artifact,
format_artifacts,
format_json,
format_list,
format_metric_values,
format_project_summary,
format_query_result,
format_run_summary,
format_snapshot,
format_spaces,
format_system_metric_names,
format_system_metrics,
)
from trackio.frontend_config import (
TRACKIO_CONFIG_PATH,
get_persisted_frontend_dir,
set_persisted_frontend_dir,
unset_persisted_frontend_dir,
)
from trackio.markdown import Markdown
from trackio.server import get_project_summary, get_run_summary
from trackio.sqlite_storage import SQLiteStorage
def _get_space(args):
return getattr(args, "space", None)
def _get_remote(args):
from trackio.remote_client import RemoteClient
space = _get_space(args)
if not space:
return None
hf_token = getattr(args, "hf_token", None)
return RemoteClient(space, hf_token=hf_token)
def _handle_status():
print("Reading local Trackio projects...\n")
projects = SQLiteStorage.get_projects()
if not projects:
print("No Trackio projects found.")
return
local_projects = []
synced_projects = []
unsynced_projects = []
for project in projects:
space_id = SQLiteStorage.get_space_id(project)
if space_id is None:
local_projects.append(project)
elif SQLiteStorage.has_pending_data(project):
unsynced_projects.append(project)
else:
synced_projects.append(project)
print("Finished reading Trackio projects")
if local_projects:
print(f" * {len(local_projects)} local trackio project(s) [OK]")
if synced_projects:
print(f" * {len(synced_projects)} trackio project(s) synced to Spaces [OK]")
if unsynced_projects:
print(
f" * {len(unsynced_projects)} trackio project(s) with unsynced changes [WARNING]:"
)
for p in unsynced_projects:
print(f" - {p}")
if unsynced_projects:
print(
f"\nRun `trackio sync --project {unsynced_projects[0]}` to sync. "
"Or run `trackio sync --all` to sync all unsynced changes."
)
def _handle_sync(args):
from trackio.deploy import sync_incremental
if args.sync_all and args.project:
error_exit("Cannot use --all and --project together.")
if not args.sync_all and not args.project:
error_exit("Must provide either --project or --all.")
if args.sync_all:
projects = SQLiteStorage.get_projects()
synced_any = False
for project in projects:
space_id = SQLiteStorage.get_space_id(project)
if space_id and SQLiteStorage.has_pending_data(project):
sync_incremental(
project,
space_id,
private=args.private,
pending_only=True,
frontend_dir=args.frontend,
)
synced_any = True
if not synced_any:
print("No projects with unsynced data found.")
else:
space_id = args.space_id
if space_id is None:
space_id = SQLiteStorage.get_space_id(args.project)
sync(
project=args.project,
space_id=space_id,
private=args.private,
force=args.force,
sdk=args.sdk,
frontend_dir=args.frontend,
)
def _handle_config(args):
if args.config_command == "get":
frontend_dir = get_persisted_frontend_dir()
if frontend_dir is None:
print("No Trackio frontend config is set.")
print(f"Config file: {TRACKIO_CONFIG_PATH}")
return
print(f"frontend: {frontend_dir}")
print(f"config: {TRACKIO_CONFIG_PATH}")
return
if args.config_command == "set":
try:
frontend_dir = set_persisted_frontend_dir(args.frontend)
except ValueError as e:
error_exit(str(e))
print(f"Saved Trackio default frontend: {frontend_dir}")
print("Reset with `trackio config unset frontend`.")
return
if args.config_command == "unset":
removed = unset_persisted_frontend_dir()
if removed:
print("Removed Trackio default frontend.")
else:
print("No Trackio default frontend was set.")
return
def _extract_reports(
run: str, logs: list[dict], report_name: str | None = None
) -> list[dict]:
reports = []
for log in logs:
timestamp = log.get("timestamp")
step = log.get("step")
for key, value in log.items():
if report_name is not None and key != report_name:
continue
if isinstance(value, dict) and value.get("_type") == Markdown.TYPE:
content = value.get("_value")
if isinstance(content, str):
reports.append(
{
"run": run,
"report": key,
"step": step,
"timestamp": timestamp,
"content": content,
}
)
return reports
def _handle_query(args):
remote = _get_remote(args)
try:
if remote:
result = remote.predict(args.project, args.sql, api_name="/query_project")
else:
result = SQLiteStorage.query_project(args.project, args.sql)
except FileNotFoundError as e:
error_exit(str(e))
except ValueError as e:
error_exit(str(e))
if args.json:
print(format_json(result))
else:
print(format_query_result(result))
def _datetime_to_iso(value):
return value.isoformat() if value is not None else None
def _space_url(space) -> str | None:
host = getattr(space, "host", None)
if host:
return host if host.startswith("http") else f"https://{host}"
subdomain = getattr(space, "subdomain", None)
if subdomain:
return f"https://{subdomain}.hf.space"
space_id = getattr(space, "id", None)
if not space_id:
return None
return f"https://huggingface.co/spaces/{space_id}"
def _serialize_space(space) -> dict:
space_id = getattr(space, "id", "")
namespace, _, name = space_id.partition("/")
return {
"id": space_id,
"namespace": namespace,
"name": name or space_id,
"author": getattr(space, "author", None) or namespace,
"private": bool(getattr(space, "private", False)),
"sdk": getattr(space, "sdk", None),
"url": _space_url(space),
"last_modified": _datetime_to_iso(
getattr(space, "last_modified", None)
or getattr(space, "lastModified", None)
),
"created_at": _datetime_to_iso(getattr(space, "created_at", None)),
"tags": getattr(space, "tags", None) or [],
}
def _maybe_handle_logbook_run_argv() -> bool:
argv = sys.argv[1:]
try:
logbook_idx = argv.index("logbook")
except ValueError:
return False
if len(argv) <= logbook_idx + 1 or argv[logbook_idx + 1] != "run":
return False
run_argv = argv[logbook_idx + 2 :]
run_parser = argparse.ArgumentParser(
prog=f"{os.path.basename(sys.argv[0])} logbook run",
description=(
"Run a command; log the command, its scripts, and output to a page"
),
)
run_parser.add_argument("--page", help="Page title or slug")
run_parser.add_argument("--title", help="Cell title")
run_parser.add_argument(
"--no-artifacts",
action="store_true",
help="Do not record output model/data files as artifact cells",
)
if "--" in run_argv:
sep = run_argv.index("--")
opts = run_parser.parse_args(run_argv[:sep])
command = run_argv[sep + 1 :]
else:
opts, command = run_parser.parse_known_args(run_argv)
if not command:
run_parser.error("No command provided. Use: trackio logbook run -- <command>")
args = argparse.Namespace(
logbook_action="run",
page=opts.page,
title=opts.title,
no_artifacts=opts.no_artifacts,
command=command,
)
_handle_logbook(args)
return True
def _maybe_rewrite_logbook_read_argv() -> None:
argv = sys.argv
for i in range(1, len(argv) - 1):
if argv[i] == "logbook" and argv[i + 1] == "read":
j = i + 2
if (
j < len(argv)
and not argv[j].startswith("-")
and argv[j] not in ("pages", "page", "cell")
):
argv[j : j + 1] = ["--path", argv[j]]
return
def _trackio_space_namespaces(api, token: str | None, author: str | None) -> list[str]:
if author:
return [author]
if not token:
error_exit(
"Log in with `huggingface-cli login`, pass `--hf-token`, or provide `--author`."
)
try:
whoami = api.whoami(token=token, cache=True)
except Exception as e:
error_exit(f"Failed to read Hugging Face account information: {e}")
namespaces = [whoami["name"]]
for org in whoami.get("orgs", []):
org_name = org.get("name") if isinstance(org, dict) else org
if org_name:
namespaces.append(org_name)
return list(dict.fromkeys(namespaces))
def _handle_list_spaces(args):
if _get_space(args):
error_exit("The 'list spaces' command does not support --space.")
if args.limit is not None and args.limit < 0:
error_exit("--limit must be zero or greater.")
token = args.hf_token or huggingface_hub.utils.get_token()
api = huggingface_hub.HfApi(token=token)
namespaces = _trackio_space_namespaces(api, token, args.author)
spaces_by_id = {}
try:
for namespace in namespaces:
spaces = api.list_spaces(
author=namespace,
filter="trackio",
full=True,
token=token,
)
for space in spaces:
space_id = getattr(space, "id", None)
if space_id:
spaces_by_id[space_id] = _serialize_space(space)
except Exception as e:
error_exit(f"Failed to list Trackio Spaces: {e}")
spaces = sorted(
spaces_by_id.values(),
key=lambda space: space.get("last_modified") or "",
reverse=True,
)
if args.limit is not None:
spaces = spaces[: args.limit]
if args.json:
print(format_json({"spaces": spaces}))
else:
print(format_spaces(spaces))
def main():
if _maybe_handle_logbook_run_argv():
return
_maybe_rewrite_logbook_read_argv()
parser = argparse.ArgumentParser(description="Trackio CLI")
parser.add_argument(
"--version",
action="version",
version=f"trackio {trackio.__version__}",
)
parser.add_argument(
"--space",
required=False,
help="HF Space ID (e.g. 'user/space') or Space URL to query remotely.",
)
parser.add_argument(
"--hf-token",
required=False,
help="HF token for accessing private Spaces.",
)
subparsers = parser.add_subparsers(dest="command")
ui_parser = subparsers.add_parser(
"show", help="Show the Trackio dashboard UI for a project"
)
ui_parser.add_argument(
"--project", required=False, help="Project name to show in the dashboard"
)
ui_parser.add_argument(
"--theme",
required=False,
default="default",
help="A Gradio Theme to use for the dashboard instead of the default, can be a built-in theme (e.g. 'soft', 'citrus'), or a theme from the Hub (e.g. 'gstaff/xkcd').",
)
ui_parser.add_argument(
"--mcp-server",
action="store_true",
help="Enable MCP server functionality. The Trackio dashboard will be set up as an MCP server and certain functions will be exposed as MCP tools.",
)
ui_parser.add_argument(
"--footer",
action="store_true",
default=True,
help="Show the Gradio footer. Use --no-footer to hide it.",
)
ui_parser.add_argument(
"--no-footer",
dest="footer",
action="store_false",
help="Hide the Gradio footer.",
)
ui_parser.add_argument(
"--color-palette",
required=False,
help="Comma-separated list of hex color codes for plot lines (e.g. '#FF0000,#00FF00,#0000FF'). If not provided, the TRACKIO_COLOR_PALETTE environment variable will be used, or the default palette if not set.",
)
ui_parser.add_argument(
"--host",
required=False,
help="Host to bind the server to (e.g. '0.0.0.0' for remote access). If not provided, defaults to '127.0.0.1' (localhost only).",
)
ui_parser.add_argument(
"--frontend",
required=False,
help="Custom frontend directory to serve. Must contain index.html.",
)
subparsers.add_parser(
"status",
help="Show the status of all local Trackio projects, including sync status.",
)
sync_parser = subparsers.add_parser(
"sync",
help="Sync a local project's database to a Hugging Face Space. If the Space does not exist, it will be created.",
)
sync_parser.add_argument(
"--project",
required=False,
help="The name of the local project.",
)
sync_parser.add_argument(
"--space-id",
required=False,
help="The Hugging Face Space ID where the project will be synced (e.g. username/space_id). If not provided, uses the previously-configured Space.",
)
sync_parser.add_argument(
"--all",
action="store_true",
dest="sync_all",
help="Sync all projects that have unsynced data to their configured Spaces.",
)
sync_parser.add_argument(
"--private",
action="store_true",
help="Make the Hugging Face Space private if creating a new Space. By default, the repo will be public unless the organization's default is private. This value is ignored if the repo already exists.",
)
sync_parser.add_argument(
"--force",
action="store_true",
help="Overwrite the existing database without prompting for confirmation.",
)
sync_parser.add_argument(
"--sdk",
choices=["gradio", "static"],
default="gradio",
help="The type of Space to deploy. 'gradio' (default) deploys a live Gradio server. 'static' deploys a static Space that reads from an HF Bucket.",
)
sync_parser.add_argument(
"--frontend",
required=False,
help="Custom frontend directory to deploy. Must contain index.html.",
)
freeze_parser = subparsers.add_parser(
"freeze",
help="Create a one-time static Space snapshot from a project's data.",
)
freeze_parser.add_argument(
"--space-id",
required=True,
help="The source Gradio Space ID (e.g. username/space_id).",
)
freeze_parser.add_argument(
"--project",
required=True,
help="The name of the project to freeze into a static snapshot.",
)
freeze_parser.add_argument(
"--new-space-id",
required=False,
help="The Space ID for the new static Space. Defaults to {space_id}_static.",
)
freeze_parser.add_argument(
"--private",
action="store_true",
help="Make the new static Space private.",
)
freeze_parser.add_argument(
"--frontend",
required=False,
help="Custom frontend directory to deploy to the frozen static Space.",
)
config_parser = subparsers.add_parser(
"config",
help="Manage persistent Trackio configuration.",
)
config_subparsers = config_parser.add_subparsers(
dest="config_command",
required=True,
)
config_subparsers.add_parser("get", help="Show current Trackio config.")
config_set_parser = config_subparsers.add_parser(
"set",
help="Set a persistent Trackio config value.",
)
config_set_parser.add_argument(
"key",
choices=["frontend"],
help="Config key to set.",
)
config_set_parser.add_argument(
"frontend",
help="Frontend directory to persist.",
)
config_unset_parser = config_subparsers.add_parser(
"unset",
help="Unset a persistent Trackio config value.",
)
config_unset_parser.add_argument(
"key",
choices=["frontend"],
help="Config key to unset.",
)
list_parser = subparsers.add_parser(
"list",
help="List projects, runs, or metrics",
)
list_subparsers = list_parser.add_subparsers(dest="list_type", required=True)
list_projects_parser = list_subparsers.add_parser(
"projects",
help="List all projects",
)
list_projects_parser.add_argument(
"--json",
action="store_true",
help="Output in JSON format",
)
list_spaces_parser = list_subparsers.add_parser(
"spaces",
help="List Trackio Spaces for your Hugging Face account and organizations",
)
list_spaces_parser.add_argument(
"--author",
required=False,
help="Only list Trackio Spaces under this user or organization namespace.",
)
list_spaces_parser.add_argument(
"--limit",
type=int,
required=False,
help="Maximum number of Spaces to return.",
)
list_spaces_parser.add_argument(
"--json",
action="store_true",
help="Output in JSON format",
)
list_runs_parser = list_subparsers.add_parser(
"runs",
help="List runs for a project",
)
list_runs_parser.add_argument(
"--project",
required=True,
help="Project name",
)
list_runs_parser.add_argument(
"--json",
action="store_true",
help="Output in JSON format",
)
list_metrics_parser = list_subparsers.add_parser(
"metrics",
help="List metrics for a run",
)
list_metrics_parser.add_argument(
"--project",
required=True,
help="Project name",
)
list_metrics_parser.add_argument(
"--run",
required=True,
help="Run name",
)
list_metrics_parser.add_argument(
"--json",
action="store_true",
help="Output in JSON format",
)
list_system_metrics_parser = list_subparsers.add_parser(
"system-metrics",
help="List system metrics for a run",
)
list_system_metrics_parser.add_argument(
"--project",
required=True,
help="Project name",
)
list_system_metrics_parser.add_argument(
"--run",
required=True,
help="Run name",
)
list_system_metrics_parser.add_argument(
"--json",
action="store_true",
help="Output in JSON format",
)
list_alerts_parser = list_subparsers.add_parser(
"alerts",
help="List alerts for a project or run",
)
list_alerts_parser.add_argument(
"--project",
required=True,
help="Project name",
)
list_alerts_parser.add_argument(
"--run",
required=False,
help="Run name (optional)",
)
list_alerts_parser.add_argument(
"--level",
required=False,
help="Filter by alert level (info, warn, error)",
)
list_alerts_parser.add_argument(
"--json",
action="store_true",
help="Output in JSON format",
)
list_alerts_parser.add_argument(
"--since",
required=False,
help="Only show alerts after this ISO 8601 timestamp",
)
list_reports_parser = list_subparsers.add_parser(
"reports",
help="List markdown reports for a project or run",
)
list_reports_parser.add_argument(
"--project",
required=True,
help="Project name",
)
list_reports_parser.add_argument(
"--run",
required=False,
help="Run name (optional)",
)
list_reports_parser.add_argument(
"--json",
action="store_true",
help="Output in JSON format",
)
list_artifacts_parser = list_subparsers.add_parser(
"artifacts",
help="List artifacts for a project",
)
list_artifacts_parser.add_argument(
"--project",
required=True,
help="Project name",
)
list_artifacts_parser.add_argument(
"--json",
action="store_true",
help="Output in JSON format",
)
get_parser = subparsers.add_parser(
"get",
help="Get project, run, or metric information",
)
get_subparsers = get_parser.add_subparsers(dest="get_type", required=True)
get_project_parser = get_subparsers.add_parser(
"project",
help="Get project summary",
)
get_project_parser.add_argument(
"--project",
required=True,
help="Project name",
)
get_project_parser.add_argument(
"--json",
action="store_true",
help="Output in JSON format",
)
get_run_parser = get_subparsers.add_parser(
"run",
help="Get run summary",
)
get_run_parser.add_argument(
"--project",
required=True,
help="Project name",
)
get_run_parser.add_argument(
"--run",
required=True,
help="Run name",
)
get_run_parser.add_argument(
"--json",
action="store_true",
help="Output in JSON format",
)
get_artifact_parser = get_subparsers.add_parser(
"artifact",
help="Get an artifact version (manifest, aliases, metadata)",
)
get_artifact_parser.add_argument(
"--project",
required=True,
help="Project name",
)
get_artifact_parser.add_argument(
"--name",
required=True,
help="Artifact name",
)
get_artifact_parser.add_argument(
"--version",
required=False,
help="Version or alias to resolve (e.g. 'v2' or 'best'). Defaults to latest.",
)
get_artifact_parser.add_argument(
"--json",
action="store_true",
help="Output in JSON format",
)
get_metric_parser = get_subparsers.add_parser(
"metric",
help="Get metric values for a run",
)
get_metric_parser.add_argument(
"--project",
required=True,
help="Project name",
)
get_metric_parser.add_argument(
"--run",
required=True,
help="Run name",
)
get_metric_parser.add_argument(
"--metric",
required=True,
help="Metric name",
)
get_metric_parser.add_argument(
"--step",
type=int,
required=False,
help="Get metric at exactly this step",
)
get_metric_parser.add_argument(
"--around",
type=int,
required=False,
help="Get metrics around this step (use with --window)",
)
get_metric_parser.add_argument(
"--at-time",
required=False,
help="Get metrics around this ISO 8601 timestamp (use with --window)",
)
get_metric_parser.add_argument(
"--window",
type=int,
required=False,
default=10,
help="Window size: ±steps for --around, ±seconds for --at-time (default: 10)",
)
get_metric_parser.add_argument(
"--json",
action="store_true",
help="Output in JSON format",
)
get_snapshot_parser = get_subparsers.add_parser(
"snapshot",
help="Get all metrics at/around a step or timestamp",
)
get_snapshot_parser.add_argument(
"--project",
required=True,
help="Project name",
)
get_snapshot_parser.add_argument(
"--run",
required=True,
help="Run name",
)
get_snapshot_parser.add_argument(
"--step",
type=int,
required=False,
help="Get all metrics at exactly this step",
)
get_snapshot_parser.add_argument(
"--around",
type=int,
required=False,
help="Get all metrics around this step (use with --window)",
)
get_snapshot_parser.add_argument(
"--at-time",
required=False,
help="Get all metrics around this ISO 8601 timestamp (use with --window)",
)
get_snapshot_parser.add_argument(
"--window",
type=int,
required=False,
default=10,
help="Window size: ±steps for --around, ±seconds for --at-time (default: 10)",
)
get_snapshot_parser.add_argument(
"--json",
action="store_true",
help="Output in JSON format",
)
get_system_metric_parser = get_subparsers.add_parser(
"system-metric",
help="Get system metric values for a run",
)
get_system_metric_parser.add_argument(
"--project",
required=True,
help="Project name",
)
get_system_metric_parser.add_argument(
"--run",
required=True,
help="Run name",
)
get_system_metric_parser.add_argument(
"--metric",
required=False,
help="System metric name (optional, if not provided returns all system metrics)",
)
get_system_metric_parser.add_argument(
"--json",
action="store_true",
help="Output in JSON format",
)
get_alerts_parser = get_subparsers.add_parser(
"alerts",
help="Get alerts for a project or run",
)
get_alerts_parser.add_argument(
"--project",
required=True,
help="Project name",
)
get_alerts_parser.add_argument(
"--run",
required=False,
help="Run name (optional)",
)
get_alerts_parser.add_argument(
"--level",
required=False,
help="Filter by alert level (info, warn, error)",
)
get_alerts_parser.add_argument(
"--json",
action="store_true",
help="Output in JSON format",
)
get_alerts_parser.add_argument(
"--since",
required=False,
help="Only show alerts after this ISO 8601 timestamp",
)
get_report_parser = get_subparsers.add_parser(
"report",
help="Get markdown report entries for a run",
)
get_report_parser.add_argument(
"--project",
required=True,
help="Project name",
)
get_report_parser.add_argument(
"--run",
required=True,
help="Run name",
)
get_report_parser.add_argument(
"--report",
required=True,
help="Report metric name",
)
get_report_parser.add_argument(
"--json",
action="store_true",
help="Output in JSON format",
)
query_parser = subparsers.add_parser(
"query",
help="Run a read-only SQL query against a project database",
)
query_subparsers = query_parser.add_subparsers(dest="query_type", required=True)
query_project_parser = query_subparsers.add_parser(
"project",
help="Run a read-only SQL query against a project's SQLite database",
)
query_project_parser.add_argument(
"--project",
required=True,
help="Project name",
)
query_project_parser.add_argument(
"--sql",
required=True,
help="Read-only SQL query to execute",
)
query_project_parser.add_argument(
"--json",
action="store_true",
help="Output in JSON format",
)
skills_parser = subparsers.add_parser(
"skills",
help="Manage Trackio skills for AI coding assistants",
)
skills_subparsers = skills_parser.add_subparsers(
dest="skills_action", required=True
)
skills_add_parser = skills_subparsers.add_parser(
"add",
help=(
"Install the Trackio skill to the central .agents/skills location; "
"pass agent flags to also symlink it for specific assistants"
),
)
skills_add_parser.add_argument(
"--cursor",
action="store_true",
help="Install for Cursor",
)
skills_add_parser.add_argument(
"--claude",
action="store_true",
help="Install for Claude Code",
)
skills_add_parser.add_argument(
"--codex",
action="store_true",
help="Install for Codex",
)
skills_add_parser.add_argument(
"--opencode",
action="store_true",
help="Install for OpenCode",
)
skills_add_parser.add_argument(
"--pi",
action="store_true",
help="Install for pi",
)
skills_add_parser.add_argument(
"--global",
dest="global_",
action="store_true",
help="Install globally (user-level) instead of in the current project directory",
)
skills_add_parser.add_argument(
"--dest",
type=str,
required=False,
help="Install into a custom destination (path to skills directory)",
)
skills_add_parser.add_argument(
"--force",
action="store_true",
help="Overwrite existing skill if it already exists",
)
skills_add_parser.add_argument(
"--no-command",
dest="no_command",
action="store_true",
help="Do not install the /logbook slash command alongside the skill",
)
skills_add_parser.add_argument(
"--no-hook",
dest="no_hook",
action="store_true",
help="Do not install the Claude Code hook that mirrors todos into the logbook",
)
logbook_parser = subparsers.add_parser(
"logbook",
help="Create and publish a shareable experiment logbook",
)
logbook_sub = logbook_parser.add_subparsers(
dest="logbook_action",
required=True,
metavar="{open,cell,run,page,read,serve,publish,sync}",
)
lb_open = logbook_sub.add_parser(
"open", help="Start or attach to the logbook in this directory"
)
lb_open.add_argument(
"space_id", nargs="?", help="Optional HF Space id to publish to"
)
lb_open.add_argument("--title", help="Logbook title (only when creating)")
lb_open.add_argument(
"--no-serve",
action="store_true",
help="Open or attach without launching the local logbook preview",
)
lb_open.add_argument("--port", type=int, default=7861)
lb_open.add_argument("--no-browser", action="store_true")
lb_cell = logbook_sub.add_parser(
"cell", help="Append a typed notebook-style cell to a logbook page"
)
lb_cell_sub = lb_cell.add_subparsers(dest="cell_type", required=True)
lb_cell_md = lb_cell_sub.add_parser("markdown", help="Append a markdown cell")
lb_cell_md.add_argument("body", help="Markdown body")
lb_cell_md.add_argument("--title", help="Cell title")
lb_cell_md.add_argument("--page", help="Page title or slug")
lb_cell_art = lb_cell_sub.add_parser(
"artifact", help="Append an artifact cell referencing a Trackio artifact"
)
lb_cell_art.add_argument("name", help="Artifact reference project/name:vN")
lb_cell_art.add_argument("--title", help="Cell title")
lb_cell_art.add_argument("--page", help="Page title or slug")
lb_cell_art.add_argument(
"--type",
dest="artifact_type",
help='Artifact type, e.g. "dataset" or "model"',
)
lb_cell_code = lb_cell_sub.add_parser("code", help="Append a code cell")
lb_cell_code.add_argument("--title", help="Cell title")
lb_cell_code.add_argument("--page", help="Page title or slug")
lb_cell_code.add_argument(
"--code", action="append", default=[], help="Path to code/config to include"
)
lb_cell_code.add_argument("--code-text", help="Inline code to include")
lb_cell_code.add_argument("--language", help="Language for --code-text")
lb_cell_code.add_argument("--output", required=True, help="Output text")
lb_cell_figure = lb_cell_sub.add_parser("figure", help="Append a figure cell")
lb_cell_figure.add_argument("--title", help="Cell title")
lb_cell_figure.add_argument("--page", help="Page title or slug")
lb_cell_figure.add_argument("--html", help="Path to HTML, or inline HTML text")
lb_cell_figure.add_argument("--html-text", help="Inline HTML text")
lb_cell_figure.add_argument("--raw", help="Path/URL/text for raw data")
lb_cell_figure.add_argument("--raw-text", help="Inline raw data")
lb_cell_dash = lb_cell_sub.add_parser(
"dashboard", help="Embed a Trackio dashboard for a project"
)
lb_cell_dash.add_argument("project", help="Trackio project name")
lb_cell_dash.add_argument(
"--space",
dest="space_id",
help="HF Space id (owner/name) hosting the dashboard",
)
lb_cell_dash.add_argument("--title", help="Cell title")
lb_cell_dash.add_argument("--page", help="Page title or slug")
lb_run = logbook_sub.add_parser(
"run",
help="Run a command; log the command, its scripts, and output to a page",
)
lb_run.add_argument("--page", help="Page title or slug")
lb_run.add_argument("--title", help="Cell title")
lb_run.add_argument(
"--no-artifacts",
action="store_true",
help="Do not record output model/data files as artifact cells",
)
lb_run.add_argument("command", nargs="*")
lb_page = logbook_sub.add_parser(
"page", help="Create or select a page and make it the default target"
)
lb_page.add_argument("title", help="Page title")
lb_read = logbook_sub.add_parser(
"read", help="Read logbook pages/cells in an agent-friendly form"
)
lb_read.add_argument(
"--path",
help=(
"Logbook to read: local path, HF Space id, or URL "
"(can also be passed positionally: trackio logbook read <source>)"
),
)
lb_read.add_argument("--json", action="store_true", help="Output JSON")
lb_read.add_argument(
"--head",
type=int,
default=None,
help="Lines of code shown per code cell (default 3; 0 hides code)",
)
lb_read.add_argument(
"--tail",
type=int,
default=None,
help="Lines of output shown per code cell (default 3; 0 hides output)",
)
lb_read.add_argument(
"--raw-limit",
type=int,
default=None,
help="Inline figure raw data up to this many chars (default 500; 0 disables)",
)
lb_read_sub = lb_read.add_subparsers(dest="read_target")
lb_read_pages = lb_read_sub.add_parser("pages", help="List logbook pages")
lb_read_pages.add_argument("--json", action="store_true", help="Output JSON")
lb_read_page = lb_read_sub.add_parser("page", help="Read a page for agents")
lb_read_page.add_argument("page", nargs="?", help="Page title or slug")
lb_read_page.add_argument("--json", action="store_true", help="Output JSON")
lb_read_page.add_argument(
"--head", type=int, default=argparse.SUPPRESS, help="Code lines per code cell"
)
lb_read_page.add_argument(
"--tail", type=int, default=argparse.SUPPRESS, help="Output lines per code cell"
)
lb_read_page.add_argument(
"--raw-limit",
type=int,
default=argparse.SUPPRESS,
help="Inline figure raw data up to this many chars",
)
lb_read_cell = lb_read_sub.add_parser("cell", help="Read one cell by id")
lb_read_cell.add_argument("cell_id", help="Cell id")
lb_read_cell.add_argument("--json", action="store_true", help="Output JSON")
lb_read_cell.add_argument("--full", action="store_true", help="Include full body")
lb_read_cell.add_argument(
"--raw", action="store_true", help="Include figure raw data"
)
lb_read_cell.add_argument("--html", action="store_true", help="Include figure HTML")
lb_serve = logbook_sub.add_parser("serve", help="Preview the logbook locally")
lb_serve.add_argument("path", nargs="?", help="Logbook workspace/path to serve")
lb_serve.add_argument("--port", type=int, default=7861)
lb_serve.add_argument("--no-browser", action="store_true")
lb_pub = logbook_sub.add_parser(
"publish", help="Publish to a static HF Space (first publish enables auto-sync)"
)
lb_pub.add_argument("space_id", nargs="?", help="HF Space id (username/space)")
lb_pub.add_argument(
"--private",
action="store_true",
help="Make the logbook and any promoted dashboards/buckets private",
)
logbook_sub.add_parser(
"sync", help="Push local edits to the Space now (after the first publish)"
)
logbook_sub.add_parser("_sync")
logbook_sub.add_parser("sync-todos")
args, unknown_args = parser.parse_known_args()
if unknown_args:
trailing_global_parser = argparse.ArgumentParser(add_help=False)
trailing_global_parser.add_argument("--space", required=False)
trailing_global_parser.add_argument("--hf-token", required=False)
trailing_globals, remaining_unknown = trailing_global_parser.parse_known_args(
unknown_args
)
if remaining_unknown:
parser.error(f"unrecognized arguments: {' '.join(remaining_unknown)}")
if trailing_globals.space is not None:
args.space = trailing_globals.space
if trailing_globals.hf_token is not None:
args.hf_token = trailing_globals.hf_token
if args.command in ("show", "status", "sync", "freeze", "skills") and _get_space(
args
):
error_exit(
f"The '{args.command}' command does not support --space (remote mode)."
)
if args.command == "show":
color_palette = None
if args.color_palette:
color_palette = [color.strip() for color in args.color_palette.split(",")]
show(
project=args.project,
theme=args.theme,
mcp_server=args.mcp_server,
footer=args.footer,
color_palette=color_palette,
host=args.host,
frontend_dir=args.frontend,
)
elif args.command == "status":
_handle_status()
elif args.command == "sync":
_handle_sync(args)
elif args.command == "freeze":
freeze(
space_id=args.space_id,
project=args.project,
new_space_id=args.new_space_id,
private=args.private,
frontend_dir=args.frontend,
)
elif args.command == "config":
_handle_config(args)
elif args.command == "list":
if args.list_type == "spaces":
_handle_list_spaces(args)
return
remote = _get_remote(args)
if args.list_type == "projects":
if remote:
projects = remote.predict(api_name="/get_all_projects")
else:
projects = SQLiteStorage.get_projects()
if args.json:
print(format_json({"projects": projects}))
else:
print(format_list(projects, "Projects"))
elif args.list_type == "runs":
if remote:
run_records = remote.predict(
args.project, api_name="/get_runs_for_project"
)
runs = [r["name"] if isinstance(r, dict) else r for r in run_records]
else:
db_path = SQLiteStorage.get_project_db_path(args.project)
if not db_path.exists():
error_exit(f"Project '{args.project}' not found.")
runs = SQLiteStorage.get_runs(args.project)
if args.json:
print(format_json({"project": args.project, "runs": runs}))
else:
print(format_list(runs, f"Runs in '{args.project}'"))
elif args.list_type == "metrics":
if remote:
metrics = remote.predict(
args.project, args.run, api_name="/get_metrics_for_run"
)
else:
db_path = SQLiteStorage.get_project_db_path(args.project)
if not db_path.exists():
error_exit(f"Project '{args.project}' not found.")
runs = SQLiteStorage.get_runs(args.project)
if args.run not in runs:
error_exit(
f"Run '{args.run}' not found in project '{args.project}'."
)
metrics = SQLiteStorage.get_all_metrics_for_run(args.project, args.run)
if args.json:
print(
format_json(
{"project": args.project, "run": args.run, "metrics": metrics}
)
)
else:
print(
format_list(
metrics, f"Metrics for '{args.run}' in '{args.project}'"
)
)
elif args.list_type == "system-metrics":
if remote:
system_metrics = remote.predict(
args.project, args.run, api_name="/get_system_metrics_for_run"
)
else:
db_path = SQLiteStorage.get_project_db_path(args.project)
if not db_path.exists():
error_exit(f"Project '{args.project}' not found.")
runs = SQLiteStorage.get_runs(args.project)
if args.run not in runs:
error_exit(
f"Run '{args.run}' not found in project '{args.project}'."
)
system_metrics = SQLiteStorage.get_all_system_metrics_for_run(
args.project, args.run
)
if args.json:
print(
format_json(
{
"project": args.project,
"run": args.run,
"system_metrics": system_metrics,
}
)
)
else:
print(format_system_metric_names(system_metrics))
elif args.list_type == "alerts":
if remote:
alerts = remote.predict(
args.project,
args.run,
args.level,
args.since,
api_name="/get_alerts",
)
else:
db_path = SQLiteStorage.get_project_db_path(args.project)
if not db_path.exists():
error_exit(f"Project '{args.project}' not found.")
alerts = SQLiteStorage.get_alerts(
args.project,
run_name=args.run,
level=args.level,
since=args.since,
)
if args.json:
print(
format_json(
{
"project": args.project,
"run": args.run,
"level": args.level,
"since": args.since,
"alerts": alerts,
}
)
)
else:
print(format_alerts(alerts))
elif args.list_type == "reports":
if remote:
run_records = remote.predict(
args.project, api_name="/get_runs_for_project"
)
runs = [r["name"] if isinstance(r, dict) else r for r in run_records]
else:
db_path = SQLiteStorage.get_project_db_path(args.project)
if not db_path.exists():
error_exit(f"Project '{args.project}' not found.")
runs = SQLiteStorage.get_runs(args.project)
if args.run and args.run not in runs:
error_exit(f"Run '{args.run}' not found in project '{args.project}'.")
target_runs = [args.run] if args.run else runs
all_reports = []
for run_name in target_runs:
if remote:
logs = remote.predict(args.project, run_name, api_name="/get_logs")
else:
logs = SQLiteStorage.get_logs(args.project, run_name)
all_reports.extend(_extract_reports(run_name, logs))
if args.json:
print(
format_json(
{
"project": args.project,
"run": args.run,
"reports": all_reports,
}
)
)
else:
report_lines = [
f"{entry['run']} | {entry['report']} | step={entry['step']} | {entry['timestamp']}"
for entry in all_reports
]
if args.run:
print(
format_list(
report_lines,
f"Reports for '{args.run}' in '{args.project}'",
)
)
else:
print(format_list(report_lines, f"Reports in '{args.project}'"))
elif args.list_type == "artifacts":
if remote:
artifacts = remote.predict(args.project, api_name="/get_artifacts")
else:
db_path = SQLiteStorage.get_project_db_path(args.project)
if not db_path.exists():
error_exit(f"Project '{args.project}' not found.")
artifacts = SQLiteStorage.get_artifacts(args.project)
if args.json:
print(format_json({"project": args.project, "artifacts": artifacts}))
else:
print(format_artifacts(artifacts, args.project))
elif args.command == "get":
remote = _get_remote(args)
if args.get_type == "artifact":
if remote:
record = remote.predict(
args.project,
args.name,
args.version,
api_name="/get_artifact_manifest",
)
else:
db_path = SQLiteStorage.get_project_db_path(args.project)
if not db_path.exists():
error_exit(f"Project '{args.project}' not found.")
record = SQLiteStorage.get_artifact_manifest(
args.project, args.name, args.version
)
if record is None:
spec = f":{args.version}" if args.version else ""
error_exit(
f"Artifact '{args.name}{spec}' not found in project "
f"'{args.project}'."
)
if args.json:
print(format_json(record))
else:
print(format_artifact(record))
elif args.get_type == "project":
if remote:
summary = remote.predict(args.project, api_name="/get_project_summary")
else:
db_path = SQLiteStorage.get_project_db_path(args.project)
if not db_path.exists():
error_exit(f"Project '{args.project}' not found.")
summary = get_project_summary(args.project)
if args.json:
print(format_json(summary))
else:
print(format_project_summary(summary))
elif args.get_type == "run":
if remote:
summary = remote.predict(
args.project, args.run, api_name="/get_run_summary"
)
else:
db_path = SQLiteStorage.get_project_db_path(args.project)
if not db_path.exists():
error_exit(f"Project '{args.project}' not found.")
runs = SQLiteStorage.get_runs(args.project)
if args.run not in runs:
error_exit(
f"Run '{args.run}' not found in project '{args.project}'."
)
summary = get_run_summary(args.project, args.run)
if args.json:
print(format_json(summary))
else:
print(format_run_summary(summary))
elif args.get_type == "metric":
at_time = getattr(args, "at_time", None)
if remote:
values = remote.predict(
args.project,
args.run,
args.metric,
args.step,
args.around,
at_time,
args.window,
api_name="/get_metric_values",
)
else:
db_path = SQLiteStorage.get_project_db_path(args.project)
if not db_path.exists():
error_exit(f"Project '{args.project}' not found.")
runs = SQLiteStorage.get_runs(args.project)
if args.run not in runs:
error_exit(
f"Run '{args.run}' not found in project '{args.project}'."
)
metrics = SQLiteStorage.get_all_metrics_for_run(args.project, args.run)
if args.metric not in metrics:
error_exit(
f"Metric '{args.metric}' not found in run '{args.run}' of project '{args.project}'."
)
values = SQLiteStorage.get_metric_values(
args.project,
args.run,
args.metric,
step=args.step,
around_step=args.around,
at_time=at_time,
window=args.window,
)
if args.json:
print(
format_json(
{
"project": args.project,
"run": args.run,
"metric": args.metric,
"values": values,
}
)
)
else:
print(format_metric_values(values))
elif args.get_type == "snapshot":
if not args.step and not args.around and not getattr(args, "at_time", None):
error_exit(
"Provide --step, --around (with --window), or --at-time (with --window)."
)
at_time = getattr(args, "at_time", None)
if remote:
snapshot = remote.predict(
args.project,
args.run,
args.step,
args.around,
at_time,
args.window,
api_name="/get_snapshot",
)
else:
db_path = SQLiteStorage.get_project_db_path(args.project)
if not db_path.exists():
error_exit(f"Project '{args.project}' not found.")
runs = SQLiteStorage.get_runs(args.project)
if args.run not in runs:
error_exit(
f"Run '{args.run}' not found in project '{args.project}'."
)
snapshot = SQLiteStorage.get_snapshot(
args.project,
args.run,
step=args.step,
around_step=args.around,
at_time=at_time,
window=args.window,
)
if args.json:
result = {
"project": args.project,
"run": args.run,
"metrics": snapshot,
}
if args.step is not None:
result["step"] = args.step
if args.around is not None:
result["around"] = args.around
result["window"] = args.window
if at_time is not None:
result["at_time"] = at_time
result["window"] = args.window
print(format_json(result))
else:
print(format_snapshot(snapshot))
elif args.get_type == "system-metric":
if remote:
system_metrics = remote.predict(
args.project, args.run, api_name="/get_system_logs"
)
if args.metric:
all_system_metric_names = remote.predict(
args.project,
args.run,
api_name="/get_system_metrics_for_run",
)
if args.metric not in all_system_metric_names:
error_exit(
f"System metric '{args.metric}' not found in run '{args.run}' of project '{args.project}'."
)
filtered_metrics = [
{
k: v
for k, v in entry.items()
if k == "timestamp" or k == args.metric
}
for entry in system_metrics
if args.metric in entry
]
if args.json:
print(
format_json(
{
"project": args.project,
"run": args.run,
"metric": args.metric,
"values": filtered_metrics,
}
)
)
else:
print(format_system_metrics(filtered_metrics))
else:
if args.json:
print(
format_json(
{
"project": args.project,
"run": args.run,
"system_metrics": system_metrics,
}
)
)
else:
print(format_system_metrics(system_metrics))
else:
db_path = SQLiteStorage.get_project_db_path(args.project)
if not db_path.exists():
error_exit(f"Project '{args.project}' not found.")
runs = SQLiteStorage.get_runs(args.project)
if args.run not in runs:
error_exit(
f"Run '{args.run}' not found in project '{args.project}'."
)
if args.metric:
system_metrics = SQLiteStorage.get_system_logs(
args.project, args.run
)
all_system_metric_names = (
SQLiteStorage.get_all_system_metrics_for_run(
args.project, args.run
)
)
if args.metric not in all_system_metric_names:
error_exit(
f"System metric '{args.metric}' not found in run '{args.run}' of project '{args.project}'."
)
filtered_metrics = [
{
k: v
for k, v in entry.items()
if k == "timestamp" or k == args.metric
}
for entry in system_metrics
if args.metric in entry
]
if args.json:
print(
format_json(
{
"project": args.project,
"run": args.run,
"metric": args.metric,
"values": filtered_metrics,
}
)
)
else:
print(format_system_metrics(filtered_metrics))
else:
system_metrics = SQLiteStorage.get_system_logs(
args.project, args.run
)
if args.json:
print(
format_json(
{
"project": args.project,
"run": args.run,
"system_metrics": system_metrics,
}
)
)
else:
print(format_system_metrics(system_metrics))
elif args.get_type == "alerts":
if remote:
alerts = remote.predict(
args.project,
args.run,
args.level,
args.since,
api_name="/get_alerts",
)
else:
db_path = SQLiteStorage.get_project_db_path(args.project)
if not db_path.exists():
error_exit(f"Project '{args.project}' not found.")
alerts = SQLiteStorage.get_alerts(
args.project,
run_name=args.run,
level=args.level,
since=args.since,
)
if args.json:
print(
format_json(
{
"project": args.project,
"run": args.run,
"level": args.level,
"since": args.since,
"alerts": alerts,
}
)
)
else:
print(format_alerts(alerts))
elif args.get_type == "report":
if remote:
logs = remote.predict(args.project, args.run, api_name="/get_logs")
else:
db_path = SQLiteStorage.get_project_db_path(args.project)
if not db_path.exists():
error_exit(f"Project '{args.project}' not found.")
runs = SQLiteStorage.get_runs(args.project)
if args.run not in runs:
error_exit(
f"Run '{args.run}' not found in project '{args.project}'."
)
logs = SQLiteStorage.get_logs(args.project, args.run)
reports = _extract_reports(args.run, logs, report_name=args.report)
if not reports:
error_exit(
f"Report '{args.report}' not found in run '{args.run}' of project '{args.project}'."
)
if args.json:
print(
format_json(
{
"project": args.project,
"run": args.run,
"report": args.report,
"values": reports,
}
)
)
else:
output = []
for idx, entry in enumerate(reports, start=1):
output.append(
f"Entry {idx} | step={entry['step']} | timestamp={entry['timestamp']}"
)
output.append(entry["content"])
if idx < len(reports):
output.append("-" * 80)
print("\n".join(output))
elif args.command == "query":
if args.query_type == "project":
_handle_query(args)
elif args.command == "skills":
if args.skills_action == "add":
_handle_skills_add(args)
elif args.command == "logbook":
_handle_logbook(args)
else:
parser.print_help()
def _sync_suffix(lb, proj):
if lb.is_autosync(proj):
space = lb.read_metadata(proj).get("space_id")
return f" · syncing to {space}…"
return ""
def _logbook_cell_target(lb, proj, args):
return lb.resolve_page(proj, getattr(args, "page", None))
def _print_logbook_pages(pages):
if not pages:
print("No pages.")
return
print("Pages:")
for page in pages:
count = page["cell_count"]
print(
f"- {page['slug']} · {page['title']} · "
f"{count} cell{'' if count == 1 else 's'}"
)
def _print_logbook_page_outline(page):
print(f"Page: {page['title']} ({page['slug']})")
if not page["cells"]:
print("No cells.")
return
for cell in page["cells"]:
created = (
f" · {cell['created_at'].replace('T', ' ')[:16]}"
if cell.get("created_at")
else ""
)
print(f"\n### {cell['title']} · {cell['type']} · {cell['id']}{created}")
if cell.get("preview"):
print(cell["preview"].rstrip())
print(
"\nFetch full payloads with: trackio logbook read cell <cell-id> "
"[--full|--raw|--html]"
)
def _print_logbook_cell(cell):
print(f"Cell: {cell['title']} ({cell['id']})")
print(f"Page: {cell['page_title']} ({cell['page']})")
print(f"Type: {cell['type']}")
if cell.get("created_at"):
print(f"Created: {cell['created_at']}")
content_keys = [key for key in ("body", "raw", "html") if key in cell]
if not content_keys:
if cell["type"] == "code":
print("\nBody omitted. Use --full to include code/output.")
elif cell["type"] == "figure":
print("\nFigure content omitted. Use --raw, --html, or --full.")
return
for key in content_keys:
label = key.upper()
print(f"\n--- {label} ---\n")
print(cell[key])
def _read_logbook_payload(path_or_text, inline_text):
if inline_text is not None:
return inline_text
if not path_or_text:
return ""
path = Path(path_or_text)
if path.is_file():
return path.read_text(encoding="utf-8")
return path_or_text
def _handle_logbook(args):
from trackio import logbook as lb
action = args.logbook_action
try:
if action == "open":
print("Warning: Trackio Logbook is an experimental feature.")
proj = lb.find_project_dir()
if proj is not None:
if args.space_id:
metadata = lb.read_metadata(proj)
metadata["space_id"] = args.space_id
lb.write_metadata(proj, metadata)
print(f"Attached to existing logbook at {lb.logbook_root(proj)}")
if args.title:
print(
"Note: --title was ignored because this logbook already "
"exists. To retitle it, edit the '# ...' heading in "
f"{lb.logbook_root(proj) / 'pages' / 'index.md'}."
)
elif args.space_id:
proj = lb.clone_logbook(args.space_id)
if proj is not None:
print(
f"Cloned logbook from {args.space_id} into "
f"{lb.logbook_root(proj)}"
)
if proj is None:
proj = lb.create_logbook(
args.title or os.path.basename(os.getcwd()), space_id=args.space_id
)
print(f"Opened logbook at {lb.logbook_root(proj)}")
if args.space_id:
print(f"Will publish to: {args.space_id}")
if not args.no_serve:
lb.start_preview(proj, port=args.port, open_browser=not args.no_browser)
elif action == "run":
proj = lb.require_project_dir()
command = list(args.command or [])
if command and command[0] == "--":
command = command[1:]
if not command:
error_exit("No command provided. Use: trackio logbook run -- <command>")
rc = lb.run_and_log(
proj,
command,
page=args.page,
title=args.title,
capture_artifacts=not getattr(args, "no_artifacts", False),
)
slug = lb.read_metadata(proj).get("last_page", "?")
print(f"Logged run to page '{slug}'.{_sync_suffix(lb, proj)}")
sys.exit(rc)
elif action == "cell":
proj = lb.require_project_dir()
slug = _logbook_cell_target(lb, proj, args)
if args.cell_type == "markdown":
lb.add_markdown_cell(proj, slug, args.body, title=args.title)
elif args.cell_type == "artifact":
lb.add_artifact_cell(
proj,
slug,
args.name,
title=args.title,
artifact_type=args.artifact_type,
)
elif args.cell_type == "code":
lb.add_code_cell(
proj,
slug,
args.output,
title=args.title,
code_paths=args.code,
code_text=args.code_text,
language=args.language,
)
elif args.cell_type == "figure":
html = _read_logbook_payload(args.html, args.html_text)
raw = _read_logbook_payload(args.raw, args.raw_text)
lb.add_figure_cell(
proj,
slug,
html=html,
raw=raw,
title=args.title,
)
elif args.cell_type == "dashboard":
lb.add_dashboard_cell(
proj,
slug,
args.project,
space_id=args.space_id,
title=args.title,
)
print(
f"Logged {args.cell_type} cell to page "
f"'{slug}'.{_sync_suffix(lb, proj)}"
)
lb.trigger_autosync(proj)
elif action == "page":
proj = lb.require_project_dir()
page_slug = lb.ensure_page(proj, args.title)
print(f"Selected page '{page_slug}' as default.{_sync_suffix(lb, proj)}")
lb.trigger_autosync(proj)
elif action == "read":
proj = lb.resolve_read_source(args.path)
preview_opts = {
"head": lb.DEFAULT_HEAD
if getattr(args, "head", None) is None
else args.head,
"tail": lb.DEFAULT_TAIL
if getattr(args, "tail", None) is None
else args.tail,
"raw_limit": lb.DEFAULT_RAW_LIMIT
if getattr(args, "raw_limit", None) is None
else args.raw_limit,
}
if args.read_target is None:
if args.json:
print(format_json(lb.read_logbook_data(proj, **preview_opts)))
else:
print(lb.read_logbook(proj, **preview_opts))
elif args.read_target == "pages":
pages = lb.list_pages(proj)
if args.json:
print(format_json({"pages": pages}))
else:
_print_logbook_pages(pages)
elif args.read_target == "page":
page = lb.read_page_outline(proj, args.page, **preview_opts)
if args.json:
print(format_json(page))
else:
_print_logbook_page_outline(page)
elif args.read_target == "cell":
cell = lb.read_cell(
proj,
args.cell_id,
include_full=args.full,
include_raw=args.raw,
include_html=args.html,
)
if args.json:
print(format_json(cell))
else:
_print_logbook_cell(cell)
elif action == "_sync":
lb.sync_worker()
elif action == "sync-todos":
lb.sync_todos_from_stdin()
elif action == "serve":
lb.serve(path=args.path, port=args.port, open_browser=not args.no_browser)
elif action == "publish":
url = lb.publish(space_id=args.space_id, private=args.private)
print(f"Published: {url}")
space_id = lb.read_metadata(lb.require_project_dir()).get("space_id")
if space_id:
subdomain = re.sub(r"[^a-z0-9]+", "-", space_id.lower()).strip("-")
print(f"Rendered at: https://{subdomain}.static.hf.space/")
elif action == "sync":
proj = lb.require_project_dir()
if not lb.is_autosync(proj):
error_exit("Publish first: trackio logbook publish <username/space>")
lb.trigger_autosync(proj)
print(f"Syncing to {lb.read_metadata(proj).get('space_id')}…")
except lb.LogbookError as e:
error_exit(str(e))
def _handle_skills_add(args):
import shutil
from pathlib import Path
CENTRAL_LOCAL = Path(".agents/skills")
CENTRAL_GLOBAL = Path("~/.agents/skills")
CLAUDE_LOCAL = Path(".claude/skills")
CLAUDE_GLOBAL = Path("~/.claude/skills")
SKILL_ID = "trackio"
GITHUB_RAW = "https://raw.githubusercontent.com/gradio-app/trackio/main"
SKILL_PREFIX = ".agents/skills/trackio"
SKILL_FILES = [
"SKILL.md",
"alerts.md",
"logging_metrics.md",
"retrieving_metrics.md",
"storage_schema.md",
"logbook.md",
]
COMMAND_PREFIX = ".agents/commands"
COMMAND_FILE = "logbook.md"
REPO_ROOT = Path(__file__).resolve().parent.parent
USE_LOCAL = (REPO_ROOT / SKILL_PREFIX / "SKILL.md").is_file()
if USE_LOCAL:
print(f"Using local Trackio source at {REPO_ROOT}")
def download(url: str) -> str:
from huggingface_hub.utils import get_session
try:
response = get_session().get(url)
response.raise_for_status()
except Exception as e:
error_exit(
f"Failed to download {url}\n{e}\n\n"
"Make sure you have internet access. The skill files are fetched from "
"the Trackio GitHub repository."
)
return response.text
def get_content(prefix: str, fname: str) -> str:
if USE_LOCAL:
local = REPO_ROOT / prefix / fname
if local.is_file():
return local.read_text(encoding="utf-8")
return download(f"{GITHUB_RAW}/{prefix}/{fname}")
def remove_existing(path: Path, force: bool):
if not (path.exists() or path.is_symlink()):
return
if not force:
error_exit(
f"Skill already exists at {path}.\nRe-run with --force to overwrite."
)
if path.is_dir() and not path.is_symlink():
shutil.rmtree(path)
else:
path.unlink()
def install_to(skills_dir: Path, force: bool) -> Path:
skills_dir = skills_dir.expanduser().resolve()
skills_dir.mkdir(parents=True, exist_ok=True)
dest = skills_dir / SKILL_ID
remove_existing(dest, force)
dest.mkdir()
for fname in SKILL_FILES:
(dest / fname).write_text(
get_content(SKILL_PREFIX, fname), encoding="utf-8"
)
return dest
def create_symlink(
agent_skills_dir: Path, central_skill_path: Path, force: bool
) -> Path:
agent_skills_dir = agent_skills_dir.expanduser().resolve()
agent_skills_dir.mkdir(parents=True, exist_ok=True)
link_path = agent_skills_dir / SKILL_ID
remove_existing(link_path, force)
link_path.symlink_to(os.path.relpath(central_skill_path, agent_skills_dir))
return link_path
def install_command(
command_dir: Path, force: bool, strip_frontmatter: bool = False
) -> Path:
import re
command_dir = command_dir.expanduser().resolve()
command_dir.mkdir(parents=True, exist_ok=True)
dest = command_dir / COMMAND_FILE
if dest.exists() and not force:
error_exit(
f"Command already exists at {dest}.\nRe-run with --force to overwrite."
)
content = get_content(COMMAND_PREFIX, COMMAND_FILE)
if strip_frontmatter:
content = re.sub(r"\A---\n.*?\n---\n+", "", content, flags=re.S)
dest.write_text(content, encoding="utf-8")
return dest
global_targets = {
"cursor": Path("~/.cursor/skills"),
"claude": CLAUDE_GLOBAL,
"codex": Path("~/.codex/skills"),
"opencode": Path("~/.opencode/skills"),
"pi": Path("~/.pi/agent/skills"),
}
local_targets = {
"cursor": Path(".cursor/skills"),
"claude": CLAUDE_LOCAL,
"codex": Path(".codex/skills"),
"opencode": Path(".opencode/skills"),
"pi": Path(".pi/skills"),
}
targets_dict = global_targets if args.global_ else local_targets
command_targets_global = {
"cursor": Path("~/.cursor/commands"),
"claude": Path("~/.claude/commands"),
"codex": Path("~/.codex/prompts"),
"opencode": Path("~/.config/opencode/commands"),
"pi": Path("~/.pi/agent/prompts"),
}
command_targets_local = {
"cursor": Path(".cursor/commands"),
"claude": Path(".claude/commands"),
"codex": Path("~/.codex/prompts"),
"opencode": Path(".opencode/commands"),
"pi": Path(".pi/prompts"),
}
command_dict = command_targets_global if args.global_ else command_targets_local
COMMAND_NOTES = {
"codex": " (Codex only reads ~/.codex/prompts; restart Codex to pick it up)"
}
STRIP_FRONTMATTER = {"pi"}
if args.dest:
if (
args.cursor
or args.claude
or args.codex
or args.opencode
or args.pi
or args.global_
):
error_exit("--dest cannot be combined with agent flags or --global.")
skill_dest = install_to(Path(args.dest), args.force)
print(f"Installed '{SKILL_ID}' to {skill_dest}")
return
selected = [
name
for name, flag in (
("cursor", args.cursor),
("claude", args.claude),
("codex", args.codex),
("opencode", args.opencode),
("pi", args.pi),
)
if flag
]
central_path = CENTRAL_GLOBAL if args.global_ else CENTRAL_LOCAL
central_skill_path = install_to(central_path, args.force)
print(f"Installed '{SKILL_ID}' to central location: {central_skill_path}")
for name in selected:
link_path = create_symlink(targets_dict[name], central_skill_path, args.force)
print(f"Created symlink: {link_path}")
if not args.no_command:
for name in selected:
if name in command_dict:
cmd_path = install_command(
command_dict[name],
args.force,
strip_frontmatter=name in STRIP_FRONTMATTER,
)
note = COMMAND_NOTES.get(name, "")
print(f"Installed '/logbook' command: {cmd_path}{note}")
else:
print(
f"Skipped '/logbook' command for {name} "
"(no slash-command directory convention)."
)
if args.claude and not args.no_hook:
hook_path = _install_claude_logbook_hook(args.global_)
if hook_path:
print(f"Installed logbook todo-sync hook: {hook_path}")
def _install_claude_logbook_hook(global_: bool):
import json
from pathlib import Path
settings = (
Path("~/.claude/settings.json") if global_ else Path(".claude/settings.json")
).expanduser()
settings.parent.mkdir(parents=True, exist_ok=True)
data = {}
if settings.exists():
try:
data = json.loads(settings.read_text(encoding="utf-8"))
except Exception:
print(f"Could not parse {settings}; skipping hook install.")
return None
command = "trackio logbook sync-todos"
hooks = data.setdefault("hooks", {})
post = hooks.setdefault("PostToolUse", [])
for entry in post:
for h in entry.get("hooks", []):
if h.get("command") == command:
return settings
post.append(
{
"matcher": "TodoWrite",
"hooks": [{"type": "command", "command": command}],
}
)
settings.write_text(json.dumps(data, indent=2), encoding="utf-8")
return settings
if __name__ == "__main__":
main()