Go devtools monorepo - thirteen CLI tools for shell optimization and personal workflows.
These tools represent patterns that graduated from ad-hoc shell scripts into typed, testable binaries. Each tool emits structured telemetry to a unified JSONL bus, enabling usage-driven decisions about what to build, optimize, or deprecate.
- mdq - query fields and tables across markdown files
- perfgate - statistical before/after performance gating
- shellprof - fish shell function profiler with call graphs
- hookval - validate Claude hook signal contract against schema
- effiscore - Anthropic API efficiency scoring via Datadog metrics
- linkari - Android share → tmux webhook bridge with AI-powered triage scoring
- wasend - send WhatsApp messages from the command line
- fetchpage - headless webpage fetcher via Playwright
- protonexport - export ProtonMail conversations to markdown
- workctl - fetch and export Atlassian & GitHub work data (weekly/quarterly summaries, career reports)
- ghwatch - stream GitHub repository activity to the terminal (push, PRs, workflow runs)
- ts-go - tree-sitter-based structural Go source analysis (signatures, types, body extraction, search, rewrite)
Last Updated: 2026-07-22
make install # go install → ~/go/binRequires Go 1.26+.
Query structured data out of markdown files matching a glob.
Heading extraction correctly ignores # comments inside fenced code blocks (``` and ~~~).
# Extract a field value across all epics
mdq query "docs/epics/*.md" --field Status
# Extract a field scoped to a specific table section
mdq query "docs/epics/*.md" --field Owner --table "Milestones"
# Render a table from a named section
mdq table epic.md "Milestones"
# Extract a section's full content
mdq extract epic.md "Background"
# List all headings across files
mdq list "docs/**/*.md" --headings
# Filter headings by level
mdq list "docs/**/*.md" --headings --level 2
# Group by parent directory (KB manifest)
mdq list "docs/*/*.md" --headings --level 1 --group-by dir
# Exclude noisy directories
mdq list "docs/*/*.md" --headings --group-by dir --exclude standups,temp,.output
# JSON output for scripting/agents
mdq list "docs/*/*.md" --headings --level 1 --group-by dir --format jsonOutput formats: text (default), json, table. JSON output enables direct consumption by AI agents and automation consumers without shell parsing. --group-by dir produces [{folder, count, titles}] in JSON mode.
Benchmark commands and enforce statistical performance thresholds.
# Benchmark a command (10 runs, 2 warmup)
perfgate run --cmd "fish -c 'nowdate'" --runs 10 --save baseline.json
# Compare two results
perfgate compare --before baseline.json --after current.json
# CI gate: fail if regression exceeds 5%
perfgate gate --cmd "fish -c 'nowdate'" --baseline baseline.json --max-regression 5Stats reported: mean, median, P95, stddev, min, max. Exit code 1 on gate failure. Perfgate provides the measurement layer for detecting when shell functions are fast enough to stay at the fish layer vs. when they should graduate to a compiled binary.
Profile fish shell functions to find slow call paths. Shellprof surfaces call-graph data that feeds graduation decisions - identifying which fish functions are hot enough to warrant crystallization into Go.
# Profile a function (default: 3-level call depth)
shellprof profile nowdate
# Deep profile up to 5 levels
shellprof profile nowdate --depth 5
# Single-run trace (full depth)
shellprof trace nowdate
# Output formats: text (default), json, flame
shellprof profile nowdate --format flameValidate the Claude UserPromptSubmit hook signal contract against a machine-readable schema.
# Run hook, validate all 8 emitted signals against schema
hookval validate
# Generate Hook Context Signals markdown table (for CLAUDE.md insertion)
hookval gen-docs
# Validate schema file is well-formed
hookval lint-schema
# Override defaults
hookval validate --schema ~/.claude/hook-signal-schema.yaml --hook ~/.claude/hooks/prompt-context.fishExit 0 = all signals pass. Exit 1 = per-signal violation report. Prevents silent doc/impl drift in hook context injection.
Compute per-user Anthropic API efficiency signals from Datadog metrics. Data adapter for the ClaudeConfig AI usage scoring rubric.
# Plain text report (default 7d window)
effiscore score --user brian_lopez
# JSON output (ClaudeConfig M2 contract)
effiscore score --user brian_lopez --json
# Custom window
effiscore score --user brian_lopez --window 14dFive dimensions: Cache Hit Rate, Cache Reuse Factor, I/O Ratio, Token Savings, Model Mix (Haiku %). Composite score weighted 30/25/20/15/10 with tier classification (poor/fair/good/excellent). Requires DD_API_KEY and DD_APP_KEY environment variables. Every run emits cloud_llm_efficiency and dd_api_health events to the topology bus.
Webhook service that bridges Android share actions to tmux sessions with AI-powered content triage. Receives POST /share from Android (HTTP Shortcuts or standalone APK), validates and routes payloads to tmux via a local HTTP server or Tailscale Funnel. Shared URLs are scored by an LLM triage pipeline (Haiku) using per-profile prompt manifests, with auto-archiving and FCM push notifications for high-value content.
First run (fresh host):
# 1. Scaffold ~/.config/linkari/config.toml with ${secretsmanager:...} defaults
linkari config init
# 2. Edit secret references as needed, then validate
linkari doctor
# 3. Zero-flag production boot
linkari serveOperations:
# Validate secrets + dirs without booting
linkari doctor # human-readable (exit 1 on any fail)
linkari doctor --json # structured JSON
# In K3s pods, set LINKARI_K8S_MODE=true to include volume health checks
# SQLite durability tools
linkari db backup --queue-db ~/.config/linkari/queue.db --dest /tmp/queue.db
linkari db restore --queue-db ~/.config/linkari/queue.db --src /tmp/queue.db
# Background daemon (POSIX: macOS, Linux, Termux)
linkari serve --detach
kill $(cat ~/.local/state/linkari/linkari.pid)
# Local dev (skip Tailscale Funnel)
linkari serve --local
LINKARI_LOCAL=1 linkari serve
# Break-glass (all flags; no config.toml required)
linkari serve --tsnet --tsnet-authkey $TS_AUTHKEY --token $LINKARI_TOKEN \
--firebase-sa ~/.config/linkari/firebase-sa.json --notify-min-score 10 --debugIf tsnet_authkey is not configured and --tsnet was not set explicitly, linkari serve automatically falls back to local-only mode with a WARN log. Config is hot-reloadable via SIGHUP (actions, archive thresholds, push config, watchdog tuning).
Structured capture config (~/.config/linkari/config.toml):
| Field | Type | Description |
|---|---|---|
artifact_dir |
string | Directory for capture artifacts (e.g. ~/docs/captures) |
artifact_filename_template |
string | Go text/template for artifact filenames (e.g. {{.Date}}_{{.Key}}.md) |
domain_routes |
map[string]string | Per-domain action override - maps domain patterns to action names (e.g. "jira.atlassian.net" = "capture_jira_auto") |
post_capture_command |
string | Optional shell command executed after the artifact is written. Template vars: {{.Key}}, {{.ArtifactPath}}, {{.URL}}, {{.Date}}. Execution is best-effort - non-zero exit logs capture_command_error but the queue row stays status=captured. Only {{.Key}} is validated (regex-gated) before substitution; no other content from the fetched payload reaches the command. |
Actions: text (paste into existing pane), url (opens new tmux window via uinit with profile), ginit (parses Jira key, opens ginit <KEY>), capture_jira_auto (action kind KindCapture - intercepts Jira browse URLs and writes a structured markdown artifact to docs/captures/ with YAML frontmatter instead of routing to the LLM scorer), capture_confluence_auto (same KindCapture path for Confluence page URLs - fetches ADF content and converts to markdown; unknown ADF node types are skipped with an adf_unsupported_block warning and the rest of the page still renders), capture_github_pr_auto (same KindCapture path for GitHub PR URLs matching github.com/.*/pull/ - fetches PR metadata via GitHub REST API; works unauthenticated at 60 req/hr or authenticated when a GitHub token is configured; ArtifactKey format: {owner}-{repo}-{prNumber}; non-PR GitHub URLs fall through to the existing fetch path). Seven URL profiles: eng, life, travel, fashion, music, finance, dining. URL windows use remain-on-exit failed - auto-close on success, stay open on error. Domain heuristics auto-classify URLs into profiles (e.g. github.com → eng, booking.com → travel). Login-wall domains (Instagram, X/Twitter, Facebook) are pre-filtered to avoid wasting Haiku calls on inaccessible content; LinkedIn /pulse/ articles are exempted as publicly accessible.
Server-side scoring: URL actions with ServerScore: true bypass the tmux → fish pipeline entirely - a goroutine fetches page content via Jina Reader, evaluates with Haiku, scores, archives, and sends FCM push. Unsupported domains (YouTube, Spotify, TikTok, etc.) return early without burning a Haiku call.
Auth model (permanent): All LLM scoring shells out to the claude CLI binary, which authenticates via the user's own Claude Code subscription (OAuth2 device flow). Linkari does not support Anthropic API keys, API client libraries, or direct Anthropic HTTP calls. Each user installs and runs Linkari on their own laptop - there is no shared API key deployment model.
Execution runtime: Three runtime implementations - LocalRuntime (default, subprocess), ContainerRuntime (gVisor-sandboxed via containerd for ffmpeg/whisper), and HybridRuntime (routes ffmpeg/whisper through containers but keeps claude CLI local for OAuth2 reasons). Controlled by sandbox.enabled in config.toml.
Shield: X-Linkari-Client header validation on the Funnel mux. Two modes: log (default, debug logging) and enforce (403 on invalid/missing headers). CORS preflight (OPTIONS) is always exempt.
Integration source control ([server.sources]): Each ingestion source can be independently enabled or disabled via config.toml. All flags default to true - omitting the block is identical to all-enabled. SIGHUP reloads the flags; a toggled-off source stops on its next poll tick. Disabled sources emit a source_disabled event.
| Flag | Default | Controls |
|---|---|---|
youtube_watch_later_enabled |
true |
yt_watch_later Watch Later playlist sync |
youtube_monitored_enabled |
true |
yt_monitored subscription channel poller |
youtube_liked_enabled |
true |
yt_liked Liked Videos sync |
bluesky_firehose_enabled |
true |
bsky_firehose Bluesky relay scoring |
YouTube sub-behavior toggles ([server.youtube]): Two pipeline stages - Enqueue and Transcribe (Whisper audio fallback) - can be independently gated per source. Dedup (seen_content) always runs regardless of flags. All flags default to true.
| Flag | Default | Controls |
|---|---|---|
auto_enqueue_subscriptions |
true |
Write yt_monitored items to score queue |
auto_enqueue_watch_later |
true |
Write yt_watch_later items to score queue |
transcribe_subscriptions |
true |
Whisper audio fallback for yt_monitored items with no subtitles |
transcribe_watch_later |
true |
Whisper audio fallback for yt_watch_later items with no subtitles |
Note: transcribe=false only skips the Whisper stage - yt-dlp subtitle extraction still runs, and scoring proceeds normally if subtitles are found.
Database durability config ([server.db]): EPIC-223 F3.
| Field | Type | Default | Description |
|---|---|---|---|
backup_path |
string | ~/.local/state/linkari/backups/latest.db |
Absolute path to the backup database file. linkari doctor checks backup freshness (ok ≤24h, warn ≤72h, fail >72h). When configured, the linkari db backup command writes a .backup-meta.json sidecar that tracks completed_at, bytes, and duration_ms. The check is optional - omitting this field skips the backup freshness check in doctor. |
Endpoints:
| Endpoint | Method | Description |
|---|---|---|
/share |
POST | Route share payloads to tmux |
/actions |
GET | Action registry for dynamic Android intents |
/queue |
GET | List queued items (filter by ?status=) |
/queue/{id}/score |
POST | Score a queued item; auto-archives above profile threshold |
/queue/{id}/outcome |
POST | Record user outcome (opened, saved, dismissed) for a scored item |
/queue/{id}/feedback |
POST | Submit feedback (thumbs up/down, correction) on a scored item |
/queue/slug/{slug}/feedback |
POST | Feedback by workspace slug (convenience alias) |
/queue/slug/{slug}/outcome |
POST | Outcome by workspace slug (convenience alias) |
/profiles/stats |
GET | Per-profile scoring statistics and feedback summary |
/archive |
GET | List archived items (filter by ?profile=, ?status=captured for capture-only rows) |
/digest |
GET | Recent scored items (last 24h) |
/notify |
POST | Score callback → FCM push when above threshold |
/register |
POST | Register FCM device token for push notifications |
/search |
POST | Full-text search over scored queue items |
/push/test |
POST | Test FCM push delivery |
/auth/google |
POST | Google ID token verification for mobile clients |
/auth/invite |
POST | Redeem invite code to create a user account |
/admin/invite |
POST | Generate invite codes (operator bearer token required) |
/health |
GET | Minimal health probe (Funnel-safe, returns {"status":"ok"}) |
/healthz |
GET | Health check with DB status (local only) |
/logs |
GET | Last 100 log lines (local only) |
/logs/stream |
GET | SSE realtime log stream (local only) |
All endpoints except /healthz, /logs, /logs/stream, and /auth/* require bearer token auth (or Google ID token for authenticated users). Rate limited to 30 req/min per IP.
Adaptive assistant (EPIC-072): Feedback and outcome signals flow back from the client via /queue/{id}/feedback and /queue/{id}/outcome (or by slug). A tag-based clustering engine (Jaccard similarity on topic_tags) detects content themes across scored items. When a scored item meets the profile threshold, action routing dispatches it to either Jira ticket drafting (draft_jira_ticket) or research digest append (append_research_digest). Per-profile statistics are available via GET /profiles/stats.
Classification cascade (EPIC-079): classifyShareRequestFast (stages 1-5, no LLM) runs synchronously pre-enqueue. classifyShareRequest (stages 1-6, includes LLM) runs async in scoreAsync. File shares fall through to stage-6 LLM classify using synthesized metadata. Stage-6 has two distinct invocation paths: (a) URL shares post-Jina fetch, where the fetched content is reclassified when stage 5 returned url_domain_fallback; (b) non-URL shares with metadata hints (ExtraSubject/ExtraText/Filename), which are classified from synthesized metadata before content fetch. Both call classifyContentProfile but at different pipeline stages.
Vision scoring (EPIC-079 M3): HaikuVisionEvaluator scores local image files via claude --print --allowedTools Read --output-format json --json-schema <schema>. Selected when req.Type == "image" and the file is readable. The score-first short-circuit instruction tells the model to respond immediately with {"score": 0, "verdict": "personal photo"} for camera-roll images with no engineered content, bypassing full rubric evaluation.
Vision prefilter gates: Three gates run before vision eval in scoreAsync, checked in order (first match wins):
| Gate | Config Key | Default | Trigger |
|---|---|---|---|
oversize_file |
image_noise_gate_max_bytes |
15 MB | File exceeds max bytes |
camera_photo_gate |
(heuristic, no config) | - | Gallery app + camera filename pattern (IMG_/DSC_/PXL_ prefix) + no text metadata. Bypassed when image_text_extraction_enabled=true and extracted text exceeds image_short_circuit_bypass_min_chars. |
noise_gate_min_size |
image_noise_gate_min_bytes |
1 KB | File below min bytes with no text metadata |
When any gate fires, vision eval is skipped and the share falls through to metadata-only scoring. Each gate emits a score_prefilter_skip event.
Image text extraction pre-pass (EPIC-122): When image_text_extraction_enabled = true (default: false), a Claude Haiku vision call runs before scoring for all type=image shares that pass the noise gate. The call transcribes all visible text verbatim (model: claude-haiku-4-5, 30s timeout, max_concurrency=1). When extracted text exceeds image_short_circuit_bypass_min_chars (default: 20), the camera-photo short-circuit is suppressed and the transcript is included in the scoring prompt - 9 fields instead of 5 (calling_package, relative_path, is_screenshot, url, transcribed_text appended when present). Transcript files are persisted to transcripts_dir at YYYYMMDD_<rowID>_IMG_<slug>.md. CLI failure or timeout falls through to current scoring behavior; the share is never blocked. Feature is disabled by default for safe rollout.
| Config Field | Type | Default | Description |
|---|---|---|---|
image_text_extraction_enabled |
bool | false |
Enable Haiku vision pre-pass before image scoring. Set to true in [server] to activate. |
image_short_circuit_bypass_min_chars |
int | 20 |
Minimum extracted-text length (chars) required to suppress the camera-photo short-circuit and activate full rubric. |
Rejection taxonomy: Shares can be rejected (prefiltered) before or during scoring. Each reason code produces a specific queue row effect and optionally triggers an FCM push notification to the user.
User-notified prefilters (FCM push via enqueuePrefilterPush):
| Reason Code | Trigger | Queue Effect | User-Facing Verdict |
|---|---|---|---|
unsupported_pipeline |
URL matches streaming domains (YouTube, Spotify, TikTok, Twitch, SoundCloud, Netflix, Vimeo, Rumble, Dailymotion). Domain list configurable via unsupported_pipeline_domains in config.toml. |
Pre-enqueue (primary): no queue row created, score_prefilter_skip event emitted (phase: "pre_enqueue"). Safety-net in scoreAsync: MarkFailedWithReason (fires only when scoreAsync is called directly, bypassing the HTTP handler). |
"Video platform - not yet supported" |
login_wall_domain |
URL matches login-walled domains (Instagram, X/Twitter, Facebook, LinkedIn non-/pulse/) |
No queue row created (pre-enqueue) | "Login-walled site - can't access content" |
screenshot_no_text |
Screenshot with empty ExtraSubject + ExtraText | MarkFailedWithReason |
"Screenshot had no extractable text" |
fetch_failed |
Jina Reader HTTP error | MarkFailedWithReason |
"Could not fetch page content" |
empty_content |
Jina response body empty after truncation | MarkFailedWithReason |
"Page had no readable content" |
no_metadata |
File share with no ExtraSubject, ExtraText, Filename, or MimeType | MarkFailedWithReason |
"File had no scorable metadata" |
template_error |
Profile template load failure | MarkFailedWithReason |
"Scoring configuration error" |
Internal-only prefilters (no FCM push):
| Reason Code | Trigger | Queue Effect |
|---|---|---|
eval_failed |
Evaluate() returns error |
MarkFailedWithReason |
score_persist_failed |
Score DB write error | MarkFailedWithReason |
scoring_timeout |
Relayed row exceeds relayed_watchdog_max_age (default 15m) |
MarkFailedWithReason (watchdog) |
Audio pipeline prefilters (no FCM push):
| Reason Code | Trigger |
|---|---|
ffmpeg_failed |
ffmpeg audio conversion error |
wav_stat_failed |
WAV file stat failure |
segment_failed |
ffmpeg segmentation error |
transcription_failed |
Whisper single-pass failure |
transcription_failed_chunk_N |
Whisper chunk N failure |
empty_transcript |
Whisper produces empty output |
template_load_failed |
Voice note synopsis template missing |
FCM push for prefiltered shares is controlled by notify_on_prefilter_skip in config.toml (default false; Go runtime default is true but the TOML zero-value override wins when the key is absent). Reasons not in the prefilterVerdicts map fall back to "Share could not be scored".
Vision telemetry: The claude CLI's --output-format json envelope reports input_tokens reflecting only text tokens from the CLI wrapper, not image tokens. ImageTokensEstimated is back-calculated when InputTokens < 100 && CostUSD > 0.01 using Haiku 4.5 pricing ($1.00/MTok input, $5.00/MTok output): imageCost = totalCost - (inputTokens × $1/MTok + outputTokens × $5/MTok), then imageTokensEstimated = imageCost / $1/MTok. This value is emitted in vision_token_correction events and slog but is not persisted to SQLite (known limitation).
Event types: Key pipeline events emitted to linkari_events.jsonl:
| Event Type | Description |
|---|---|
classify_stage_win |
Classification cascade resolved a profile (fields: profile, classify_source, phase) |
score_prefilter_summary |
Per-share pipeline summary (fields: prefilter_stage, eval_skipped, latency_ms, cost_usd, type) - always emitted via defer |
score_prefilter_skip |
Vision gate or login-wall rejection (fields: stage, file_size or filename) |
score_repair_turn |
Scoring required a repair turn (fields: cost_usd, profile, type) |
vision_token_correction |
Image token back-calculation applied (fields: input_tokens, image_tokens_estimated, cost_usd) |
score_cost_exceeded |
Scoring cost exceeded threshold (fields: cost_usd, threshold_usd, profile) |
score_cluster_semaphore_full |
Scoring semaphore saturated (slog only, not in JSONL event log) |
Auth: Google Sign-In (ID token verification against Google JWKS) for mobile clients. Invite-code flow for onboarding new users; operator bearer token generates codes via POST /admin/invite.
Queue & replay: Every share is persisted to SQLite before routing. If tmux is unavailable, the request returns "queued" and a background goroutine replays pending items every 30s when tmux comes back. A RelayedWatchdog marks rows stuck in relayed status past a configurable max age as failed with scoring_timeout. A SnapshotWorker periodically writes a clean copy of queue.db via VACUUM INTO for crash recovery.
Scoring & archiving: POST /queue/{id}/score accepts a score (0-100), tags, and slug. Items auto-archive when score meets the profile threshold (80 default, 70 for finance/dining, disabled for life). Archived high-score items trigger an FCM digest push at most once per hour (configurable per-profile throttle via config.toml). Push delivery uses a durable outbox pattern (push_outbox table) with exponential backoff and a 24h TTL.
CLI subcommands:
| Subcommand | Description |
|---|---|
serve |
Start the webhook HTTP server (local + optional Tailscale Funnel) |
config init |
Scaffold ~/.config/linkari/config.toml |
doctor |
Validate secrets and directories without booting |
score <url> |
Run the triage scoring pipeline against a single URL |
score-write |
Write a pre-computed score directly (legacy/scripting) |
triage <url> |
Run profile-aware LLM triage (Haiku) and persist the verdict |
eval capture |
Snapshot triage inputs+outputs into a fixture JSON |
eval run |
Replay fixtures against the scorer; exit non-zero on regression |
eval refresh-goldens |
Re-score fixture goldens against current prompt manifests |
profile lint |
Validate profile YAML manifests against the v1 schema |
search <query> |
Full-text search over scored queue items |
backfill [dir] |
Ingest existing _score.json files into the queue database |
tag-backfill |
Re-process scored items missing topic tags through tag extraction |
digest |
List recent scored items (CLI equivalent of GET /digest) |
completion |
Generate shell completions (fish/bash/zsh/powershell) |
Triage pipeline: linkari triage loads a per-profile YAML manifest from docs/prompts/profiles/, renders a system prompt via text/template, shells out to the claude CLI for a single-turn Haiku call, parses the returned markdown verdict, and persists the score through Queue.ScoreByURL. The eval harness (linkari eval) supports capture/replay of fixture corpora for regression testing across prompt iterations.
Observability: JSONL event logging to ~/.config/linkari/linkari_events.jsonl. Key event types: linkari_share, linkari_digest, share_scoring_timeout, score_async_* (server-side pipeline stages), push_attempt, snapshot_written, shield_blocked, config_reloaded. Structured logging via slog with configurable format (text/json) and level. SIGHUP hot-reloads config and emits config_reloaded.
K3s deployment (Timoni CUE): Linkari can be deployed to a self-hosted K3s cluster with durable storage and automated backups. The Timoni module (infra/timoni/templates/) provisions two PersistentVolumeClaims (10Gi each, default local-path storage class):
linkari-data: Live database (queue.db, Tailscale state).replicas: 1,Recreatestrategy, single-writer invariant enforced byPodDisruptionBudget(maxUnavailable: 0).linkari-backup: Backup snapshots written by the sidecar (linkari db backup --interval 6h). Mounted read-only by the main pod's health checks, read-write by the optional backup sidecar.
Operator overrides via values.cue:
values: {
backupStorage: "50Gi" // Custom backup PVC size
backupStorageClass: "nvme" // Alternative storage class (e.g. different disk)
backupInterval: "12h" // Snapshot frequency
backupPath: "/backup/linkari/queue.db" // Snapshot dest (on backup PVC)
}Deploy:
timoni build linkari ./infra/timoni -n linkari -o yaml | kubectl apply -f - # dry-run first
make k8s-diff # Preview (dry-run)
make k8s-deploy # Apply to k3dBackup sidecar: An optional init container runs linkari db backup --interval <dur> --overwrite --dest <path> in a loop, producing snapshots to the linkari-backup PVC. The sidecar opens the database read-only (separate WAL reader) so it does not contend with the main process's single pooled connection or violate the single-writer invariant. Failed cycles are non-fatal - the prior good snapshot is retained and the loop continues. Off-node replication (e.g. rsync to S3) remains an infrastructure concern (CronJob outside the pod). SIGTERM gracefully exits after the in-flight snapshot completes, so pod termination does not interrupt backups mid-write.
Migration runbook: For one-time Mac→K3s cutover, keep the Mac authoritative until the in-pod verification passes: (1) linkari db backup, (2) PRAGMA integrity_check, (3) rsync the backup into the linkari-backup PVC and verify sha256sum, (4) make k8s-deploy, (5) confirm db-restore logs show seed complete, (6) verify in-pod integrity plus per-table row-count parity before cutover.
Send WhatsApp messages from the command line. Supports two transports: personal (whatsmeow, QR login) and cloud (WhatsApp Business API, token-based - EPIC-001 M4, planned).
# Personal (default) - authenticate via QR code
wasend login
wasend send -t 15551234567 "Hello from CLI"
echo "Hello" | wasend send -t 15551234567 --stdin
wasend logout
# Cloud API (planned) - headless, token-based
wasend send --api cloud -t 15551234567 "Hello from CLI"
wasend send --api cloud -t 15551234567 --template hello_worldExport ProtonMail conversations to markdown files with YAML front-matter.
# Export emails matching a contact
protonexport export -u user@proton.me -p password -c contact@example.com
# Specify output directory and worker count
protonexport export -u user@proton.me -p password -c contact@example.com -o ./export -w 20Credentials can also be set via PROTON_USERNAME, PROTON_PASSWORD, PROTON_SENDER environment variables.
Fetch fully rendered HTML from a URL using headless Chromium via Playwright. Waits for JS rendering and network idle before outputting to stdout.
# Basic fetch (outputs rendered HTML to stdout)
fetchpage https://example.com
# With stealth mode (anti-detection fingerprint)
fetchpage https://example.com --stealth
# Custom timeout and post-load wait
fetchpage https://example.com --timeout 60 --wait 5
# Use chromium instead of chrome
fetchpage https://example.com --channel chromiumRequires one-time Playwright setup: make setup-fetchpage.
Fetch and export Atlassian & GitHub work data. Reads Jira, Confluence, and GitHub to produce weekly/quarterly summaries, performance reviews, career progression reports, and trend analysis. Config lives in ~/.config/workctl/config.yaml; cache is SQLite (optionally encrypted via WORKCTL_CACHE_PASSPHRASE).
workctl init # scaffold config
workctl weekly # weekly work summary
workctl quarterly # quarterly summary
workctl review # performance review generation
workctl insights # work insights across sources
workctl career # career progression analysis
workctl trends # trend analysis
workctl cache --refresh # force cache refreshStream GitHub repository activity to the terminal. Polls push events, pull requests, and workflow runs at a configurable interval; deduplicates across restarts via a local state file. Outputs human-readable text or JSONL.
ghwatch --repo owner/repo # watch all event types (30s interval)
ghwatch --repo owner/repo --events push,pr # filter event types
ghwatch --repo owner/repo --interval 60s --json # JSONL output
ghwatch --repo owner/repo --since 4h # lookback window on first runRequires $GITHUB_TOKEN or --token.
Structural Go source analysis using tree-sitter. Parses files in <5ms without a Go workspace or build context - use it to orient on large files before reading.
# List all function/method signatures with line ranges
ts-go funcs cmd/linkari/handler.go
ts-go funcs --format compact cmd/linkari/handler.go # lower token cost
# Extract a single function body + doc comments by name
ts-go extract cmd/linkari/handler.go handleShare
# List type declarations (structs, interfaces, aliases) with field counts
ts-go types cmd/linkari/queue.go
ts-go types --format compact cmd/linkari/queue.go
# Search files for a tree-sitter S-expression pattern
ts-go search '(function_declaration name: (identifier) @name)' 'cmd/linkari/*.go'
ts-go search '(call_expression function: (identifier) @fn (#eq? @fn "slog.Info"))' 'cmd/**/*.go'
# Rewrite source by matching a pattern and applying a template
ts-go rewrite '(function_declaration name: (identifier) @name)' 'func renamed_@name()' '*.go' --diff
ts-go rewrite '(comment) @c' '' 'main.go' --writeOutput formats: json (default), compact. --diff and --write flags available for rewrite. Separate module (cmd/ts-go/go.mod) to isolate CGo tree-sitter dependencies from the root module.
Remote tmux session manager over SSH. Manages persistent tmux sessions on remote hosts via a local daemon, with auto-reconnect on disconnect.
bmux start # start the local daemon
bmux serve # serve a remote tmux session over SSH
bmux attach # attach to a running session
bmux status # show daemon and session status
bmux stop # stop the daemon
bmux doctor # validate config and SSH connectivity
bmux config # manage config file (~/.config/bmux/config.yaml)
bmux completion fish # generate fish completionsSeparate module (cmd/bmux/go.mod) to isolate golang.org/x/crypto SSH dependencies from the root module. Internal packages (config, ssh, daemon, pane, bridge, reconnect) live under cmd/bmux/internal/.
make install-bmux-completions # install + register fish completionsmake build # builds all 12 binaries to bin/
make install # go install → ~/go/bin
make core # builds mdq, perfgate, shellprof, hookval, effiscore
make clean # removes bin/
go test ./... # run root module tests
cd cmd/linkari && go test ./... # linkari (separate module)
cd cmd/wasend && go test ./... # wasend (separate module)
cd cmd/protonexport && go test ./... # protonexport (separate module)
cd cmd/workctl && go test ./... # workctl + ghwatch (separate module)Version, commit, and build date are injected at build time via ldflags. The Go workspace (go.work) ties the root module to six separate-module satellites: linkari, fetchpage, ts-go, wasend, protonexport, and workctl.
Container images (linkari sandbox runtime):
make container-build # native platform (dev iteration)
make container-push IMAGE_REGISTRY=ghcr.io/blo-grindr/linkari # multi-arch push
make lima-start # start Lima gVisor VM
make lima-test # gVisor smoke test
make integration-test # container integration tests (requires Lima)Three images: ffmpeg (audio conversion), whisper (speech-to-text), claude-sandbox (LLM invocation). All support linux/amd64 + linux/arm64 via docker buildx.
Every tool emits schema v2 JSONL events to ~/.automation-metrics/events/YYYY-MM-DD.jsonl via emit_jsonl. Events include command, subcommand, duration, exit code, and flags - correlated by session_id across all tools in the suite.
Recent signal from the bus (all counts real, sampled from local event files on 2026-07-22):
| Tool | Observed runs |
|---|---|
ts-go |
1,255,209 |
linkari serve |
66,382 |
claude_prompt |
18,758 |
workctl fish |
13,391 |
mdq query |
3,054 |
| Total events | 1,539,107 |
This telemetry feeds topology consumers like agrad (graduation signals) and aregress (regression detection), which track whether a tool is earning its place at the Go CLI layer or should be simplified back to a shell function.
cmd/bmux/ # bmux entry point (separate module - golang.org/x/crypto SSH deps isolated)
internal/ # config, ssh, daemon, pane, bridge, reconnect, io packages
cmd/mdq/ # mdq entry point (root module)
cmd/perfgate/ # perfgate entry point (root module)
cmd/shellprof/ # shellprof entry point (root module)
cmd/hookval/ # hookval entry point (root module)
cmd/effiscore/ # effiscore entry point (root module)
cmd/fetchpage/ # fetchpage entry point (separate module)
cmd/ts-go/ # ts-go entry point (separate module - tree-sitter CGo deps isolated)
cmd/linkari/ # linkari entry point (separate module, ~80 files)
cmd/wasend/ # wasend entry point (separate module)
cmd/protonexport/ # protonexport entry point (separate module)
cmd/workctl/ # workctl module root (separate module)
cmd/workctl/ # workctl binary entry point
cmd/ghwatch/ # ghwatch binary entry point (shares workctl go.mod)
internal/ # workctl + ghwatch shared internals
internal/mdq/ # markdown parser, query engine, output formatting
internal/perfgate/ # benchmark runner, statistics, gating logic
internal/shellprof/ # fish instrumentation, profiling, call graph
internal/hookval/ # schema parsing, signal validation, doc generation
internal/effiscore/ # DD metrics client, efficiency scoring, topology emission
internal/telemetry/ # CLI telemetry via emit_jsonl
internal/version/ # shared version formatting
container/ # Dockerfiles for gVisor sandbox runtime (ffmpeg, whisper, claude-sandbox)
Active development. 15+ tools building and passing tests. Goreleaser + Homebrew distribution live as of v0.2.14.
castexCLI shipped:init,report,directive,sync,doctorsubcommands (agent observability + registry health)chain-eval v0.2.14:indexsubcommand live, epic-to-chain linkage fixed (310 → 124 orphans), CI all greenplaid-servicestable on personal Tailnet; hourly sync, 390+ transactions in SQLite
Last Updated: 2026-06-09
