← Files QuarryFi R&D TrackerARCHIVED FILE

hooks/track-session.sh

27.9 KB · Oct 2, 2026 · 00:30 UTC

↓ Download file

#!/usr/bin/env bash
# QuarryFi session tracker for OpenAI Codex
#
# Accuracy-first design:
# - Event hooks still flush immediately on real Codex activity
# - A background timer sends active-session heartbeats every 60s
# - All senders share one "last sent" clock to avoid double-counting
# - Failures are silenced so tracking never interrupts Codex

set -o pipefail
umask 077

CONFIG_DIR="$HOME/.quarryfi"
CONFIG_FILE="$CONFIG_DIR/config.json"
AUDIT_LOG="$CONFIG_DIR/audit.log"
AUDIT_MAX_BYTES=1048576
DEFAULT_API_URL="https://quarryfi.com"
HEARTBEAT_INTERVAL_SECONDS=60
MIN_TICK_DURATION_SECONDS=45
MAX_IDLE_SECONDS=300

CLI_EVENT="${1:-}"
CLI_CWD="${2:-}"
CLI_SESSION_ID="${3:-}"
EVENT_JSON=$(cat 2>/dev/null || true)
SCRIPT_PATH="${BASH_SOURCE[0]:-$0}"
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$SCRIPT_PATH")" 2>/dev/null && pwd)
PLUGIN_ROOT=$(CDPATH= cd -- "$SCRIPT_DIR/.." 2>/dev/null && pwd)
PLUGIN_MANIFEST="$PLUGIN_ROOT/.codex-plugin/plugin.json"
HOOK_MODE="event_plus_timer"

json_string() {
  printf '%s' "$1" | grep -o "\"$2\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" | head -1 | sed 's/.*:[[:space:]]*"\(.*\)"/\1/'
}

parse_event_fields() {
  if command -v node >/dev/null 2>&1; then
    printf '%s' "$EVENT_JSON" | node -e '
let input="";
process.stdin.setEncoding("utf8");
process.stdin.on("data",(chunk)=>input+=chunk);
process.stdin.on("end",()=>{
  try {
    const event=JSON.parse(input||"{}");
    const clean=(value)=>String(value??"").replace(/[\t\r\n]/g," ");
    process.stdout.write([
      clean(event.hook_event_name),
      clean(event.cwd),
      clean(event.session_id),
      clean(event.file_path??event.tool_input?.file_path),
    ].join("\t"));
  } catch {
    process.exitCode=1;
  }
});'
    return
  fi

  printf '%s\t%s\t%s\t%s' \
    "$(json_string "$EVENT_JSON" "hook_event_name")" \
    "$(json_string "$EVENT_JSON" "cwd")" \
    "$(json_string "$EVENT_JSON" "session_id")" \
    "$(json_string "$EVENT_JSON" "file_path")"
}

json_escape() {
  printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g' | tr '\n\r\t' '   '
}

normalize_api_url() {
  # Released plugins always send to QuarryFi. The argument remains accepted so
  # older config files still parse, but it cannot redirect a seat credential.
  echo "$DEFAULT_API_URL"
}

IFS=$'\t' read -r EVENT_NAME_FROM_JSON EVENT_CWD_FROM_JSON EVENT_SESSION_ID_FROM_JSON EVENT_FILE_PATH_FROM_JSON <<< "$(parse_event_fields)"
EVENT_NAME_FROM_ENV="${CODEX_HOOK_EVENT:-${HOOK_EVENT_NAME:-}}"

get_plugin_version() {
  if [ -f "$PLUGIN_MANIFEST" ]; then
    grep -o '"version"[[:space:]]*:[[:space:]]*"[^"]*"' "$PLUGIN_MANIFEST" 2>/dev/null | head -1 | sed 's/.*:[[:space:]]*"\(.*\)"/\1/'
    return
  fi
  echo "unknown"
}

get_runtime_channel() {
  case "$PLUGIN_ROOT" in
    *"/.codex/plugins/cache/openai-curated-remote/"*) echo "codex_public_directory_cache" ;;
    *"/.codex/plugins/cache/openai-curated/"*) echo "codex_public_directory_cache" ;;
    *"/.codex/plugins/cache/personal-plugins/"*) echo "codex_personal_cache" ;;
    *"/plugins/quarryfi-time-tracker"*) echo "codex_local_clone" ;;
    *) echo "codex_plugin_custom" ;;
  esac
}

get_install_revision() {
  if [ -f "$SCRIPT_PATH" ]; then
    shasum -a 256 "$SCRIPT_PATH" 2>/dev/null | cut -c1-12
    return
  fi
  if [ -f "$PLUGIN_MANIFEST" ]; then
    shasum -a 256 "$PLUGIN_MANIFEST" 2>/dev/null | cut -c1-12
    return
  fi
  echo "unknown"
}

get_cwd() {
  if [ -n "$CLI_CWD" ]; then
    echo "$CLI_CWD"
    return
  fi
  if [ -n "$EVENT_CWD_FROM_JSON" ]; then
    echo "$EVENT_CWD_FROM_JSON"
    return
  fi
  echo "${CODEX_PROJECT_DIR:-${CODEX_CWD:-$(pwd)}}"
}

resolve_effective_cwd() {
  local cwd="$1"

  [ ! -f "$CONFIG_FILE" ] && {
    echo "$cwd"
    return
  }

  if ! command -v node >/dev/null 2>&1; then
    echo "$cwd"
    return
  fi

  node - "$CONFIG_FILE" "$cwd" <<'NODE' 2>/dev/null || printf '%s\n' "$cwd"
const fs = require("fs");
const [file, cwdArg] = process.argv.slice(2);
const cwd = String(cwdArg || "");
const cfg = JSON.parse(fs.readFileSync(file, "utf8"));
const profiles = Array.isArray(cfg.profiles) ? cfg.profiles.filter(Boolean) : [cfg];
const keyedProfiles = profiles.filter((profile) => profile && profile.api_key);

function normalizedPath(value) {
  return String(value || "").replace(/\/+$/, "");
}

function projectPaths(profile) {
  return [
    ...(Array.isArray(profile.projects) ? profile.projects : []),
    ...(Array.isArray(profile.project_dirs) ? profile.project_dirs : []),
    ...(Array.isArray(profile.projectDirs) ? profile.projectDirs : []),
  ].filter(Boolean).map(normalizedPath);
}

function matchesProject(path, project) {
  const prefix = normalizedPath(project);
  const normalized = normalizedPath(path);
  return !prefix || normalized === prefix || normalized.startsWith(`${prefix}/`);
}

function codexDefault(profile) {
  return profile.codex_default_project ||
    profile.codexDefaultProject ||
    profile.codex_default_project_dir ||
    profile.codexDefaultProjectDir ||
    profile.codex_default_workspace ||
    profile.codexDefaultWorkspace ||
    "";
}

if (keyedProfiles.some((profile) => {
  const projects = projectPaths(profile);
  return projects.length === 0 || projects.some((project) => matchesProject(cwd, project));
})) {
  console.log(cwd);
  process.exit(0);
}

const defaults = keyedProfiles
  .map(codexDefault)
  .filter(Boolean);

if (defaults.length === 1) {
  console.log(defaults[0]);
  process.exit(0);
}

console.log(cwd);
NODE
}

session_dir() {
  local cwd="$1"
  local hash
  hash=$(printf '%s' "$cwd" | shasum -a 256 2>/dev/null | cut -c1-12)
  echo "$CONFIG_DIR/session-codex-${hash}"
}

session_file() {
  local cwd="$1"
  local name="$2"
  echo "$(session_dir "$cwd")/$name"
}

ensure_session_dir() {
  mkdir -p "$CONFIG_DIR" 2>/dev/null || true
  mkdir -p "$1" 2>/dev/null || true
}

get_session_id() {
  local cwd="$1"
  local sid_file
  sid_file=$(session_file "$cwd" "session_id")

  if [ -n "$CLI_SESSION_ID" ]; then
    printf '%s' "$CLI_SESSION_ID" > "$sid_file" 2>/dev/null || true
    echo "$CLI_SESSION_ID"
    return
  fi
  if [ -n "$EVENT_SESSION_ID_FROM_JSON" ]; then
    printf '%s' "$EVENT_SESSION_ID_FROM_JSON" > "$sid_file" 2>/dev/null || true
    echo "$EVENT_SESSION_ID_FROM_JSON"
    return
  fi
  if [ -n "${CODEX_SESSION_ID:-}" ]; then
    printf '%s' "$CODEX_SESSION_ID" > "$sid_file" 2>/dev/null || true
    echo "$CODEX_SESSION_ID"
    return
  fi
  if [ -f "$sid_file" ]; then
    cat "$sid_file" 2>/dev/null
    return
  fi

  local new_id
  new_id="codex-$(date +%s)-${RANDOM:-0}"
  printf '%s' "$new_id" > "$sid_file" 2>/dev/null || true
  echo "$new_id"
}

persist_session_context() {
  local cwd="$1"
  local session_id="$2"
  printf '%s' "$cwd" > "$(session_file "$cwd" "cwd")" 2>/dev/null || true
  printf '%s' "$session_id" > "$(session_file "$cwd" "session_id")" 2>/dev/null || true
}

timestamp_utc() {
  date -u +"%Y-%m-%dT%H:%M:%SZ"
}

epoch_now() {
  date +%s 2>/dev/null || echo 0
}

clamp_duration() {
  local duration="$1"
  if [ "$duration" -lt 0 ] 2>/dev/null; then
    echo 0
  elif [ "$duration" -gt 86400 ] 2>/dev/null; then
    echo 86400
  else
    echo "$duration"
  fi
}

duration_since_last_sent() {
  local cwd="$1"
  local now_ts="$2"
  local last_file
  last_file=$(session_file "$cwd" "last_sent")
  if [ ! -f "$last_file" ]; then
    echo 0
    return
  fi

  local last_ts
  last_ts=$(cat "$last_file" 2>/dev/null)
  if [ -z "$last_ts" ]; then
    echo 0
    return
  fi

  clamp_duration $(( now_ts - last_ts ))
}

record_last_sent() {
  local cwd="$1"
  local now_ts="$2"
  printf '%s' "$now_ts" > "$(session_file "$cwd" "last_sent")" 2>/dev/null || true
}

record_last_activity() {
  local cwd="$1"
  local now_ts="$2"
  printf '%s' "$now_ts" > "$(session_file "$cwd" "last_activity")" 2>/dev/null || true
}

activity_is_fresh() {
  local cwd="$1"
  local now_ts="$2"
  local activity_file last_activity age
  activity_file=$(session_file "$cwd" "last_activity")
  [ -f "$activity_file" ] || return 1
  last_activity=$(cat "$activity_file" 2>/dev/null)
  [ -n "$last_activity" ] || return 1
  age=$(( now_ts - last_activity ))
  [ "$age" -lt 0 ] 2>/dev/null && return 0
  [ "$age" -le "$MAX_IDLE_SECONDS" ] 2>/dev/null
}

cleanup_session_state() {
  local cwd="$1"
  rm -f \
    "$(session_file "$cwd" "last_sent")" \
    "$(session_file "$cwd" "last_activity")" \
    "$(session_file "$cwd" "session_id")" \
    "$(session_file "$cwd" "cwd")" \
    "$(session_file "$cwd" "timer.pid")" 2>/dev/null || true
  rmdir "$(session_dir "$cwd")" 2>/dev/null || true
}

rotate_audit_log() {
  if [ -f "$AUDIT_LOG" ]; then
    local size
    size=$(wc -c < "$AUDIT_LOG" 2>/dev/null || echo 0)
    if [ "$size" -gt "$AUDIT_MAX_BYTES" ]; then
      local lines keep
      lines=$(wc -l < "$AUDIT_LOG" 2>/dev/null || echo 0)
      keep=$(( lines / 2 ))
      tail -n "$keep" "$AUDIT_LOG" > "$AUDIT_LOG.tmp" 2>/dev/null && \
        mv -f "$AUDIT_LOG.tmp" "$AUDIT_LOG" 2>/dev/null || \
        rm -f "$AUDIT_LOG.tmp"
    fi
  fi
}

append_audit() {
  local profile_name="$1"
  local payload="$2"
  local api_url="$3"
  local http_status="$4"

  local safe_profile safe_api_url safe_status
  safe_profile=$(json_escape "$profile_name")
  safe_api_url=$(json_escape "$api_url")
  safe_status=$(json_escape "$http_status")
  rotate_audit_log
  printf '{"ts":"%s","profile":"%s","api_url":"%s","http_status":"%s","payload":%s}\n' \
    "$(timestamp_utc)" "$safe_profile" "$safe_api_url" "$safe_status" "$payload" >> "$AUDIT_LOG" 2>/dev/null || true
}

append_status_audit() {
  local project_name="$1"
  local event_name="$2"
  local status="$3"

  local safe_project safe_event safe_status
  safe_project=$(json_escape "$project_name")
  safe_event=$(json_escape "$event_name")
  safe_status=$(json_escape "$status")
  rotate_audit_log
  printf '{"ts":"%s","project":"%s","event":"%s","status":"%s"}\n' \
    "$(timestamp_utc)" "$safe_project" "$safe_event" "$safe_status" >> "$AUDIT_LOG" 2>/dev/null || true
}

get_project_name() {
  local cwd="$1"
  local name
  name=$(basename "$cwd")
  if [ -z "$name" ] || [ "$name" = "/" ]; then
    name=$(git -C "$cwd" rev-parse --show-toplevel 2>/dev/null | xargs basename 2>/dev/null)
  fi
  echo "${name:-unknown}"
}

get_editor() {
  case "${CODEX_CLIENT:-cli}" in
    app|desktop) echo "Codex App" ;;
    *) echo "Codex CLI" ;;
  esac
}

get_branch() {
  local cwd="$1"
  git -C "$cwd" rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown"
}

get_head_sha() {
  git -C "$1" rev-parse HEAD 2>/dev/null | grep -E '^[a-fA-F0-9]{7,64}$' || true
}

get_repo_fingerprint() {
  local cwd="$1" remote canonical
  remote=$(git -C "$cwd" config --get remote.origin.url 2>/dev/null || true)
  case "$remote" in
    *github.com:*) canonical=${remote#*github.com:} ;;
    *github.com/*) canonical=${remote#*github.com/} ;;
    *) return ;;
  esac
  canonical=$(printf '%s' "$canonical" | sed 's/\.git$//' | tr '[:upper:]' '[:lower:]')
  if command -v shasum >/dev/null 2>&1; then
    printf '%s' "$canonical" | shasum -a 256 | awk '{print $1}'
  elif command -v sha256sum >/dev/null 2>&1; then
    printf '%s' "$canonical" | sha256sum | awk '{print $1}'
  fi
}

get_changed_file_count() {
  local count
  count=$(git -C "$1" status --porcelain --untracked-files=no 2>/dev/null | wc -l | tr -d ' ')
  case "$count" in ''|*[!0-9]*) echo 0 ;; *) [ "$count" -gt 10000 ] && echo 10000 || echo "$count" ;; esac
}

get_activity_kind() {
  local value
  value=$(printf '%s' "$EVENT_FILE_PATH_FROM_JSON" | tr '[:upper:]' '[:lower:]')
  case "$value" in
    *'/test/'*|*'/tests/'*|*'__tests__'*|*'.test.'*|*'.spec.'*) echo "test" ;;
    *migration*|*schema*|*.sql) echo "schema" ;;
    *.md|*.markdown|*.rst) echo "documentation" ;;
    *.json|*.yaml|*.yml|*.toml|*.ini|*.env) echo "configuration" ;;
    *) echo "implementation" ;;
  esac
}

get_language() {
  local dir="$1"

  if [ -n "$EVENT_FILE_PATH_FROM_JSON" ]; then
    local ext
    ext="${EVENT_FILE_PATH_FROM_JSON##*.}"
    if [ -n "$ext" ] && [ "$ext" != "$EVENT_FILE_PATH_FROM_JSON" ]; then
      case "$ext" in
        ts|tsx) echo "typescript"; return ;;
        js|jsx) echo "javascript"; return ;;
        py) echo "python"; return ;;
        rs) echo "rust"; return ;;
        go) echo "go"; return ;;
        rb) echo "ruby"; return ;;
        java|kt) echo "java"; return ;;
        php) echo "php"; return ;;
        swift) echo "swift"; return ;;
        c|cpp|h) echo "c/cpp"; return ;;
        sh|bash|zsh) echo "shell"; return ;;
        json) echo "json"; return ;;
        md|markdown) echo "markdown"; return ;;
      esac
    fi
  fi

  if [ -f "$dir/package.json" ] || [ -f "$dir/tsconfig.json" ]; then
    if [ -f "$dir/tsconfig.json" ]; then echo "typescript"; return; fi
    echo "javascript"; return
  fi
  if [ -f "$dir/Cargo.toml" ]; then echo "rust"; return; fi
  if [ -f "$dir/go.mod" ]; then echo "go"; return; fi
  if [ -f "$dir/pyproject.toml" ] || [ -f "$dir/setup.py" ] || [ -f "$dir/setup.cfg" ]; then echo "python"; return; fi
  if [ -f "$dir/Gemfile" ]; then echo "ruby"; return; fi
  if [ -f "$dir/build.gradle" ] || [ -f "$dir/pom.xml" ]; then echo "java"; return; fi
  if [ -f "$dir/mix.exs" ]; then echo "elixir"; return; fi
  if [ -f "$dir/composer.json" ]; then echo "php"; return; fi
  if [ -f "$dir/Package.swift" ]; then echo "swift"; return; fi
  if [ -f "$dir/CMakeLists.txt" ] || [ -f "$dir/Makefile" ]; then echo "c/cpp"; return; fi

  echo "multi"
}

get_file_type() {
  local dir="$1"

  if [ -n "$EVENT_FILE_PATH_FROM_JSON" ]; then
    local event_ext
    event_ext="${EVENT_FILE_PATH_FROM_JSON##*.}"
    if [ -n "$event_ext" ] && [ "$event_ext" != "$EVENT_FILE_PATH_FROM_JSON" ]; then
      echo "$event_ext"
      return
    fi
  fi

  local recent_ext
  recent_ext=$(git -C "$dir" diff --name-only HEAD~3 HEAD 2>/dev/null | grep '\.' | sed 's/.*\.//' | sort | uniq -c | sort -rn | head -1 | awk '{print $2}')
  if [ -n "$recent_ext" ]; then
    local ext_count
    ext_count=$(git -C "$dir" diff --name-only HEAD~3 HEAD 2>/dev/null | grep '\.' | sed 's/.*\.//' | sort -u | wc -l | tr -d ' ')
    if [ "$ext_count" -gt 3 ]; then
      echo "multi"
    else
      echo "$recent_ext"
    fi
    return
  fi

  case "$(get_language "$dir")" in
    typescript) echo "ts" ;;
    javascript) echo "js" ;;
    python) echo "py" ;;
    rust) echo "rs" ;;
    go) echo "go" ;;
    ruby) echo "rb" ;;
    java) echo "java" ;;
    php) echo "php" ;;
    swift) echo "swift" ;;
    c/cpp) echo "cpp" ;;
    shell) echo "sh" ;;
    *) echo "multi" ;;
  esac
}

map_event() {
  case "$1" in
    SessionStart) echo "session_start" ;;
    Stop) echo "session_end" ;;
    *) echo "heartbeat" ;;
  esac
}

build_payload() {
  local event="$1"
  local duration="$2"
  local now="$3"
  local session_id="$4"
  local project_name="$5"
  local editor="$6"
  local branch="$7"
  local language="$8"
  local file_type="$9"
  local plugin_version="${10}"
  local runtime_channel="${11}"
  local install_revision="${12}"
  local host_app="${13}"
  local head_sha="${14}"
  local repo_fingerprint="${15}"
  local activity_kind="${16}"
  local changed_file_count="${17}"
  local safe_event safe_now safe_session_id safe_project_name safe_editor safe_branch safe_language safe_file_type
  local safe_plugin_version safe_runtime_channel safe_install_revision safe_host_app safe_head_sha safe_repo_fingerprint safe_activity_kind
  safe_event=$(json_escape "$event")
  safe_now=$(json_escape "$now")
  safe_session_id=$(json_escape "$session_id")
  safe_project_name=$(json_escape "$project_name")
  safe_editor=$(json_escape "$editor")
  safe_branch=$(json_escape "$branch")
  safe_language=$(json_escape "$language")
  safe_file_type=$(json_escape "$file_type")
  safe_plugin_version=$(json_escape "$plugin_version")
  safe_runtime_channel=$(json_escape "$runtime_channel")
  safe_install_revision=$(json_escape "$install_revision")
  safe_host_app=$(json_escape "$host_app")
  safe_head_sha=$(json_escape "$head_sha")
  safe_repo_fingerprint=$(json_escape "$repo_fingerprint")
  safe_activity_kind=$(json_escape "$activity_kind")

  local head_fragment="" repo_fragment=""
  [ -n "$safe_head_sha" ] && head_fragment=",\"head_sha\":\"${safe_head_sha}\""
  [ -n "$safe_repo_fingerprint" ] && repo_fragment=",\"repo_fingerprint\":\"${safe_repo_fingerprint}\""

  cat <<EOF
{"client":{"plugin_version":"${safe_plugin_version}","runtime_channel":"${safe_runtime_channel}","hook_mode":"${HOOK_MODE}","install_revision":"${safe_install_revision}","host_app":"${safe_host_app}"},"heartbeats":[{"source":"codex","project_name":"${safe_project_name}","language":"${safe_language}","file_type":"${safe_file_type}","branch":"${safe_branch}","editor":"${safe_editor}","timestamp":"${safe_now}","duration_seconds":${duration},"session_id":"${safe_session_id}","event":"${safe_event}"${head_fragment}${repo_fragment},"activity_kind":"${safe_activity_kind}","changed_file_count":${changed_file_count}}]}
EOF
}

maybe_emit_update_notice() {
  local response_file="$1"
  local source="$2"
  [ ! -s "$response_file" ] && return
  grep -Eq '"updateAvailable"[[:space:]]*:[[:space:]]*true' "$response_file" 2>/dev/null || return

  local latest_version update_url safe_version notice_dir notice_file
  latest_version=$(grep -o '"latestVersion"[[:space:]]*:[[:space:]]*"[^"]*"' "$response_file" 2>/dev/null | head -1 | sed 's/.*:[[:space:]]*"\([^"]*\)"/\1/')
  update_url=$(grep -o '"updateUrl"[[:space:]]*:[[:space:]]*"[^"]*"' "$response_file" 2>/dev/null | head -1 | sed 's/.*:[[:space:]]*"\([^"]*\)"/\1/')
  safe_version=$(printf '%s' "$latest_version" | tr -cd '0-9A-Za-z._-')
  [ -z "$safe_version" ] && return

  notice_dir="$CONFIG_DIR/update-notices"
  notice_file="$notice_dir/${source}-${safe_version}"
  mkdir -p "$notice_dir" 2>/dev/null || return
  find "$notice_dir" -type f -mtime +30 -delete 2>/dev/null || true
  [ -f "$notice_file" ] && return
  : > "$notice_file"
  printf '[QuarryFi] Tracker update v%s available: %s\n' "$latest_version" "$update_url" >&2
}

send_heartbeat_to_profile() {
  local api_key="$1"
  local api_url="$2"
  local profile_name="$3"
  local payload="$4"
  local project_name="$5"
  local event_name="$6"

  api_url=$(normalize_api_url "$api_url")
  local http_status response_file
  response_file=$(mktemp "${TMPDIR:-/tmp}/quarryfi-heartbeat.XXXXXX" 2>/dev/null || true)
  [ -z "$response_file" ] && response_file="/dev/null"
  http_status=$(curl -s -o "$response_file" -w "%{http_code}" \
    --max-time 5 \
    --proto '=https' \
    --tlsv1.2 \
    -X POST \
    -H "Authorization: Bearer ${api_key}" \
    -H "Content-Type: application/json" \
    -d "$payload" \
    "${api_url}/api/heartbeat" 2>/dev/null || echo "000")

  append_audit "$profile_name" "$payload" "$api_url" "$http_status"
  if [ "$http_status" = "200" ] || [ "$http_status" = "201" ] || [ "$http_status" = "204" ]; then
    maybe_emit_update_notice "$response_file" "codex"
    append_status_audit "$project_name" "$event_name" "sent"
  else
    append_status_audit "$project_name" "$event_name" "error:${http_status}"
  fi
  [ "$response_file" != "/dev/null" ] && rm -f "$response_file"
}

is_legacy_config() {
  grep -q '"profiles"' "$CONFIG_FILE" 2>/dev/null && return 1
  grep -q '"api_key"' "$CONFIG_FILE" 2>/dev/null && return 0
  return 1
}

dispatch_to_profiles() {
  local cwd="$1"
  local session_id="$2"
  local event="$3"
  local duration="$4"

  [ ! -f "$CONFIG_FILE" ] && return

  local now project_name editor branch language file_type payload
  local plugin_version runtime_channel install_revision host_app head_sha repo_fingerprint activity_kind changed_file_count
  now=$(timestamp_utc)
  project_name=$(get_project_name "$cwd")
  editor=$(get_editor)
  branch=$(get_branch "$cwd")
  language=$(get_language "$cwd")
  file_type=$(get_file_type "$cwd")
  plugin_version=$(get_plugin_version)
  runtime_channel=$(get_runtime_channel)
  install_revision=$(get_install_revision)
  host_app=$(printf '%s' "$editor" | tr '[:upper:] ' '[:lower:]_' | tr -s '_')
  head_sha=$(get_head_sha "$cwd")
  repo_fingerprint=$(get_repo_fingerprint "$cwd")
  activity_kind=$(get_activity_kind)
  changed_file_count=$(get_changed_file_count "$cwd")
  append_status_audit "$project_name" "$event" "hook_fired"
  payload=$(build_payload "$event" "$duration" "$now" "$session_id" "$project_name" "$editor" "$branch" "$language" "$file_type" "$plugin_version" "$runtime_channel" "$install_revision" "$host_app" "$head_sha" "$repo_fingerprint" "$activity_kind" "$changed_file_count")

  if is_legacy_config; then
    local config_content api_key api_url
    config_content=$(cat "$CONFIG_FILE" 2>/dev/null)
    api_key=$(json_string "$config_content" "api_key")
    api_url=$(json_string "$config_content" "api_url")
    api_url="${api_url:-$DEFAULT_API_URL}"
    if [ -n "$api_key" ]; then
      send_heartbeat_to_profile "$api_key" "$api_url" "default" "$payload" "$project_name" "$event"
    else
      append_status_audit "$project_name" "$event" "skipped:missing_credentials"
    fi
    return
  fi

  if command -v node >/dev/null 2>&1; then
    local matched_profiles
    matched_profiles=$(node - "$CONFIG_FILE" "$cwd" <<'NODE' 2>/dev/null
const fs = require("fs");
const [file, cwd] = process.argv.slice(2);
const cfg = JSON.parse(fs.readFileSync(file, "utf8"));
const profiles = Array.isArray(cfg.profiles) ? cfg.profiles : [cfg];
const normalizedCwd = String(cwd || "");

function matchesProject(project) {
  const prefix = String(project || "").replace(/\/+$/, "");
  return !prefix || normalizedCwd === prefix || normalizedCwd.startsWith(`${prefix}/`);
}

function projectPaths(profile) {
  return [
    ...(Array.isArray(profile.projects) ? profile.projects : []),
    ...(Array.isArray(profile.project_dirs) ? profile.project_dirs : []),
    ...(Array.isArray(profile.projectDirs) ? profile.projectDirs : []),
  ].filter(Boolean);
}

for (const profile of profiles) {
  if (!profile || !profile.api_key) continue;
  const projects = projectPaths(profile);
  if (projects.length > 0 && !projects.some(matchesProject)) continue;
  console.log([
    profile.name || "unnamed",
    profile.api_key,
    profile.api_url || "https://quarryfi.com",
  ].join("\t"));
}
NODE
)

    local sent=0
    local send_pids=""
    while IFS=$'\t' read -r profile_name api_key api_url; do
      [ -z "$api_key" ] && continue
      send_heartbeat_to_profile "$api_key" "$api_url" "$profile_name" "$payload" "$project_name" "$event" &
      send_pids="${send_pids} $!"
      sent=$((sent + 1))
    done <<< "$matched_profiles"

    if [ "$sent" -gt 0 ]; then
      local pid
      for pid in $send_pids; do
        wait "$pid" 2>/dev/null || true
      done
    elif command -v node >/dev/null 2>&1; then
      local fallback_profile
      fallback_profile=$(node - "$CONFIG_FILE" <<'NODE' 2>/dev/null
const fs = require("fs");
const [file] = process.argv.slice(2);
const cfg = JSON.parse(fs.readFileSync(file, "utf8"));
const profiles = (Array.isArray(cfg.profiles) ? cfg.profiles : [cfg])
  .filter((profile) => profile && profile.api_key);

if (profiles.length !== 1) process.exit(0);

const profile = profiles[0];
console.log([
  profile.name || "unnamed",
  profile.api_key,
  profile.api_url || "https://quarryfi.com",
].join("\t"));
NODE
)
      if [ -n "$fallback_profile" ]; then
        while IFS=$'\t' read -r profile_name api_key api_url; do
          [ -z "$api_key" ] && continue
          send_heartbeat_to_profile "$api_key" "$api_url" "$profile_name" "$payload" "$project_name" "$event" &
          send_pids="${send_pids} $!"
          sent=$((sent + 1))
        done <<< "$fallback_profile"
        for pid in $send_pids; do
          wait "$pid" 2>/dev/null || true
        done
      else
        append_status_audit "$project_name" "$event" "skipped:no_matching_profile"
      fi
    else
      append_status_audit "$project_name" "$event" "skipped:no_matching_profile"
    fi
  fi
}

timer_is_running() {
  local cwd="$1"
  local pid_file timer_state timer_pid timer_revision current_revision
  pid_file=$(session_file "$cwd" "timer.pid")
  [ -f "$pid_file" ] || return 1
  timer_state=$(cat "$pid_file" 2>/dev/null)
  case "$timer_state" in
    *"|"*) ;;
    *)
      rm -f "$pid_file" 2>/dev/null || true
      return 1
      ;;
  esac
  timer_pid=${timer_state%%|*}
  timer_revision=${timer_state#*|}
  [ -n "$timer_pid" ] || return 1
  current_revision=$(get_install_revision)
  if [ -z "$timer_revision" ] || [ "$timer_revision" != "$current_revision" ]; then
    rm -f "$pid_file" 2>/dev/null || true
    return 1
  fi
  kill -0 "$timer_pid" 2>/dev/null
}

start_timer_loop() {
  local cwd="$1"
  local session_id="$2"
  local pid_file install_revision
  pid_file=$(session_file "$cwd" "timer.pid")

  if timer_is_running "$cwd"; then
    return
  fi

  install_revision=$(get_install_revision)
  nohup "$0" "__timer_loop" "$cwd" "$session_id" >/dev/null 2>&1 &
  printf '%s|%s' "$!" "$install_revision" > "$pid_file" 2>/dev/null || true
}

stop_timer_loop() {
  local cwd="$1"
  local pid_file timer_state timer_pid
  pid_file=$(session_file "$cwd" "timer.pid")
  if [ -f "$pid_file" ]; then
    timer_state=$(cat "$pid_file" 2>/dev/null)
    timer_pid=${timer_state%%|*}
    if [ -n "$timer_pid" ]; then
      kill "$timer_pid" 2>/dev/null || true
    fi
    rm -f "$pid_file" 2>/dev/null || true
  fi
}

cleanup_timer_pid() {
  local cwd="$1"
  local pid_file timer_identity
  pid_file=$(session_file "$cwd" "timer.pid")
  timer_identity="$$|$(get_install_revision)"
  if [ "$(cat "$pid_file" 2>/dev/null)" = "$timer_identity" ]; then
    rm -f "$pid_file" 2>/dev/null || true
  fi
}

run_timer_loop() {
  local cwd="$1"
  local session_id="$2"
  local pid_file timer_identity
  pid_file=$(session_file "$cwd" "timer.pid")
  timer_identity="$$|$(get_install_revision)"
  printf '%s' "$timer_identity" > "$pid_file" 2>/dev/null || true
  trap 'cleanup_timer_pid "$cwd"' EXIT
  trap 'cleanup_timer_pid "$cwd"; exit 0' HUP INT TERM

  while true; do
    if [ ! -f "$(session_file "$cwd" "session_id")" ]; then
      exit 0
    fi
    if [ "$(cat "$pid_file" 2>/dev/null)" != "$timer_identity" ]; then
      exit 0
    fi

    local now_ts duration_seconds
    now_ts=$(epoch_now)
    if ! activity_is_fresh "$cwd" "$now_ts"; then
      append_status_audit "$(basename "$cwd")" "heartbeat" "timer_expired:idle"
      exit 0
    fi

    sleep "$HEARTBEAT_INTERVAL_SECONDS" || exit 0

    if [ ! -f "$(session_file "$cwd" "session_id")" ]; then
      exit 0
    fi
    if [ "$(cat "$pid_file" 2>/dev/null)" != "$timer_identity" ]; then
      exit 0
    fi

    now_ts=$(epoch_now)
    if ! activity_is_fresh "$cwd" "$now_ts"; then
      append_status_audit "$(basename "$cwd")" "heartbeat" "timer_expired:idle"
      exit 0
    fi
    duration_seconds=$(duration_since_last_sent "$cwd" "$now_ts")
    if [ "$duration_seconds" -lt "$MIN_TICK_DURATION_SECONDS" ] 2>/dev/null; then
      continue
    fi

    dispatch_to_profiles "$cwd" "$session_id" "heartbeat" "$duration_seconds"
    record_last_sent "$cwd" "$now_ts"
  done
}

main() {
  local cwd session_id raw_event event_type now_ts duration_seconds
  cwd=$(get_cwd)
  cwd=$(resolve_effective_cwd "$cwd")
  [ -z "$cwd" ] && exit 0
  ensure_session_dir "$(session_dir "$cwd")"

  if [ "$CLI_EVENT" = "__timer_loop" ]; then
    run_timer_loop "$cwd" "$(get_session_id "$cwd")"
    exit 0
  fi

  session_id=$(get_session_id "$cwd")
  persist_session_context "$cwd" "$session_id"

  raw_event="${CLI_EVENT:-${EVENT_NAME_FROM_JSON:-$EVENT_NAME_FROM_ENV}}"
  [ -z "$raw_event" ] && raw_event="heartbeat"
  event_type=$(map_event "$raw_event")

  if { [ "$event_type" = "session_end" ] && [ ! -f "$(session_file "$cwd" "last_sent")" ] && [ ! -f "$(session_file "$cwd" "timer.pid")" ]; }; then
    exit 0
  fi

  now_ts=$(epoch_now)
  record_last_activity "$cwd" "$now_ts"
  if [ "$raw_event" = "SessionStart" ]; then
    dispatch_to_profiles "$cwd" "$session_id" "session_start" 0
    record_last_sent "$cwd" "$now_ts"
    start_timer_loop "$cwd" "$session_id"
    exit 0
  fi

  if ! timer_is_running "$cwd"; then
    start_timer_loop "$cwd" "$session_id"
  fi

  duration_seconds=$(duration_since_last_sent "$cwd" "$now_ts")
  dispatch_to_profiles "$cwd" "$session_id" "$event_type" "$duration_seconds"
  record_last_sent "$cwd" "$now_ts"

  if [ "$event_type" = "session_end" ]; then
    stop_timer_loop "$cwd"
    cleanup_session_state "$cwd"
  fi
}

main
exit 0

SHA-256: 6e384ace059313df09241faab5be189beb23e0e67e0c6be93750ed8e5d2e5392