loki-mode 6.6.0 → 6.7.0

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
@@ -428,6 +428,9 @@ show_help() {
428
428
  echo " doctor [--json] Check system prerequisites and skill symlinks"
429
429
  echo " setup-skill Create skill symlinks for all providers"
430
430
  echo " watchdog [cmd] Process health monitoring (status|help)"
431
+ echo " telemetry [cmd] OpenTelemetry management (status|enable|disable)"
432
+ echo " worktree [cmd] Parallel worktree management (list|merge|clean|status)"
433
+ echo " agent [cmd] Agent type dispatch (list|info|run|start|review)"
431
434
  echo " remote [PRD] Start remote session (connect from phone/browser, Claude Pro/Max)"
432
435
  echo " version Show version"
433
436
  echo " help Show this help"
@@ -1633,7 +1636,7 @@ cmd_provider() {
1633
1636
 
1634
1637
  case "$subcommand" in
1635
1638
  show|current)
1636
- cmd_provider_show
1639
+ cmd_provider_show "$@"
1637
1640
  ;;
1638
1641
  set)
1639
1642
  cmd_provider_set "$@"
@@ -4598,6 +4601,55 @@ cmd_doctor() {
4598
4601
  done
4599
4602
  echo ""
4600
4603
 
4604
+ echo -e "${CYAN}Integrations:${NC}"
4605
+ # MCP SDK check
4606
+ if python3 -c "import mcp" 2>/dev/null; then
4607
+ echo -e " ${GREEN}PASS${NC} MCP SDK (Python)"
4608
+ pass_count=$((pass_count + 1))
4609
+ else
4610
+ echo -e " ${YELLOW}WARN${NC} MCP SDK - not installed (pip3 install mcp)"
4611
+ warn_count=$((warn_count + 1))
4612
+ fi
4613
+ # Detect best Python for ML packages (3.12 preferred, 3.14+ has compat issues)
4614
+ local _py_ml="python3"
4615
+ if command -v /opt/homebrew/bin/python3.12 &>/dev/null; then
4616
+ _py_ml="/opt/homebrew/bin/python3.12"
4617
+ elif command -v python3.12 &>/dev/null; then
4618
+ _py_ml="python3.12"
4619
+ fi
4620
+ # Vector search dependencies
4621
+ if $_py_ml -c "import numpy" 2>/dev/null; then
4622
+ echo -e " ${GREEN}PASS${NC} numpy (vector search)"
4623
+ pass_count=$((pass_count + 1))
4624
+ else
4625
+ echo -e " ${YELLOW}WARN${NC} numpy - not installed (pip3 install numpy)"
4626
+ warn_count=$((warn_count + 1))
4627
+ fi
4628
+ if $_py_ml -c "import sentence_transformers" 2>/dev/null; then
4629
+ echo -e " ${GREEN}PASS${NC} sentence-transformers (embeddings)"
4630
+ pass_count=$((pass_count + 1))
4631
+ else
4632
+ echo -e " ${YELLOW}WARN${NC} sentence-transformers - not installed (loki memory vectors setup)"
4633
+ warn_count=$((warn_count + 1))
4634
+ fi
4635
+ # ChromaDB check
4636
+ if curl -sf http://localhost:8100/api/v2/heartbeat >/dev/null 2>&1; then
4637
+ echo -e " ${GREEN}PASS${NC} ChromaDB server (port 8100)"
4638
+ pass_count=$((pass_count + 1))
4639
+ else
4640
+ echo -e " ${YELLOW}WARN${NC} ChromaDB - not running (docker start loki-chroma)"
4641
+ warn_count=$((warn_count + 1))
4642
+ fi
4643
+ # OTEL status
4644
+ if [ -n "${LOKI_OTEL_ENDPOINT:-}" ]; then
4645
+ echo -e " ${GREEN}PASS${NC} OTEL endpoint: $LOKI_OTEL_ENDPOINT"
4646
+ pass_count=$((pass_count + 1))
4647
+ else
4648
+ echo -e " ${YELLOW}WARN${NC} OTEL - not configured (set LOKI_OTEL_ENDPOINT)"
4649
+ warn_count=$((warn_count + 1))
4650
+ fi
4651
+ echo ""
4652
+
4601
4653
  echo -e "${CYAN}System:${NC}"
4602
4654
  doctor_check "bash (>= 4.0)" bash recommended 4.0 || true
4603
4655
 
@@ -7657,6 +7709,12 @@ main() {
7657
7709
  cluster)
7658
7710
  cmd_cluster "$@"
7659
7711
  ;;
7712
+ worktree|wt)
7713
+ cmd_worktree "$@"
7714
+ ;;
7715
+ agent)
7716
+ cmd_agent "$@"
7717
+ ;;
7660
7718
  state)
7661
7719
  cmd_state "$@"
7662
7720
  ;;
@@ -7666,6 +7724,9 @@ main() {
7666
7724
  syslog)
7667
7725
  cmd_syslog "$@"
7668
7726
  ;;
7727
+ telemetry|otel)
7728
+ cmd_telemetry "$@"
7729
+ ;;
7669
7730
  remote|rc)
7670
7731
  cmd_remote "$@"
7671
7732
  ;;
@@ -7686,6 +7747,166 @@ main() {
7686
7747
  esac
7687
7748
  }
7688
7749
 
7750
+ # Worktree management (v6.7.0)
7751
+ cmd_worktree() {
7752
+ local subcommand="${1:-list}"
7753
+ shift 2>/dev/null || true
7754
+
7755
+ case "$subcommand" in
7756
+ list)
7757
+ echo -e "${BOLD}Worktree Status${NC}"
7758
+ echo ""
7759
+
7760
+ # List git worktrees
7761
+ local worktrees
7762
+ worktrees=$(git worktree list --porcelain 2>/dev/null)
7763
+ if [ -z "$worktrees" ]; then
7764
+ echo " No active worktrees"
7765
+ return 0
7766
+ fi
7767
+
7768
+ local count=0
7769
+ local current_wt=""
7770
+ while IFS= read -r line; do
7771
+ case "$line" in
7772
+ "worktree "*)
7773
+ current_wt="${line#worktree }"
7774
+ ;;
7775
+ "branch "*)
7776
+ local branch="${line#branch refs/heads/}"
7777
+ local status="running"
7778
+ local stream_name=""
7779
+
7780
+ # Check for merge signal
7781
+ if [[ "$branch" == loki-parallel-* ]]; then
7782
+ stream_name="${branch#loki-parallel-}"
7783
+ if [ -f ".loki/signals/MERGE_REQUESTED_${stream_name}" ]; then
7784
+ status="merge-ready"
7785
+ elif [ -f ".loki/signals/WORKTREE_FAILED_${stream_name}" ]; then
7786
+ status="failed"
7787
+ fi
7788
+ fi
7789
+
7790
+ # Skip main worktree
7791
+ if [ "$current_wt" != "$(git rev-parse --show-toplevel 2>/dev/null)" ]; then
7792
+ case "$status" in
7793
+ merge-ready) echo -e " ${GREEN}[merge-ready]${NC} $branch ${DIM}$current_wt${NC}" ;;
7794
+ failed) echo -e " ${RED}[failed]${NC} $branch ${DIM}$current_wt${NC}" ;;
7795
+ *) echo -e " ${CYAN}[active]${NC} $branch ${DIM}$current_wt${NC}" ;;
7796
+ esac
7797
+ count=$((count + 1))
7798
+ fi
7799
+ current_wt=""
7800
+ ;;
7801
+ esac
7802
+ done <<< "$worktrees"
7803
+
7804
+ if [ "$count" -eq 0 ]; then
7805
+ echo " No parallel worktrees (only main)"
7806
+ fi
7807
+ echo ""
7808
+ echo " Total: $count worktree(s)"
7809
+ ;;
7810
+
7811
+ merge)
7812
+ local name="${1:-}"
7813
+ if [ -z "$name" ]; then
7814
+ echo -e "${RED}Usage: loki worktree merge <name>${NC}"
7815
+ return 1
7816
+ fi
7817
+
7818
+ echo -e "${BOLD}Merging worktree: $name${NC}"
7819
+ local signal_file=".loki/signals/MERGE_REQUESTED_${name}"
7820
+ if [ ! -f "$signal_file" ]; then
7821
+ echo -e "${RED}No merge signal found for: $name${NC}"
7822
+ echo "Use 'loki worktree list' to see available worktrees."
7823
+ return 1
7824
+ fi
7825
+
7826
+ local branch
7827
+ branch=$(python3 -c "import json; print(json.load(open('$signal_file'))['branch'])" 2>/dev/null)
7828
+ local worktree_path
7829
+ worktree_path=$(python3 -c "import json; print(json.load(open('$signal_file'))['worktree'])" 2>/dev/null)
7830
+
7831
+ if git merge --no-ff "$branch" -m "merge($name): auto-merge from parallel worktree"; then
7832
+ echo -e "${GREEN}Merge successful${NC}"
7833
+ rm -f "$signal_file"
7834
+ if [ -n "$worktree_path" ] && [ -d "$worktree_path" ]; then
7835
+ git worktree remove "$worktree_path" --force 2>/dev/null || true
7836
+ git branch -d "$branch" 2>/dev/null || true
7837
+ fi
7838
+ else
7839
+ echo -e "${RED}Merge conflict - resolve manually${NC}"
7840
+ git merge --abort 2>/dev/null || true
7841
+ return 1
7842
+ fi
7843
+ ;;
7844
+
7845
+ clean)
7846
+ echo -e "${BOLD}Cleaning all worktrees...${NC}"
7847
+ local removed=0
7848
+ local main_wt
7849
+ main_wt=$(git rev-parse --show-toplevel 2>/dev/null || echo "")
7850
+ while read -r line; do
7851
+ local wt="${line#worktree }"
7852
+ if [ -n "$wt" ] && [ "$wt" != "$main_wt" ]; then
7853
+ echo " Removing: $wt"
7854
+ git worktree remove "$wt" --force 2>/dev/null || true
7855
+ removed=$((removed + 1))
7856
+ fi
7857
+ done < <(git worktree list --porcelain 2>/dev/null | grep "^worktree ")
7858
+ rm -f .loki/signals/MERGE_REQUESTED_* .loki/signals/WORKTREE_FAILED_* 2>/dev/null || true
7859
+ echo -e "${GREEN}Cleanup complete ($removed worktrees removed)${NC}"
7860
+ ;;
7861
+
7862
+ status)
7863
+ echo -e "${BOLD}Parallel Orchestrator Status${NC}"
7864
+ echo ""
7865
+
7866
+ local merge_ready=0
7867
+ local failed=0
7868
+ local active=0
7869
+
7870
+ for f in .loki/signals/MERGE_REQUESTED_*; do
7871
+ [ -f "$f" ] && merge_ready=$((merge_ready + 1))
7872
+ done
7873
+ for f in .loki/signals/WORKTREE_FAILED_*; do
7874
+ [ -f "$f" ] && failed=$((failed + 1))
7875
+ done
7876
+
7877
+ local total_wt
7878
+ total_wt=$(git worktree list 2>/dev/null | wc -l | tr -d ' ')
7879
+ total_wt=$((total_wt - 1)) # Exclude main
7880
+ [ "$total_wt" -lt 0 ] && total_wt=0
7881
+ active=$((total_wt - merge_ready - failed))
7882
+ [ "$active" -lt 0 ] && active=0
7883
+
7884
+ echo " Active worktrees: $active"
7885
+ echo " Merge-ready: $merge_ready"
7886
+ echo " Failed: $failed"
7887
+ echo " Total: $total_wt"
7888
+ ;;
7889
+
7890
+ --help|-h|help)
7891
+ echo -e "${BOLD}loki worktree${NC} - Manage parallel worktrees (v6.7.0)"
7892
+ echo ""
7893
+ echo "Usage: loki worktree <command>"
7894
+ echo ""
7895
+ echo "Commands:"
7896
+ echo " list List active worktrees with status"
7897
+ echo " merge NAME Merge a completed worktree back"
7898
+ echo " clean Remove all worktrees"
7899
+ echo " status Show parallel orchestrator state"
7900
+ echo ""
7901
+ ;;
7902
+ *)
7903
+ echo -e "${RED}Unknown worktree command: $subcommand${NC}"
7904
+ echo "Run 'loki worktree help' for usage."
7905
+ return 1
7906
+ ;;
7907
+ esac
7908
+ }
7909
+
7689
7910
  # SQLite queryable state inspection
7690
7911
  cmd_state() {
7691
7912
  local subcmd="${1:-help}"
@@ -7967,6 +8188,126 @@ print()
7967
8188
  cmd_audit scan "$@"
7968
8189
  return
7969
8190
  ;;
8191
+ lint)
8192
+ echo -e "${BOLD}Running static analysis...${NC}"
8193
+ echo ""
8194
+ local findings=0
8195
+ local target="${2:-.}"
8196
+
8197
+ # JavaScript/TypeScript
8198
+ if [ -f "$target/package.json" ]; then
8199
+ echo -e "${CYAN}JavaScript/TypeScript:${NC}"
8200
+ local js_files
8201
+ js_files=$(find "$target" -name "*.js" -o -name "*.ts" -o -name "*.jsx" -o -name "*.tsx" | grep -v node_modules | grep -v dist | head -50)
8202
+ if [ -n "$js_files" ]; then
8203
+ if [ -f "$target/.eslintrc.js" ] || [ -f "$target/.eslintrc.json" ] || [ -f "$target/.eslintrc.yml" ] || [ -f "$target/eslint.config.js" ] || [ -f "$target/eslint.config.mjs" ]; then
8204
+ npx eslint $js_files 2>&1 | head -50
8205
+ [ ${PIPESTATUS[0]} -ne 0 ] && findings=$((findings + 1))
8206
+ else
8207
+ echo " No ESLint config found - running syntax check"
8208
+ for f in $js_files; do
8209
+ node --check "$f" 2>&1 || findings=$((findings + 1))
8210
+ done
8211
+ fi
8212
+ fi
8213
+ fi
8214
+
8215
+ # Python
8216
+ if [ -f "$target/setup.py" ] || [ -f "$target/pyproject.toml" ] || [ -f "$target/requirements.txt" ]; then
8217
+ echo -e "${CYAN}Python:${NC}"
8218
+ local py_files
8219
+ py_files=$(find "$target" -name "*.py" | grep -v __pycache__ | grep -v .venv | grep -v venv | head -50)
8220
+ if [ -n "$py_files" ]; then
8221
+ if command -v ruff &>/dev/null; then
8222
+ ruff check $py_files 2>&1 | head -50
8223
+ [ ${PIPESTATUS[0]} -ne 0 ] && findings=$((findings + 1))
8224
+ else
8225
+ echo " ruff not available - running py_compile"
8226
+ for f in $py_files; do
8227
+ python3 -m py_compile "$f" 2>&1 || findings=$((findings + 1))
8228
+ done
8229
+ fi
8230
+ fi
8231
+ fi
8232
+
8233
+ # Shell scripts
8234
+ local sh_files
8235
+ sh_files=$(find "$target" -name "*.sh" | grep -v node_modules | head -20)
8236
+ if [ -n "$sh_files" ]; then
8237
+ echo -e "${CYAN}Shell:${NC}"
8238
+ if command -v shellcheck &>/dev/null; then
8239
+ shellcheck $sh_files 2>&1 | head -50
8240
+ [ ${PIPESTATUS[0]} -ne 0 ] && findings=$((findings + 1))
8241
+ else
8242
+ echo " shellcheck not available - running bash -n"
8243
+ for f in $sh_files; do
8244
+ bash -n "$f" 2>&1 || findings=$((findings + 1))
8245
+ done
8246
+ fi
8247
+ fi
8248
+
8249
+ echo ""
8250
+ if [ "$findings" -gt 0 ]; then
8251
+ echo -e "${RED}Static analysis found issues ($findings checks failed)${NC}"
8252
+ return 1
8253
+ else
8254
+ echo -e "${GREEN}Static analysis passed${NC}"
8255
+ return 0
8256
+ fi
8257
+ ;;
8258
+
8259
+ test)
8260
+ echo -e "${BOLD}Running test coverage check...${NC}"
8261
+ echo ""
8262
+ local target="${2:-.}"
8263
+ local min_coverage="${LOKI_MIN_COVERAGE:-80}"
8264
+ local test_failed=0
8265
+
8266
+ # JavaScript/TypeScript (jest/vitest)
8267
+ if [ -f "$target/package.json" ]; then
8268
+ local test_cmd=""
8269
+ if grep -q '"vitest"' "$target/package.json" 2>/dev/null; then
8270
+ test_cmd="npx vitest run --coverage"
8271
+ elif grep -q '"jest"' "$target/package.json" 2>/dev/null; then
8272
+ test_cmd="npx jest --coverage --passWithNoTests"
8273
+ fi
8274
+ if [ -n "$test_cmd" ]; then
8275
+ echo -e "${CYAN}Running: $test_cmd${NC}"
8276
+ (cd "$target" && eval "$test_cmd") 2>&1
8277
+ [ $? -ne 0 ] && test_failed=1
8278
+ fi
8279
+ fi
8280
+
8281
+ # Python (pytest)
8282
+ if [ -f "$target/setup.py" ] || [ -f "$target/pyproject.toml" ]; then
8283
+ if command -v pytest &>/dev/null; then
8284
+ echo -e "${CYAN}Running: pytest --cov${NC}"
8285
+ (cd "$target" && pytest --cov 2>&1) || test_failed=1
8286
+ fi
8287
+ fi
8288
+
8289
+ # Go
8290
+ if [ -f "$target/go.mod" ]; then
8291
+ echo -e "${CYAN}Running: go test -cover${NC}"
8292
+ (cd "$target" && go test -cover ./... 2>&1) || test_failed=1
8293
+ fi
8294
+
8295
+ # Rust
8296
+ if [ -f "$target/Cargo.toml" ]; then
8297
+ echo -e "${CYAN}Running: cargo test${NC}"
8298
+ (cd "$target" && cargo test 2>&1) || test_failed=1
8299
+ fi
8300
+
8301
+ echo ""
8302
+ if [ "$test_failed" -ne 0 ]; then
8303
+ echo -e "${RED}Test coverage gate FAILED${NC}"
8304
+ return 1
8305
+ else
8306
+ echo -e "${GREEN}Tests passed (min coverage: ${min_coverage}%)${NC}"
8307
+ return 0
8308
+ fi
8309
+ ;;
8310
+
7970
8311
  --help|-h|help)
7971
8312
  echo -e "${BOLD}loki audit${NC} - Agent audit log and quality scanning"
7972
8313
  echo ""
@@ -7976,6 +8317,8 @@ print()
7976
8317
  echo " log [N] Show last N audit log entries (default: 50)"
7977
8318
  echo " count Count actions by type"
7978
8319
  echo " scan Run quality scan against dashboard API"
8320
+ echo " lint Run static analysis on project files"
8321
+ echo " test Run test coverage check"
7979
8322
  echo " help Show this help"
7980
8323
  echo ""
7981
8324
  echo "Quality Scan Options (loki audit scan):"
@@ -8227,33 +8570,97 @@ if count == 0:
8227
8570
  local query="${2:-}"
8228
8571
  if [ -z "$query" ]; then
8229
8572
  echo -e "${RED}Usage: loki memory search <query>${NC}"
8230
- exit 1
8573
+ return 1
8231
8574
  fi
8232
8575
 
8233
- echo -e "${BOLD}Search Results for: $query${NC}"
8234
- echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
8576
+ echo -e "${BOLD}Memory Search: $query${NC}"
8577
+ echo ""
8235
8578
 
8236
- LOKI_SEARCH_QUERY="$query" LOKI_LEARNINGS_DIR="$learnings_dir" python3 -c "
8237
- import json, os
8579
+ # Try vector search first, fall back to keyword
8580
+ python3 << PYEOF
8581
+ import os, json, sys, glob
8238
8582
 
8239
- learnings_dir = os.environ['LOKI_LEARNINGS_DIR']
8240
- query = os.environ['LOKI_SEARCH_QUERY'].lower()
8583
+ query = """$query"""
8584
+ results = []
8241
8585
 
8242
- for filename in ['patterns.jsonl', 'mistakes.jsonl', 'successes.jsonl']:
8243
- filepath = os.path.join(learnings_dir, filename)
8244
- if not os.path.exists(filepath):
8586
+ # Keyword search across all memory files
8587
+ memory_dirs = [
8588
+ '.loki/memory/episodic',
8589
+ '.loki/memory/semantic',
8590
+ '.loki/memory/skills',
8591
+ '.loki/memory/handoffs',
8592
+ ]
8593
+
8594
+ for mdir in memory_dirs:
8595
+ if not os.path.isdir(mdir):
8245
8596
  continue
8597
+ for fpath in glob.glob(os.path.join(mdir, '*.json')):
8598
+ try:
8599
+ with open(fpath) as f:
8600
+ data = json.load(f)
8601
+ text = json.dumps(data).lower()
8602
+ if query.lower() in text:
8603
+ # Score by number of matches
8604
+ score = text.count(query.lower())
8605
+ title = data.get('title', data.get('task_id', data.get('name', os.path.basename(fpath))))
8606
+ category = os.path.basename(mdir)
8607
+ results.append((score, category, title, fpath))
8608
+ except Exception:
8609
+ continue
8246
8610
 
8247
- category = filename.replace('.jsonl', '')
8248
- with open(filepath, 'r') as f:
8249
- for line in f:
8250
- try:
8251
- e = json.loads(line)
8252
- desc = e.get('description', '').lower()
8253
- if query in desc:
8254
- print(f\"[{category}] [{e.get('project', 'unknown')}] {e['description'][:80]}...\")
8255
- except: pass
8256
- " 2>/dev/null
8611
+ # Also search markdown files
8612
+ for mdir in ['.loki/memory', '.loki']:
8613
+ if not os.path.isdir(mdir):
8614
+ continue
8615
+ for fpath in glob.glob(os.path.join(mdir, '*.md')):
8616
+ try:
8617
+ with open(fpath) as f:
8618
+ text = f.read().lower()
8619
+ if query.lower() in text:
8620
+ score = text.count(query.lower())
8621
+ title = os.path.basename(fpath)
8622
+ results.append((score, 'docs', title, fpath))
8623
+ except Exception:
8624
+ continue
8625
+
8626
+ # Try vector search if available
8627
+ vector_results = []
8628
+ try:
8629
+ sys.path.insert(0, '.')
8630
+ from memory.retrieval import MemoryRetrieval
8631
+ from memory.storage import MemoryStorage
8632
+ storage = MemoryStorage('.loki/memory')
8633
+ retriever = MemoryRetrieval(storage)
8634
+ vr = retriever.search(query, top_k=5)
8635
+ for item in vr:
8636
+ vector_results.append(item)
8637
+ if vector_results:
8638
+ print(" [Vector Search Results]")
8639
+ for item in vector_results[:5]:
8640
+ if isinstance(item, dict):
8641
+ print(f" {item.get('title', item.get('id', '?'))}: {item.get('summary', '')[:80]}")
8642
+ else:
8643
+ print(f" {str(item)[:80]}")
8644
+ print()
8645
+ except ImportError:
8646
+ pass # Vector search not available
8647
+ except Exception:
8648
+ pass
8649
+
8650
+ # Show keyword results
8651
+ if results:
8652
+ results.sort(key=lambda x: -x[0])
8653
+ print(" [Keyword Search Results]")
8654
+ for score, category, title, fpath in results[:10]:
8655
+ print(f" [{category}] {title} ({score} matches)")
8656
+ print(f" {fpath}")
8657
+ print()
8658
+ print(f" Found: {len(results)} result(s)")
8659
+ else:
8660
+ if not vector_results:
8661
+ print(" No results found for: " + query)
8662
+ print(" Tip: Try broader search terms")
8663
+ PYEOF
8257
8664
  ;;
8258
8665
 
8259
8666
  clear)
@@ -8611,10 +9018,96 @@ except Exception as e:
8611
9018
  ;;
8612
9019
 
8613
9020
  vectors)
8614
- # Manage vector indices
8615
- if [ "${2:-}" = "rebuild" ]; then
8616
- python3 -c "
9021
+ local vec_cmd="${2:-stats}"
9022
+ case "$vec_cmd" in
9023
+ setup)
9024
+ echo -e "${BOLD}Vector Search Setup${NC}"
9025
+ echo ""
9026
+
9027
+ # Detect best Python for ML packages (3.12 preferred, 3.14+ has compat issues)
9028
+ local py_ml="python3"
9029
+ local pip_ml="pip3"
9030
+ if command -v /opt/homebrew/bin/python3.12 &>/dev/null; then
9031
+ py_ml="/opt/homebrew/bin/python3.12"
9032
+ pip_ml="/opt/homebrew/bin/pip3.12"
9033
+ echo -e " Using Python 3.12 for ML packages (${DIM}system python3 may be 3.14+${NC})"
9034
+ elif command -v python3.12 &>/dev/null; then
9035
+ py_ml="python3.12"
9036
+ pip_ml="pip3.12"
9037
+ echo -e " Using Python 3.12 for ML packages"
9038
+ else
9039
+ local py_ver
9040
+ py_ver=$(python3 -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')" 2>/dev/null || echo "0.0")
9041
+ local py_minor
9042
+ py_minor=$(echo "$py_ver" | cut -d. -f2)
9043
+ if [ "${py_minor:-0}" -ge 13 ] 2>/dev/null; then
9044
+ echo -e " ${YELLOW}WARNING: Python $py_ver detected. sentence-transformers may not install.${NC}"
9045
+ echo -e " ${YELLOW}Install Python 3.12 for best compatibility: brew install python@3.12${NC}"
9046
+ fi
9047
+ fi
9048
+
9049
+ # Check/install numpy
9050
+ echo -n " Checking numpy... "
9051
+ if $py_ml -c "import numpy" 2>/dev/null; then
9052
+ echo -e "${GREEN}installed${NC}"
9053
+ else
9054
+ echo -n "installing... "
9055
+ if $pip_ml install numpy 2>/dev/null || $pip_ml install --break-system-packages numpy 2>/dev/null; then
9056
+ echo -e "${GREEN}done${NC}"
9057
+ else
9058
+ echo -e "${RED}failed${NC}"
9059
+ echo " Try: $pip_ml install numpy"
9060
+ fi
9061
+ fi
9062
+
9063
+ # Check/install sentence-transformers
9064
+ echo -n " Checking sentence-transformers... "
9065
+ if $py_ml -c "import sentence_transformers" 2>/dev/null; then
9066
+ echo -e "${GREEN}installed${NC}"
9067
+ else
9068
+ echo -n "installing (this may take a while)... "
9069
+ if $pip_ml install sentence-transformers 2>/dev/null || $pip_ml install --break-system-packages sentence-transformers 2>/dev/null; then
9070
+ echo -e "${GREEN}done${NC}"
9071
+ else
9072
+ echo -e "${RED}failed${NC}"
9073
+ echo " Try: $pip_ml install sentence-transformers"
9074
+ fi
9075
+ fi
9076
+
9077
+ # Create vector directory
9078
+ mkdir -p .loki/memory/vectors
9079
+ echo -e " Vector directory: ${GREEN}.loki/memory/vectors/${NC}"
9080
+
9081
+ # Build initial index if memory files exist
9082
+ echo ""
9083
+ echo -n " Building vector indices... "
9084
+ python3 -c "
9085
+ try:
9086
+ import sys
9087
+ sys.path.insert(0, '.')
9088
+ from memory.retrieval import MemoryRetrieval
9089
+ from memory.storage import MemoryStorage
9090
+ storage = MemoryStorage('.loki/memory')
9091
+ retriever = MemoryRetrieval(storage)
9092
+ retriever.build_indices()
9093
+ retriever.save_indices()
9094
+ print('done')
9095
+ except ImportError as e:
9096
+ print(f'skipped (missing: {e})')
9097
+ except Exception as e:
9098
+ print(f'error: {e}')
9099
+ " 2>/dev/null || echo "skipped"
9100
+
9101
+ echo ""
9102
+ echo -e "${GREEN}Vector search setup complete${NC}"
9103
+ echo "Run 'loki memory search <query>' to search."
9104
+ ;;
9105
+
9106
+ rebuild)
9107
+ python3 -c "
8617
9108
  try:
9109
+ import sys
9110
+ sys.path.insert(0, '.')
8618
9111
  from memory.retrieval import MemoryRetrieval
8619
9112
  from memory.storage import MemoryStorage
8620
9113
  storage = MemoryStorage('.loki/memory')
@@ -8627,19 +9120,23 @@ except ImportError as e:
8627
9120
  except Exception as e:
8628
9121
  print(f'Error: {e}')
8629
9122
  " 2>/dev/null
8630
- else
8631
- echo "Vector index stats:"
8632
- if ls -1 .loki/memory/vectors/*.npz 2>/dev/null | head -1 >/dev/null 2>&1; then
8633
- for f in .loki/memory/vectors/*.npz; do
8634
- if [ -f "$f" ]; then
8635
- count=$(python3 -c "import numpy as np; d=np.load('$f'); print(len(d['ids']))" 2>/dev/null || echo "error")
8636
- echo " $(basename "$f"): $count vectors"
8637
- fi
8638
- done
8639
- else
8640
- echo " No vector indices found"
8641
- fi
8642
- fi
9123
+ ;;
9124
+
9125
+ stats|*)
9126
+ echo "Vector index stats:"
9127
+ if ls -1 .loki/memory/vectors/*.npz 2>/dev/null | head -1 >/dev/null 2>&1; then
9128
+ for f in .loki/memory/vectors/*.npz; do
9129
+ if [ -f "$f" ]; then
9130
+ count=$(python3 -c "import numpy as np; d=np.load('$f'); print(len(d['ids']))" 2>/dev/null || echo "error")
9131
+ echo " $(basename "$f"): $count vectors"
9132
+ fi
9133
+ done
9134
+ else
9135
+ echo " No vector indices found"
9136
+ echo " Run 'loki memory vectors setup' to initialize"
9137
+ fi
9138
+ ;;
9139
+ esac
8643
9140
  ;;
8644
9141
 
8645
9142
  namespace|ns)
@@ -10630,6 +11127,113 @@ for line in sys.stdin:
10630
11127
  esac
10631
11128
  }
10632
11129
 
11130
+ # Telemetry management (v6.7.0)
11131
+ cmd_telemetry() {
11132
+ local subcommand="${1:-status}"
11133
+ shift 2>/dev/null || true
11134
+
11135
+ case "$subcommand" in
11136
+ status)
11137
+ echo -e "${BOLD}Telemetry Status${NC}"
11138
+ echo ""
11139
+
11140
+ local endpoint="${LOKI_OTEL_ENDPOINT:-}"
11141
+ if [ -n "$endpoint" ]; then
11142
+ echo -e " Endpoint: ${GREEN}$endpoint${NC}"
11143
+ else
11144
+ echo -e " Endpoint: ${YELLOW}not configured${NC}"
11145
+ fi
11146
+
11147
+ # Check SDK mode
11148
+ local sdk_mode="unknown"
11149
+ if command -v node &>/dev/null; then
11150
+ sdk_mode=$(node -e "
11151
+ try {
11152
+ require('@opentelemetry/sdk-trace-node');
11153
+ console.log('real-sdk');
11154
+ } catch(_) {
11155
+ console.log('custom-exporter');
11156
+ }
11157
+ " 2>/dev/null || echo "unavailable")
11158
+ fi
11159
+ echo -e " SDK mode: $sdk_mode"
11160
+
11161
+ # Check config file
11162
+ local config_file=".loki/config.json"
11163
+ if [ -f "$config_file" ]; then
11164
+ local saved_endpoint
11165
+ saved_endpoint=$(python3 -c "import json; print(json.load(open('$config_file')).get('otel_endpoint',''))" 2>/dev/null || echo "")
11166
+ if [ -n "$saved_endpoint" ]; then
11167
+ echo -e " Saved: $saved_endpoint"
11168
+ fi
11169
+ fi
11170
+ echo ""
11171
+ ;;
11172
+
11173
+ enable)
11174
+ local endpoint="${1:-http://localhost:4318}"
11175
+ echo -e "${BOLD}Enabling telemetry${NC}"
11176
+
11177
+ # Save to config
11178
+ mkdir -p .loki
11179
+ local config_file=".loki/config.json"
11180
+ if [ -f "$config_file" ]; then
11181
+ python3 -c "
11182
+ import json
11183
+ with open('$config_file') as f:
11184
+ config = json.load(f)
11185
+ config['otel_endpoint'] = '$endpoint'
11186
+ with open('$config_file', 'w') as f:
11187
+ json.dump(config, f, indent=2)
11188
+ " 2>/dev/null
11189
+ else
11190
+ echo "{\"otel_endpoint\": \"$endpoint\"}" > "$config_file"
11191
+ fi
11192
+
11193
+ echo -e " Endpoint set to: ${GREEN}$endpoint${NC}"
11194
+ echo ""
11195
+ echo " Set LOKI_OTEL_ENDPOINT=$endpoint in your shell for immediate use."
11196
+ echo " Or restart loki to pick up the saved config."
11197
+ ;;
11198
+
11199
+ disable)
11200
+ echo -e "${BOLD}Disabling telemetry${NC}"
11201
+
11202
+ local config_file=".loki/config.json"
11203
+ if [ -f "$config_file" ]; then
11204
+ python3 -c "
11205
+ import json
11206
+ with open('$config_file') as f:
11207
+ config = json.load(f)
11208
+ config.pop('otel_endpoint', None)
11209
+ with open('$config_file', 'w') as f:
11210
+ json.dump(config, f, indent=2)
11211
+ " 2>/dev/null
11212
+ fi
11213
+
11214
+ echo -e " Telemetry disabled"
11215
+ echo " Unset LOKI_OTEL_ENDPOINT in your shell for immediate effect."
11216
+ ;;
11217
+
11218
+ --help|-h|help)
11219
+ echo -e "${BOLD}loki telemetry${NC} - OpenTelemetry management (v6.7.0)"
11220
+ echo ""
11221
+ echo "Usage: loki telemetry <command>"
11222
+ echo ""
11223
+ echo "Commands:"
11224
+ echo " status Show current telemetry config"
11225
+ echo " enable [endpoint] Enable OTEL (default: http://localhost:4318)"
11226
+ echo " disable Disable OTEL"
11227
+ echo ""
11228
+ ;;
11229
+ *)
11230
+ echo -e "${RED}Unknown telemetry command: $subcommand${NC}"
11231
+ echo "Run 'loki telemetry help' for usage."
11232
+ return 1
11233
+ ;;
11234
+ esac
11235
+ }
11236
+
10633
11237
  # Remote Control - start a remote-controllable Claude session
10634
11238
  cmd_remote() {
10635
11239
  local rc_flags=()
@@ -10945,4 +11549,281 @@ cmd_completions() {
10945
11549
  esac
10946
11550
  }
10947
11551
 
11552
+ # Agent type dispatch (v6.7.0)
11553
+ cmd_agent() {
11554
+ local subcommand="${1:-list}"
11555
+ shift 2>/dev/null || true
11556
+
11557
+ # Find agents/types.json relative to script location
11558
+ local script_dir
11559
+ script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11560
+ local types_file="${script_dir}/../agents/types.json"
11561
+ if [ ! -f "$types_file" ]; then
11562
+ # Try relative to current dir (development)
11563
+ types_file="agents/types.json"
11564
+ fi
11565
+ if [ ! -f "$types_file" ]; then
11566
+ echo -e "${RED}Error: agents/types.json not found${NC}"
11567
+ echo "Expected at: ${script_dir}/../agents/types.json"
11568
+ return 1
11569
+ fi
11570
+
11571
+ case "$subcommand" in
11572
+ list)
11573
+ local filter_swarm="${1:-}"
11574
+ echo -e "${BOLD}Agent Types${NC}"
11575
+ echo ""
11576
+ python3 << PYEOF
11577
+ import json, sys
11578
+
11579
+ with open("$types_file") as f:
11580
+ agents = json.load(f)
11581
+
11582
+ filter_swarm = "$filter_swarm"
11583
+ if filter_swarm and filter_swarm.startswith("--swarm"):
11584
+ # Handle --swarm=X or --swarm X
11585
+ if "=" in filter_swarm:
11586
+ filter_swarm = filter_swarm.split("=")[1]
11587
+ else:
11588
+ filter_swarm = "${2:-}"
11589
+
11590
+ swarms = {}
11591
+ for a in agents:
11592
+ s = a["swarm"]
11593
+ if filter_swarm and s != filter_swarm:
11594
+ continue
11595
+ swarms.setdefault(s, []).append(a)
11596
+
11597
+ total = 0
11598
+ for swarm_name in sorted(swarms.keys()):
11599
+ print(f" {swarm_name.upper()} SWARM:")
11600
+ for a in swarms[swarm_name]:
11601
+ caps = a.get("capabilities", "")
11602
+ if len(caps) > 60:
11603
+ caps = caps[:57] + "..."
11604
+ print(f" {a['type']:22s} {caps}")
11605
+ total += 1
11606
+ print()
11607
+
11608
+ print(f" Total: {total} agent type(s)")
11609
+ PYEOF
11610
+ ;;
11611
+
11612
+ info)
11613
+ local agent_type="${1:-}"
11614
+ if [ -z "$agent_type" ]; then
11615
+ echo -e "${RED}Usage: loki agent info <type>${NC}"
11616
+ return 1
11617
+ fi
11618
+ python3 << PYEOF
11619
+ import json, sys
11620
+
11621
+ with open("$types_file") as f:
11622
+ agents = json.load(f)
11623
+
11624
+ target = "$agent_type"
11625
+ found = None
11626
+ for a in agents:
11627
+ if a["type"] == target:
11628
+ found = a
11629
+ break
11630
+
11631
+ if not found:
11632
+ print(f"Agent type not found: {target}")
11633
+ print("Run 'loki agent list' to see all types.")
11634
+ sys.exit(1)
11635
+
11636
+ print(f"Type: {found['type']}")
11637
+ print(f"Name: {found['name']}")
11638
+ print(f"Swarm: {found['swarm']}")
11639
+ print(f"Capabilities: {found['capabilities']}")
11640
+ print(f"Focus: {', '.join(found.get('focus', []))}")
11641
+ print()
11642
+ print(f"Persona:")
11643
+ print(f" {found['persona']}")
11644
+ PYEOF
11645
+ ;;
11646
+
11647
+ run)
11648
+ local agent_type="${1:-}"
11649
+ local prompt="${2:-}"
11650
+ if [ -z "$agent_type" ] || [ -z "$prompt" ]; then
11651
+ echo -e "${RED}Usage: loki agent run <type> \"<prompt>\"${NC}"
11652
+ return 1
11653
+ fi
11654
+
11655
+ # Get persona from types.json
11656
+ local persona
11657
+ persona=$(AGENT_TYPE="$agent_type" TYPES_FILE="$types_file" python3 -c "
11658
+ import json, os
11659
+ with open(os.environ['TYPES_FILE']) as f:
11660
+ agents = json.load(f)
11661
+ for a in agents:
11662
+ if a['type'] == os.environ['AGENT_TYPE']:
11663
+ print(a['persona'])
11664
+ break
11665
+ " 2>/dev/null)
11666
+
11667
+ if [ -z "$persona" ]; then
11668
+ echo -e "${RED}Unknown agent type: $agent_type${NC}"
11669
+ return 1
11670
+ fi
11671
+
11672
+ local full_prompt="$persona $prompt"
11673
+ echo -e "${BOLD}Running as: $agent_type${NC}"
11674
+ echo -e "${DIM}Persona: ${persona:0:80}...${NC}"
11675
+ echo ""
11676
+
11677
+ # Invoke current provider
11678
+ local provider="${LOKI_PROVIDER:-claude}"
11679
+ if [ -f ".loki/state/provider" ]; then
11680
+ provider=$(cat ".loki/state/provider" 2>/dev/null)
11681
+ fi
11682
+
11683
+ case "$provider" in
11684
+ claude)
11685
+ claude -p "$full_prompt" 2>&1
11686
+ ;;
11687
+ codex)
11688
+ codex exec --full-auto "$full_prompt" 2>&1
11689
+ ;;
11690
+ gemini)
11691
+ gemini --approval-mode=yolo "$full_prompt" 2>&1
11692
+ ;;
11693
+ cline)
11694
+ cline -y "$full_prompt" 2>&1
11695
+ ;;
11696
+ aider)
11697
+ aider --message "$full_prompt" --yes-always --no-auto-commits < /dev/null 2>&1
11698
+ ;;
11699
+ *)
11700
+ echo -e "${RED}Unknown provider: $provider${NC}"
11701
+ return 1
11702
+ ;;
11703
+ esac
11704
+ ;;
11705
+
11706
+ start)
11707
+ local agent_type="${1:-}"
11708
+ local prd="${2:-}"
11709
+ if [ -z "$agent_type" ] || [ -z "$prd" ]; then
11710
+ echo -e "${RED}Usage: loki agent start <type> \"<prompt>\" or loki agent start <type> <prd-file>${NC}"
11711
+ return 1
11712
+ fi
11713
+
11714
+ # Get persona
11715
+ local persona
11716
+ persona=$(AGENT_TYPE="$agent_type" TYPES_FILE="$types_file" python3 -c "
11717
+ import json, os
11718
+ with open(os.environ['TYPES_FILE']) as f:
11719
+ agents = json.load(f)
11720
+ for a in agents:
11721
+ if a['type'] == os.environ['AGENT_TYPE']:
11722
+ print(a['persona'])
11723
+ break
11724
+ " 2>/dev/null)
11725
+
11726
+ if [ -z "$persona" ]; then
11727
+ echo -e "${RED}Unknown agent type: $agent_type${NC}"
11728
+ return 1
11729
+ fi
11730
+
11731
+ echo -e "${BOLD}Starting autonomous session as: $agent_type${NC}"
11732
+
11733
+ # Set agent persona as env var for run.sh to pick up
11734
+ export LOKI_AGENT_PERSONA="$persona"
11735
+ export LOKI_AGENT_TYPE="$agent_type"
11736
+
11737
+ # Delegate to regular start
11738
+ if [ -f "$prd" ]; then
11739
+ cmd_start "$prd"
11740
+ else
11741
+ # Treat as inline prompt - create temp PRD
11742
+ local tmp_prd
11743
+ tmp_prd=$(mktemp /tmp/loki-agent-prd-XXXXXX.md)
11744
+ echo "# Agent Task: $agent_type" > "$tmp_prd"
11745
+ echo "" >> "$tmp_prd"
11746
+ echo "$prd" >> "$tmp_prd"
11747
+ cmd_start "$tmp_prd"
11748
+ rm -f "$tmp_prd"
11749
+ fi
11750
+ ;;
11751
+
11752
+ review)
11753
+ local agent_type="${1:-ops-security}"
11754
+ echo -e "${BOLD}Code review as: $agent_type${NC}"
11755
+
11756
+ local persona
11757
+ persona=$(AGENT_TYPE="$agent_type" TYPES_FILE="$types_file" python3 -c "
11758
+ import json, os
11759
+ with open(os.environ['TYPES_FILE']) as f:
11760
+ agents = json.load(f)
11761
+ for a in agents:
11762
+ if a['type'] == os.environ['AGENT_TYPE']:
11763
+ print(a['persona'])
11764
+ break
11765
+ " 2>/dev/null)
11766
+
11767
+ if [ -z "$persona" ]; then
11768
+ echo -e "${RED}Unknown agent type: $agent_type${NC}"
11769
+ return 1
11770
+ fi
11771
+
11772
+ local diff
11773
+ diff=$(git diff HEAD~1 2>/dev/null || git diff --cached 2>/dev/null || echo "No changes to review")
11774
+
11775
+ local review_prompt="$persona
11776
+
11777
+ Review the following code changes. Focus on your area of expertise.
11778
+ Provide findings as:
11779
+ - CRITICAL: Must fix before merge
11780
+ - HIGH: Should fix
11781
+ - MEDIUM: Improvement suggestion
11782
+ - LOW: Nitpick
11783
+
11784
+ Changes:
11785
+ $diff"
11786
+
11787
+ local provider="${LOKI_PROVIDER:-claude}"
11788
+ [ -f ".loki/state/provider" ] && provider=$(cat ".loki/state/provider" 2>/dev/null)
11789
+
11790
+ case "$provider" in
11791
+ claude) claude -p "$review_prompt" 2>&1 ;;
11792
+ codex) codex exec --full-auto "$review_prompt" 2>&1 ;;
11793
+ gemini) gemini --approval-mode=yolo "$review_prompt" 2>&1 ;;
11794
+ cline) cline -y "$review_prompt" 2>&1 ;;
11795
+ *) echo -e "${RED}Unknown provider: $provider${NC}"; return 1 ;;
11796
+ esac
11797
+ ;;
11798
+
11799
+ --help|-h|help)
11800
+ echo -e "${BOLD}loki agent${NC} - Agent type dispatch (v6.7.0)"
11801
+ echo ""
11802
+ echo "Usage: loki agent <command> [args]"
11803
+ echo ""
11804
+ echo "Commands:"
11805
+ echo " list [--swarm NAME] List all agent types (optionally filter by swarm)"
11806
+ echo " info <type> Show agent type details"
11807
+ echo " run <type> \"<prompt>\" Single-shot: run prompt with agent persona"
11808
+ echo " start <type> <prd|prompt> Full autonomous loop with agent persona"
11809
+ echo " review [type] Code review with agent persona (default: ops-security)"
11810
+ echo " help Show this help"
11811
+ echo ""
11812
+ echo "Examples:"
11813
+ echo " loki agent list"
11814
+ echo " loki agent list --swarm engineering"
11815
+ echo " loki agent info eng-frontend"
11816
+ echo " loki agent run eng-frontend \"Create a responsive navbar\""
11817
+ echo " loki agent start eng-backend ./api-prd.md"
11818
+ echo " loki agent review ops-security"
11819
+ echo ""
11820
+ ;;
11821
+ *)
11822
+ echo -e "${RED}Unknown agent command: $subcommand${NC}"
11823
+ echo "Run 'loki agent help' for usage."
11824
+ return 1
11825
+ ;;
11826
+ esac
11827
+ }
11828
+
10948
11829
  main "$@"