loki-mode 7.18.0 → 7.18.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/autonomy/loki CHANGED
@@ -61,6 +61,13 @@ if [ -f "$_LOKI_SCRIPT_DIR/tui.sh" ]; then
61
61
  source "$_LOKI_SCRIPT_DIR/tui.sh"
62
62
  fi
63
63
 
64
+ # Crash-reporting helpers (provides loki_collection_enabled, used by
65
+ # cmd_telemetry status and cmd_crash). Self-guarded against double-source.
66
+ if [ -f "$_LOKI_SCRIPT_DIR/crash.sh" ]; then
67
+ # shellcheck source=crash.sh
68
+ source "$_LOKI_SCRIPT_DIR/crash.sh"
69
+ fi
70
+
64
71
  # Resolve the script's real path (handles symlinks)
65
72
  resolve_script_path() {
66
73
  local script="$1"
@@ -7627,6 +7634,22 @@ cmd_doctor() {
7627
7634
  elif command -v codex &>/dev/null; then
7628
7635
  echo -e " ${DIM} -- ${NC} OPENAI_API_KEY not set (Codex CLI uses its own login)"
7629
7636
  fi
7637
+ # Phase I (v7.5.25): detect ANTHROPIC_BASE_URL alt-provider routing
7638
+ # (OpenRouter, Ollama, LiteLLM, self-hosted). Claude Code reads this env
7639
+ # var natively; Loki passes it through unchanged. Warn when the user sets
7640
+ # an alt endpoint without LOKI_MODEL_OVERRIDE -- the default opus/sonnet/
7641
+ # haiku aliases may not resolve on the alt-provider.
7642
+ if [ -n "${ANTHROPIC_BASE_URL:-}" ]; then
7643
+ echo -e " ${GREEN}PASS${NC} ANTHROPIC_BASE_URL: ${ANTHROPIC_BASE_URL}"
7644
+ pass_count=$((pass_count + 1))
7645
+ if [ -z "${LOKI_MODEL_OVERRIDE:-}" ]; then
7646
+ echo -e " ${YELLOW}WARN${NC} LOKI_MODEL_OVERRIDE not set -- opus/sonnet/haiku aliases may not resolve on alt-provider"
7647
+ warn_count=$((warn_count + 1))
7648
+ else
7649
+ echo -e " ${GREEN}PASS${NC} LOKI_MODEL_OVERRIDE: ${LOKI_MODEL_OVERRIDE}"
7650
+ pass_count=$((pass_count + 1))
7651
+ fi
7652
+ fi
7630
7653
  echo ""
7631
7654
 
7632
7655
  echo -e "${CYAN}Skills:${NC}"
@@ -13456,6 +13479,9 @@ main() {
13456
13479
  report)
13457
13480
  cmd_report "$@"
13458
13481
  ;;
13482
+ crash)
13483
+ cmd_crash "$@"
13484
+ ;;
13459
13485
  share)
13460
13486
  cmd_share "$@"
13461
13487
  ;;
@@ -17927,6 +17953,201 @@ for line in sys.stdin:
17927
17953
  }
17928
17954
 
17929
17955
  # Telemetry management (v6.7.0)
17956
+ cmd_crash() {
17957
+ # Phase 0: local-only crash report inspection. NO network egress.
17958
+ #
17959
+ # PARITY: the user-facing strings here MUST match the TS canonical command
17960
+ # at loki-ts/src/commands/crash.ts byte-for-byte (ignoring color codes).
17961
+ # The whole subcommand body is implemented in one Python helper so the
17962
+ # behavior (filename-id resolution, "-" for missing fields, JSON shape,
17963
+ # URL encoding, exit codes) cannot drift from the TS route.
17964
+ #
17965
+ # Subcommands:
17966
+ # loki crash list .loki/crash/*.json reports
17967
+ # loki crash show <id> pretty-print one scrubbed report
17968
+ # loki crash submit [<id>] print the scrubbed payload + a prefilled
17969
+ # GitHub issue URL for MANUAL submission
17970
+ _LOKI_CRASH_DIR=".loki/crash" _LOKI_CRASH_SUB="${1-}" _LOKI_CRASH_ARG="${2-}" \
17971
+ python3 -c '
17972
+ import json, os, sys, urllib.parse
17973
+
17974
+ # Colors: honor NO_COLOR (https://no-color.org) exactly like the TS route
17975
+ # (loki-ts/src/util/colors.ts). Codes match autonomy/loki:25-32 byte-for-byte.
17976
+ _no_color = len(os.environ.get("NO_COLOR", "")) > 0
17977
+ def _c(code):
17978
+ return "" if _no_color else code
17979
+ RED = _c("\x1b[0;31m")
17980
+ GREEN = _c("\x1b[0;32m")
17981
+ YELLOW = _c("\x1b[1;33m")
17982
+ CYAN = _c("\x1b[0;36m")
17983
+ BOLD = _c("\x1b[1m")
17984
+ NC = _c("\x1b[0m")
17985
+
17986
+ ISSUE_BASE = "https://github.com/asklokesh/loki-mode/issues/new"
17987
+ CRASH_DIR = os.environ["_LOKI_CRASH_DIR"]
17988
+ SUB = os.environ.get("_LOKI_CRASH_SUB", "")
17989
+ ARG = os.environ.get("_LOKI_CRASH_ARG", "")
17990
+
17991
+ HELP = (
17992
+ BOLD + "loki crash" + NC + " - inspect and manually submit local crash reports\n"
17993
+ "\n"
17994
+ "Usage: loki crash [subcommand] [args]\n"
17995
+ "\n"
17996
+ "Subcommands:\n"
17997
+ " (none) List crash reports in .loki/crash/\n"
17998
+ " show <id> Pretty-print one scrubbed crash report\n"
17999
+ " submit [<id>] Print the scrubbed payload and a prefilled GitHub\n"
18000
+ " issue URL for manual submission\n"
18001
+ "\n"
18002
+ "Crash reports are anonymous, scrubbed, and stored locally only. Nothing is\n"
18003
+ "sent automatically in this version. See docs/PRIVACY.md.\n"
18004
+ )
18005
+
18006
+ def s(v):
18007
+ return "-" if v is None else str(v)
18008
+
18009
+ def pad(text, w):
18010
+ return text if len(text) >= w else text + " " * (w - len(text))
18011
+
18012
+ def report_ids():
18013
+ if not os.path.isdir(CRASH_DIR):
18014
+ return []
18015
+ try:
18016
+ names = [e for e in os.listdir(CRASH_DIR)
18017
+ if e.endswith(".json") and os.path.isfile(os.path.join(CRASH_DIR, e))]
18018
+ except OSError:
18019
+ return []
18020
+ return sorted(n[:-len(".json")] for n in names)
18021
+
18022
+ def _safe_id(rid):
18023
+ # Reject path traversal: ids are bare filenames (no separators, no "..",
18024
+ # no leading separator). This guards the direct filename lookup; the
18025
+ # fingerprint-resolution loop only ever passes real listdir ids, which
18026
+ # always pass. Mirrors the identical rule in loki-ts crash.ts.
18027
+ if rid is None or rid == "":
18028
+ return False
18029
+ if "/" in rid or "\\" in rid or ".." in rid:
18030
+ return False
18031
+ if rid[0] in ("/", "\\"):
18032
+ return False
18033
+ return True
18034
+
18035
+ def read_report(rid):
18036
+ if not _safe_id(rid):
18037
+ return None
18038
+ p = os.path.join(CRASH_DIR, rid + ".json")
18039
+ if not os.path.exists(p):
18040
+ return None
18041
+ try:
18042
+ with open(p) as f:
18043
+ return json.load(f)
18044
+ except Exception:
18045
+ return {}
18046
+
18047
+ def resolve_report(arg):
18048
+ direct = read_report(arg)
18049
+ if direct is not None:
18050
+ return (arg, direct)
18051
+ for rid in report_ids():
18052
+ r = read_report(rid)
18053
+ if r is not None and str(r.get("fingerprint", "")) == arg:
18054
+ return (rid, r)
18055
+ return None
18056
+
18057
+ def dumps(obj):
18058
+ return json.dumps(obj, indent=2, ensure_ascii=False)
18059
+
18060
+ def list_crashes():
18061
+ ids = report_ids()
18062
+ if not ids:
18063
+ sys.stdout.write(YELLOW + "No crash reports found." + NC +
18064
+ " Nothing has been captured in .loki/crash/.\n")
18065
+ return 0
18066
+ sys.stdout.write(pad("ID", 40) + " " + pad("CAPTURED_AT", 22) + " ERROR_CLASS\n")
18067
+ for rid in ids:
18068
+ d = read_report(rid) or {}
18069
+ fp = s(d.get("fingerprint"))
18070
+ captured_at = s(d.get("captured_at"))
18071
+ error_class = s(d.get("error_class"))
18072
+ visible_id = fp if fp != "-" else rid
18073
+ sys.stdout.write(pad(visible_id, 40) + " " + pad(captured_at, 22) + " " + error_class + "\n")
18074
+ sys.stdout.write(
18075
+ "\n" + str(len(ids)) + " report(s). Run \x27loki crash show <id>\x27 to inspect, "
18076
+ "\x27loki crash submit\x27 to get a prefilled GitHub issue URL.\n")
18077
+ return 0
18078
+
18079
+ def show_crash(rid):
18080
+ if not rid:
18081
+ sys.stderr.write(RED + "Missing crash id." + NC + " Use \x27loki crash\x27 to list reports.\n")
18082
+ return 2
18083
+ resolved = resolve_report(rid)
18084
+ if resolved is None:
18085
+ sys.stderr.write(RED + "Crash report not found: " + rid + NC + "\n")
18086
+ sys.stderr.write("Use \x27loki crash\x27 to see available reports.\n")
18087
+ return 1
18088
+ sys.stdout.write(dumps(resolved[1]) + "\n")
18089
+ return 0
18090
+
18091
+ def issue_url(report):
18092
+ error_class = s(report.get("error_class"))
18093
+ fp = s(report.get("fingerprint"))
18094
+ short_fp = fp[:12] if fp != "-" else "unknown"
18095
+ title = "crash: " + error_class + " (" + short_fp + ")"
18096
+ payload = dumps(report)
18097
+ body = "\n".join([
18098
+ "Anonymous crash report captured by Loki Mode (scrubbed, whitelist-only).",
18099
+ "",
18100
+ "Scrubbed payload:",
18101
+ "```json",
18102
+ payload,
18103
+ "```",
18104
+ "",
18105
+ "Nothing was sent automatically. This issue is submitted manually by me.",
18106
+ ])
18107
+ q = urllib.parse.urlencode({"title": title, "body": body})
18108
+ return ISSUE_BASE + "?" + q
18109
+
18110
+ def submit_crash(rid):
18111
+ if rid:
18112
+ target = resolve_report(rid)
18113
+ if target is None:
18114
+ sys.stderr.write(RED + "Crash report not found: " + rid + NC + "\n")
18115
+ sys.stderr.write("Use \x27loki crash\x27 to see available reports.\n")
18116
+ return 1
18117
+ else:
18118
+ ids = report_ids()
18119
+ if not ids:
18120
+ sys.stdout.write(YELLOW + "No crash reports found." + NC + " Nothing to submit.\n")
18121
+ return 0
18122
+ last_id = ids[-1]
18123
+ target = (last_id, read_report(last_id) or {})
18124
+ sys.stdout.write(BOLD + "Scrubbed payload (this is the ENTIRE report):" + NC + "\n")
18125
+ sys.stdout.write(dumps(target[1]) + "\n\n")
18126
+ sys.stdout.write(YELLOW + "Nothing is sent automatically in this version." + NC +
18127
+ " Loki Mode never transmits crash data on its own.\n")
18128
+ sys.stdout.write("To submit manually, open this prefilled GitHub issue and review it first:\n\n")
18129
+ sys.stdout.write(" " + CYAN + issue_url(target[1]) + NC + "\n\n")
18130
+ sys.stdout.write(GREEN + "The payload above is exactly what the URL contains." + NC + "\n")
18131
+ sys.stdout.write("See docs/PRIVACY.md for what is and is not collected.\n")
18132
+ return 0
18133
+
18134
+ if SUB == "":
18135
+ sys.exit(list_crashes())
18136
+ elif SUB in ("--help", "-h", "help"):
18137
+ sys.stdout.write(HELP)
18138
+ sys.exit(0)
18139
+ elif SUB == "show":
18140
+ sys.exit(show_crash(ARG))
18141
+ elif SUB == "submit":
18142
+ sys.exit(submit_crash(ARG))
18143
+ else:
18144
+ sys.stderr.write(RED + "Unknown crash subcommand: " + SUB + NC + "\n")
18145
+ sys.stdout.write(HELP)
18146
+ sys.exit(2)
18147
+ '
18148
+ return $?
18149
+ }
18150
+
17930
18151
  cmd_telemetry() {
17931
18152
  local subcommand="${1:-status}"
17932
18153
  shift 2>/dev/null || true
@@ -17991,6 +18212,25 @@ try {
17991
18212
  echo -e " Saved: $saved_endpoint"
17992
18213
  fi
17993
18214
  fi
18215
+
18216
+ # Unified collection state (PostHog usage telemetry + crash reporting).
18217
+ # loki_collection_enabled is the single source of truth (crash.sh).
18218
+ echo ""
18219
+ if type loki_collection_enabled &>/dev/null; then
18220
+ if loki_collection_enabled; then
18221
+ echo -e " Collection: ${GREEN}enabled${NC} (anonymous diagnostics; turn off with: loki telemetry off)"
18222
+ else
18223
+ echo -e " Collection: ${YELLOW}disabled${NC} (re-enable with: loki telemetry on)"
18224
+ fi
18225
+ fi
18226
+
18227
+ # Count of pending local crash reports (.loki/crash/*.json).
18228
+ local crash_dir=".loki/crash"
18229
+ local pending=0
18230
+ if [ -d "$crash_dir" ]; then
18231
+ pending=$(find "$crash_dir" -maxdepth 1 -name '*.json' -type f 2>/dev/null | wc -l | tr -d '[:space:]')
18232
+ fi
18233
+ echo -e " Crash reports pending: ${pending:-0} (see: loki crash)"
17994
18234
  echo ""
17995
18235
  ;;
17996
18236
 
@@ -18058,26 +18298,28 @@ TELEM_DISABLE_PY
18058
18298
  echo " Unset LOKI_OTEL_ENDPOINT in your shell for immediate effect."
18059
18299
  ;;
18060
18300
 
18061
- stop)
18062
- # Persistent opt-out across all sessions
18301
+ stop|off)
18302
+ # Persistent opt-out across all sessions. This is the unified
18303
+ # opt-out: it gates BOTH PostHog usage telemetry AND crash reporting.
18063
18304
  local global_config="${HOME}/.loki/config"
18064
18305
  mkdir -p "${HOME}/.loki"
18065
18306
 
18066
18307
  # Remove existing TELEMETRY_DISABLED line if present, then add
18308
+ # (idempotent).
18067
18309
  if [ -f "$global_config" ]; then
18068
18310
  grep -v "^TELEMETRY_DISABLED=" "$global_config" > "${global_config}.tmp" 2>/dev/null || true
18069
18311
  mv "${global_config}.tmp" "$global_config"
18070
18312
  fi
18071
18313
  echo "TELEMETRY_DISABLED=true" >> "$global_config"
18072
18314
 
18073
- echo -e "${BOLD}Telemetry permanently disabled${NC}"
18315
+ echo -e "${BOLD}Telemetry and crash reporting disabled${NC}"
18074
18316
  echo ""
18075
18317
  echo " Opt-out saved to: $global_config"
18076
18318
  echo " This persists across all sessions and new runs."
18077
- echo " Run 'loki telemetry start' to re-enable."
18319
+ echo " Run 'loki telemetry on' to re-enable."
18078
18320
  ;;
18079
18321
 
18080
- start)
18322
+ start|on)
18081
18323
  # Remove persistent opt-out
18082
18324
  local global_config="${HOME}/.loki/config"
18083
18325
  if [ -f "$global_config" ]; then
@@ -18085,9 +18327,10 @@ TELEM_DISABLE_PY
18085
18327
  mv "${global_config}.tmp" "$global_config"
18086
18328
  fi
18087
18329
 
18088
- echo -e "${BOLD}Telemetry opt-out removed${NC}"
18330
+ echo -e "${BOLD}Telemetry and crash reporting re-enabled${NC}"
18089
18331
  echo ""
18090
- echo " Telemetry can now be enabled with 'loki telemetry enable [endpoint]'."
18332
+ echo " Anonymous diagnostics collection is on again."
18333
+ echo " OTEL tracing can be configured with 'loki telemetry enable [endpoint]'."
18091
18334
  ;;
18092
18335
 
18093
18336
  --help|-h|help)
@@ -18096,11 +18339,13 @@ TELEM_DISABLE_PY
18096
18339
  echo "Usage: loki telemetry <command>"
18097
18340
  echo ""
18098
18341
  echo "Commands:"
18099
- echo " status Show current telemetry config"
18342
+ echo " status Show OTEL config + collection state + pending crash reports"
18100
18343
  echo " enable [endpoint] Enable OTEL (default: http://localhost:4318)"
18101
18344
  echo " disable Disable OTEL for current project"
18102
- echo " stop Permanently opt out of telemetry across all sessions"
18103
- echo " start Remove persistent opt-out, re-enable telemetry"
18345
+ echo " off Opt out of all anonymous diagnostics (telemetry + crash)"
18346
+ echo " on Re-enable anonymous diagnostics"
18347
+ echo " stop Alias for 'off' (permanent opt-out across all sessions)"
18348
+ echo " start Alias for 'on' (remove persistent opt-out)"
18104
18349
  echo ""
18105
18350
  ;;
18106
18351
  *)
package/autonomy/run.sh CHANGED
@@ -651,6 +651,15 @@ if [ -f "$TELEMETRY_SCRIPT" ]; then
651
651
  source "$TELEMETRY_SCRIPT"
652
652
  fi
653
653
 
654
+ # Crash-reporting helpers (Phase 0: local-only, zero egress).
655
+ # Provides loki_collection_enabled (unified opt-out), loki_crash_capture,
656
+ # loki_crash_friction, loki_show_disclosure_once.
657
+ CRASH_SCRIPT="$SCRIPT_DIR/crash.sh"
658
+ if [ -f "$CRASH_SCRIPT" ]; then
659
+ # shellcheck source=crash.sh
660
+ source "$CRASH_SCRIPT"
661
+ fi
662
+
654
663
 
655
664
 
656
665
  # 2026 Research Enhancements (minimal additions)
@@ -1743,6 +1752,11 @@ invoke_with_timeout() {
1743
1752
  done
1744
1753
 
1745
1754
  log_error "Provider spawn failed after $((max_retries+1)) attempts (timeout=${timeout}s)"
1755
+ # Crash friction (retry_loop): provider spawn exhausted all retries -- a
1756
+ # clear threshold (not a single retry). Best-effort, never blocks.
1757
+ if type loki_crash_friction &>/dev/null; then
1758
+ loki_crash_friction "retry_loop" "provider spawn failed after $((max_retries+1)) attempts" >/dev/null 2>&1 || true
1759
+ fi
1746
1760
  return 124
1747
1761
  }
1748
1762
 
@@ -6107,7 +6121,11 @@ track_gate_failure() {
6107
6121
  local gate_file="${TARGET_DIR:-.}/.loki/quality/gate-failure-count.json"
6108
6122
  mkdir -p "$(dirname "$gate_file")"
6109
6123
 
6110
- _GATE_FILE="$gate_file" _GATE_NAME="$gate_name" python3 -c "
6124
+ # IMPORTANT: this function's stdout IS its return value (callers do
6125
+ # count=$(track_gate_failure ...)). Capture the count first, then do any
6126
+ # side-effects with their stdout suppressed, then echo ONLY the count.
6127
+ local count
6128
+ count=$(_GATE_FILE="$gate_file" _GATE_NAME="$gate_name" python3 -c "
6111
6129
  import json, os
6112
6130
  gate_file = os.environ['_GATE_FILE']
6113
6131
  gate_name = os.environ['_GATE_NAME']
@@ -6120,7 +6138,16 @@ counts[gate_name] = counts.get(gate_name, 0) + 1
6120
6138
  with open(gate_file, 'w') as f:
6121
6139
  json.dump(counts, f, indent=2)
6122
6140
  print(counts[gate_name])
6123
- " 2>/dev/null || echo "1"
6141
+ " 2>/dev/null || echo "1")
6142
+
6143
+ # Crash friction (gate_failure): fire exactly once at the threshold (3
6144
+ # consecutive failures) so a sustained failure does not re-fire every
6145
+ # iteration. Best-effort, stdout suppressed so the count stays clean.
6146
+ if [ "${count:-0}" -eq 3 ] 2>/dev/null && type loki_crash_friction &>/dev/null; then
6147
+ loki_crash_friction "gate_failure" "gate=${gate_name} consecutive=${count}" >/dev/null 2>&1 || true
6148
+ fi
6149
+
6150
+ echo "$count"
6124
6151
  }
6125
6152
 
6126
6153
  clear_gate_failure() {
@@ -8196,6 +8223,11 @@ attempt_provider_failover() {
8196
8223
  done
8197
8224
 
8198
8225
  log_warn "Failover: all providers in chain exhausted, falling back to retry"
8226
+ # Crash friction (rate_limit_loop): a clear threshold -- every provider in
8227
+ # the failover chain is rate-limited/unhealthy. Best-effort, never blocks.
8228
+ if type loki_crash_friction &>/dev/null; then
8229
+ loki_crash_friction "rate_limit_loop" "failover chain exhausted: ${FAILOVER_CHAIN}" >/dev/null 2>&1 || true
8230
+ fi
8199
8231
  return 1
8200
8232
  }
8201
8233
 
@@ -11989,6 +12021,23 @@ if __name__ == "__main__":
11989
12021
  "status=$([[ $exit_code -eq 0 ]] && echo ok || echo error)"
11990
12022
  fi
11991
12023
 
12024
+ # Crash capture (Phase 0: local-only, best-effort, never blocks).
12025
+ # Conservative: only on a genuine non-zero failure exit. Signal-induced
12026
+ # exits (130 SIGINT / 143 SIGTERM / 137 SIGKILL) are user/operator
12027
+ # interrupts, not crashes, so we skip them. Known conservatism tradeoff:
12028
+ # this fires once per iteration on any nonzero exit, so a long, repeatedly
12029
+ # failing run can accumulate multiple local reports under .loki/crash/.
12030
+ if [ "$exit_code" -ne 0 ] 2>/dev/null && \
12031
+ [ "$exit_code" -ne 130 ] && [ "$exit_code" -ne 143 ] && [ "$exit_code" -ne 137 ] && \
12032
+ type loki_crash_capture &>/dev/null; then
12033
+ loki_crash_capture \
12034
+ "IterationError" \
12035
+ "provider exited non-zero on iteration ${ITERATION_COUNT:-?}" \
12036
+ "$([ -f "$iter_output" ] && tail -c 16384 "$iter_output" 2>/dev/null || true)" \
12037
+ "${rarv_phase:-iteration}" \
12038
+ "$exit_code"
12039
+ fi
12040
+
11992
12041
  # PRD Checklist verification on interval (v5.44.0)
11993
12042
  if type checklist_should_verify &>/dev/null && checklist_should_verify; then
11994
12043
  checklist_verify
@@ -12914,6 +12963,11 @@ main() {
12914
12963
  trap cleanup INT TERM
12915
12964
  SESSION_START_EPOCH=$(date +%s)
12916
12965
 
12966
+ # First-run disclosure (shown once, before any work; best-effort).
12967
+ if type loki_show_disclosure_once &>/dev/null; then
12968
+ loki_show_disclosure_once
12969
+ fi
12970
+
12917
12971
  echo ""
12918
12972
  echo -e "${BOLD}${BLUE}"
12919
12973
  echo " ██╗ ██████╗ ██╗ ██╗██╗ ███╗ ███╗ ██████╗ ██████╗ ███████╗"
@@ -7,8 +7,19 @@ LOKI_POSTHOG_HOST="${LOKI_TELEMETRY_ENDPOINT:-https://us.i.posthog.com}"
7
7
  LOKI_POSTHOG_KEY="phc_ya0vGBru41AJWtGNfZZ8H9W4yjoZy4KON0nnayS7s87"
8
8
 
9
9
  _loki_telemetry_enabled() {
10
+ # Unified opt-out: these checks must mirror loki_collection_enabled in
11
+ # autonomy/crash.sh so one switch gates BOTH PostHog usage telemetry and
12
+ # crash reporting.
13
+ # LOKI_TELEMETRY=off (case-insensitive)
14
+ local _telem_lower
15
+ _telem_lower="$(printf '%s' "${LOKI_TELEMETRY:-}" | tr '[:upper:]' '[:lower:]')"
16
+ [ "$_telem_lower" = "off" ] && return 1
10
17
  [ "${LOKI_TELEMETRY_DISABLED:-}" = "true" ] && return 1
11
18
  [ "${DO_NOT_TRACK:-}" = "1" ] && return 1
19
+ # Persistent opt-out in ~/.loki/config
20
+ if [ -f "${HOME}/.loki/config" ] && grep -q "^TELEMETRY_DISABLED=true" "${HOME}/.loki/config" 2>/dev/null; then
21
+ return 1
22
+ fi
12
23
  command -v curl >/dev/null 2>&1 || return 1
13
24
  return 0
14
25
  }
package/bin/loki CHANGED
@@ -116,11 +116,13 @@ fi
116
116
  # Two-token routes (provider show/list, memory list/index) match on the first
117
117
  # token only; the Bun dispatcher handles subcommand routing internally.
118
118
  case "${1:-}" in
119
- version|--version|-v|status|stats|doctor|provider|memory|rollback|internal|kpis|trust|proof|wiki)
119
+ version|--version|-v|status|stats|doctor|provider|memory|rollback|internal|kpis|trust|proof|wiki|crash)
120
120
  # v7.5.2: rollback added (wires loki-ts/src/commands/rollback.ts).
121
121
  # v7.5.3: internal added for autonomy/run.sh phase1-hooks calls.
122
122
  # v7.5.28: kpis added (Phase K MVP: read-only KPI snapshot).
123
123
  # R4: trust added (visible trust trajectory; Bun route, bash fallback).
124
+ # crash added (Crash Reporting Phase 0: inspect/manually submit local
125
+ # scrubbed crash reports; Bun route, bash fallback).
124
126
  #
125
127
  # v7.8.2: emit the cli_command product-analytics event for Bun-routed
126
128
  # commands. The bash CLI fires this from autonomy/loki main(), but the
@@ -180,8 +180,22 @@ console.log('New here? Run `loki welcome` for a 30-second tour.');
180
180
  console.log('');
181
181
 
182
182
  // Anonymous install telemetry (fire-and-forget, silent)
183
+ // Unified opt-out: these checks mirror loki_collection_enabled in
184
+ // autonomy/crash.sh so one switch gates BOTH PostHog usage telemetry and
185
+ // crash reporting.
186
+ function _lokiCollectionDisabled() {
187
+ if ((process.env.LOKI_TELEMETRY || '').toLowerCase() === 'off') return true;
188
+ if (process.env.LOKI_TELEMETRY_DISABLED === 'true') return true;
189
+ if (process.env.DO_NOT_TRACK === '1') return true;
190
+ try {
191
+ const cfg = path.join(homeDir, '.loki', 'config');
192
+ const lines = fs.readFileSync(cfg, 'utf8').split('\n');
193
+ if (lines.some((l) => l.startsWith('TELEMETRY_DISABLED=true'))) return true;
194
+ } catch {}
195
+ return false;
196
+ }
183
197
  try {
184
- if (process.env.LOKI_TELEMETRY_DISABLED !== 'true' && process.env.DO_NOT_TRACK !== '1') {
198
+ if (!_lokiCollectionDisabled()) {
185
199
  const https = require('https');
186
200
  const crypto = require('crypto');
187
201
  const idFile = path.join(homeDir, '.loki-telemetry-id');
@@ -7,7 +7,7 @@ Modules:
7
7
  control: Session control API (start/stop/pause/resume)
8
8
  """
9
9
 
10
- __version__ = "7.18.0"
10
+ __version__ = "7.18.2"
11
11
 
12
12
  # Expose the control app for easy import
13
13
  try:
@@ -19,10 +19,25 @@ _POSTHOG_KEY = "phc_ya0vGBru41AJWtGNfZZ8H9W4yjoZy4KON0nnayS7s87"
19
19
 
20
20
 
21
21
  def _is_enabled():
22
+ # Unified opt-out: these checks must mirror loki_collection_enabled in
23
+ # autonomy/crash.sh so one switch gates BOTH PostHog usage telemetry and
24
+ # crash reporting.
25
+ if os.environ.get("LOKI_TELEMETRY", "").lower() == "off":
26
+ return False
22
27
  if os.environ.get("LOKI_TELEMETRY_DISABLED") == "true":
23
28
  return False
24
29
  if os.environ.get("DO_NOT_TRACK") == "1":
25
30
  return False
31
+ # Persistent opt-out in ~/.loki/config (matches the bash grep prefix
32
+ # semantics: any line beginning with TELEMETRY_DISABLED=true).
33
+ try:
34
+ config_path = Path.home() / ".loki" / "config"
35
+ if config_path.is_file():
36
+ for line in config_path.read_text().splitlines():
37
+ if line.startswith("TELEMETRY_DISABLED=true"):
38
+ return False
39
+ except Exception:
40
+ pass
26
41
  return True
27
42
 
28
43