loki-mode 6.4.0 → 6.6.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/run.sh CHANGED
@@ -1137,6 +1137,20 @@ validate_api_keys() {
1137
1137
  claude) key_var="ANTHROPIC_API_KEY" ;;
1138
1138
  codex) key_var="OPENAI_API_KEY" ;;
1139
1139
  gemini) key_var="GOOGLE_API_KEY" ;;
1140
+ cline) # Cline manages its own keys via `cline auth`
1141
+ if ! command -v cline &>/dev/null; then
1142
+ log_error "Cline CLI not found. Install: npm install -g cline"
1143
+ return 1
1144
+ fi
1145
+ return 0
1146
+ ;;
1147
+ aider) # Aider manages keys via env vars or .aider.conf.yml
1148
+ if ! command -v aider &>/dev/null; then
1149
+ log_error "Aider not found. Install: pip install aider-chat"
1150
+ return 1
1151
+ fi
1152
+ return 0
1153
+ ;;
1140
1154
  esac
1141
1155
 
1142
1156
  if [[ -z "$key_var" ]]; then
@@ -1390,6 +1404,14 @@ get_provider_tier_param() {
1390
1404
  *) echo "medium" ;;
1391
1405
  esac
1392
1406
  ;;
1407
+ cline)
1408
+ # Cline uses single externally-configured model
1409
+ echo "${LOKI_CLINE_MODEL:-default}"
1410
+ ;;
1411
+ aider)
1412
+ # Aider uses single externally-configured model
1413
+ echo "${LOKI_AIDER_MODEL:-claude-3.7-sonnet}"
1414
+ ;;
1393
1415
  *)
1394
1416
  echo "development"
1395
1417
  ;;
@@ -2177,6 +2199,15 @@ spawn_worktree_session() {
2177
2199
  invoke_gemini "Loki Mode: $task_prompt. Read .loki/CONTINUITY.md for context." \
2178
2200
  >> "$log_file" 2>&1
2179
2201
  ;;
2202
+ cline)
2203
+ # Cline supports parallel instances
2204
+ invoke_cline "Loki Mode: $task_prompt. Read .loki/CONTINUITY.md for context." \
2205
+ >> "$log_file" 2>&1
2206
+ ;;
2207
+ aider)
2208
+ # Aider has known issues with parallel - skip
2209
+ log_warn "Aider does not support parallel sessions, skipping"
2210
+ ;;
2180
2211
  *)
2181
2212
  log_error "Unknown provider: ${PROVIDER_NAME}"
2182
2213
  return 1
@@ -2269,6 +2300,12 @@ Output ONLY the resolved file content with no conflict markers. No explanations.
2269
2300
  # Uses invoke_gemini_capture for rate limit fallback to flash model
2270
2301
  resolution=$(invoke_gemini_capture "$conflict_prompt" 2>/dev/null)
2271
2302
  ;;
2303
+ cline)
2304
+ resolution=$(invoke_cline_capture "$conflict_prompt" 2>/dev/null)
2305
+ ;;
2306
+ aider)
2307
+ resolution=$(invoke_aider_capture "$conflict_prompt" 2>/dev/null)
2308
+ ;;
2272
2309
  *)
2273
2310
  log_error "Unknown provider: ${PROVIDER_NAME}"
2274
2311
  return 1
@@ -2515,6 +2552,12 @@ check_prerequisites() {
2515
2552
  # TODO: Verify official Gemini CLI package name when available
2516
2553
  log_info "Install: npm install -g @google/gemini-cli (or visit https://ai.google.dev/)"
2517
2554
  ;;
2555
+ cline)
2556
+ log_info "Install: npm install -g cline"
2557
+ ;;
2558
+ aider)
2559
+ log_info "Install: pip install aider-chat"
2560
+ ;;
2518
2561
  *)
2519
2562
  log_info "Install the $cli_name CLI for your provider"
2520
2563
  ;;
@@ -2828,6 +2871,62 @@ invoke_gemini_capture() {
2828
2871
  echo "$output"
2829
2872
  }
2830
2873
 
2874
+ #===============================================================================
2875
+ # Cline Invocation (Tier 2 - Near-Full)
2876
+ #===============================================================================
2877
+
2878
+ # Invoke Cline CLI in autonomous mode
2879
+ # Usage: invoke_cline "prompt" [additional args...]
2880
+ invoke_cline() {
2881
+ local prompt="$1"
2882
+ shift
2883
+ local model="${LOKI_CLINE_MODEL:-}"
2884
+ local model_flag=""
2885
+ [[ -n "$model" ]] && model_flag="-m $model"
2886
+ # shellcheck disable=SC2086
2887
+ cline -y $model_flag "$prompt" "$@" 2>&1
2888
+ }
2889
+
2890
+ # Invoke Cline and capture output (for variable assignment)
2891
+ # Usage: result=$(invoke_cline_capture "prompt")
2892
+ invoke_cline_capture() {
2893
+ local prompt="$1"
2894
+ shift
2895
+ local model="${LOKI_CLINE_MODEL:-}"
2896
+ local model_flag=""
2897
+ [[ -n "$model" ]] && model_flag="-m $model"
2898
+ # shellcheck disable=SC2086
2899
+ cline -y $model_flag "$prompt" "$@" 2>&1
2900
+ }
2901
+
2902
+ #===============================================================================
2903
+ # Aider Invocation (Tier 3 - Degraded, 18+ Providers)
2904
+ #===============================================================================
2905
+
2906
+ # Invoke Aider in autonomous single-instruction mode
2907
+ # Usage: invoke_aider "prompt" [additional args...]
2908
+ invoke_aider() {
2909
+ local prompt="$1"
2910
+ shift
2911
+ local model="${LOKI_AIDER_MODEL:-claude-3.7-sonnet}"
2912
+ local extra_flags="${LOKI_AIDER_FLAGS:-}"
2913
+ # shellcheck disable=SC2086
2914
+ aider --message "$prompt" --yes-always --no-auto-commits \
2915
+ --model "$model" $extra_flags "$@" 2>&1
2916
+ }
2917
+
2918
+ # Invoke Aider and capture output (for variable assignment)
2919
+ # Usage: result=$(invoke_aider_capture "prompt")
2920
+ invoke_aider_capture() {
2921
+ local prompt="$1"
2922
+ shift
2923
+ local model="${LOKI_AIDER_MODEL:-claude-3.7-sonnet}"
2924
+ local extra_flags="${LOKI_AIDER_FLAGS:-}"
2925
+ # shellcheck disable=SC2086
2926
+ aider --message "$prompt" --yes-always --no-auto-commits \
2927
+ --model "$model" $extra_flags "$@" 2>&1
2928
+ }
2929
+
2831
2930
  #===============================================================================
2832
2931
  # Copy Skill Files to Project Directory
2833
2932
  #===============================================================================
@@ -3343,6 +3442,10 @@ track_iteration_complete() {
3343
3442
  model_tier="gpt-5.3-codex"
3344
3443
  elif [ "${PROVIDER_NAME:-claude}" = "gemini" ]; then
3345
3444
  model_tier="gemini-3-pro"
3445
+ elif [ "${PROVIDER_NAME:-claude}" = "cline" ]; then
3446
+ model_tier="${LOKI_CLINE_MODEL:-sonnet}"
3447
+ elif [ "${PROVIDER_NAME:-claude}" = "aider" ]; then
3448
+ model_tier="${LOKI_AIDER_MODEL:-claude-3.7-sonnet}"
3346
3449
  fi
3347
3450
  local phase="${LAST_KNOWN_PHASE:-}"
3348
3451
  [ -z "$phase" ] && phase=$(python3 -c "import json; print(json.load(open('.loki/state/orchestrator.json')).get('currentPhase', 'unknown'))" 2>/dev/null || echo "unknown")
@@ -5143,6 +5246,14 @@ BUILD_PROMPT
5143
5246
  invoke_gemini_capture "$prompt_text" \
5144
5247
  > "$review_output" 2>/dev/null
5145
5248
  ;;
5249
+ cline)
5250
+ invoke_cline_capture "$prompt_text" \
5251
+ > "$review_output" 2>/dev/null
5252
+ ;;
5253
+ aider)
5254
+ invoke_aider_capture "$prompt_text" \
5255
+ > "$review_output" 2>/dev/null
5256
+ ;;
5146
5257
  *)
5147
5258
  echo "VERDICT: PASS" > "$review_output"
5148
5259
  echo "FINDINGS:" >> "$review_output"
@@ -5347,6 +5458,18 @@ OVERALL_RISK: HIGH or MEDIUM or LOW"
5347
5458
  > "$result_file" 2>/dev/null || true
5348
5459
  fi
5349
5460
  ;;
5461
+ cline)
5462
+ if command -v cline &>/dev/null; then
5463
+ invoke_cline_capture "$adversarial_prompt" \
5464
+ > "$result_file" 2>/dev/null || true
5465
+ fi
5466
+ ;;
5467
+ aider)
5468
+ if command -v aider &>/dev/null; then
5469
+ invoke_aider_capture "$adversarial_prompt" \
5470
+ > "$result_file" 2>/dev/null || true
5471
+ fi
5472
+ ;;
5350
5473
  *)
5351
5474
  echo "ATTACK_VECTORS: None (unknown provider)" > "$result_file"
5352
5475
  echo "OVERALL_RISK: LOW" >> "$result_file"
@@ -6063,7 +6186,7 @@ detect_rate_limit() {
6063
6186
  claude)
6064
6187
  wait_secs=$(parse_claude_reset_time "$log_file")
6065
6188
  ;;
6066
- codex|gemini|*)
6189
+ codex|gemini|cline|aider|*)
6067
6190
  # No provider-specific reset time format known
6068
6191
  # Fall through to generic parsing
6069
6192
  ;;
@@ -7434,12 +7557,13 @@ run_autonomous() {
7434
7557
  audit_agent_action "cli_invoke" "Starting iteration $ITERATION_COUNT" "provider=${PROVIDER_NAME:-claude},tier=$CURRENT_TIER"
7435
7558
 
7436
7559
  # Provider-specific invocation with dynamic tier selection
7560
+ local exit_code=0
7437
7561
  case "${PROVIDER_NAME:-claude}" in
7438
7562
  claude)
7439
7563
  # Claude: Full features with stream-json output and agent tracking
7440
7564
  # Uses dynamic tier for model selection based on RARV phase
7441
7565
  # Pass tier to Python via environment for dashboard display
7442
- LOKI_CURRENT_MODEL="$tier_param" \
7566
+ { LOKI_CURRENT_MODEL="$tier_param" \
7443
7567
  claude --dangerously-skip-permissions --model "$tier_param" -p "$prompt" \
7444
7568
  --output-format stream-json --verbose 2>&1 | \
7445
7569
  tee -a "$log_file" "$agent_log" | \
@@ -7647,7 +7771,7 @@ if __name__ == "__main__":
7647
7771
  except BrokenPipeError:
7648
7772
  sys.exit(0)
7649
7773
  '
7650
- local exit_code=${PIPESTATUS[0]}
7774
+ } && exit_code=0 || exit_code=$?
7651
7775
  ;;
7652
7776
 
7653
7777
  codex)
@@ -7655,10 +7779,10 @@ if __name__ == "__main__":
7655
7779
  # Uses positional prompt after exec subcommand
7656
7780
  # Note: Effort is set via env var, not CLI flag
7657
7781
  # Uses dynamic tier from RARV phase (tier_param already set above)
7658
- CODEX_MODEL_REASONING_EFFORT="$tier_param" \
7782
+ { CODEX_MODEL_REASONING_EFFORT="$tier_param" \
7659
7783
  codex exec --full-auto \
7660
- "$prompt" 2>&1 | tee -a "$log_file" "$agent_log"
7661
- local exit_code=${PIPESTATUS[0]}
7784
+ "$prompt" 2>&1 | tee -a "$log_file" "$agent_log"; \
7785
+ } && exit_code=0 || exit_code=$?
7662
7786
  ;;
7663
7787
 
7664
7788
  gemini)
@@ -7672,8 +7796,8 @@ if __name__ == "__main__":
7672
7796
  # Try primary model, fallback on rate limit
7673
7797
  local tmp_output
7674
7798
  tmp_output=$(mktemp)
7675
- gemini --approval-mode=yolo --model "$model" "$prompt" < /dev/null 2>&1 | tee "$tmp_output" | tee -a "$log_file" "$agent_log"
7676
- local exit_code=${PIPESTATUS[0]}
7799
+ { gemini --approval-mode=yolo --model "$model" "$prompt" < /dev/null 2>&1 | tee "$tmp_output" | tee -a "$log_file" "$agent_log"; \
7800
+ } && exit_code=0 || exit_code=$?
7677
7801
 
7678
7802
  if [[ $exit_code -ne 0 ]] && grep -qiE "(rate.?limit|429|quota|resource.?exhausted)" "$tmp_output"; then
7679
7803
  log_warn "Rate limit hit on $model, falling back to $fallback"
@@ -7684,6 +7808,21 @@ if __name__ == "__main__":
7684
7808
  rm -f "$tmp_output"
7685
7809
  ;;
7686
7810
 
7811
+ cline)
7812
+ # Cline: Tier 2 - near-full mode with subagents and MCP
7813
+ echo "[loki] Cline model: ${LOKI_CLINE_MODEL:-default}, tier: $tier_param" >> "$log_file"
7814
+ echo "[loki] Cline model: ${LOKI_CLINE_MODEL:-default}, tier: $tier_param" >> "$agent_log"
7815
+ { invoke_cline "$prompt" 2>&1 | tee -a "$log_file" "$agent_log"; \
7816
+ } && exit_code=0 || exit_code=$?
7817
+ ;;
7818
+ aider)
7819
+ # Aider: Tier 3 - degraded mode, 18+ providers
7820
+ echo "[loki] Aider model: ${LOKI_AIDER_MODEL:-claude-3.7-sonnet}, tier: $tier_param" >> "$log_file"
7821
+ echo "[loki] Aider model: ${LOKI_AIDER_MODEL:-claude-3.7-sonnet}, tier: $tier_param" >> "$agent_log"
7822
+ { invoke_aider "$prompt" 2>&1 | tee -a "$log_file" "$agent_log"; \
7823
+ } && exit_code=0 || exit_code=$?
7824
+ ;;
7825
+
7687
7826
  *)
7688
7827
  log_error "Unknown provider: ${PROVIDER_NAME:-unknown}"
7689
7828
  local exit_code=1
@@ -8143,17 +8282,18 @@ except (json.JSONDecodeError, OSError): pass
8143
8282
  stop_dashboard
8144
8283
  stop_status_monitor
8145
8284
  kill_all_registered
8146
- rm -f .loki/loki.pid .loki/PAUSE 2>/dev/null
8285
+ rm -f "$loki_dir/loki.pid" "$loki_dir/PAUSE" 2>/dev/null
8147
8286
  # Clean up per-session PID file if running with session ID
8148
8287
  if [ -n "${LOKI_SESSION_ID:-}" ]; then
8149
- rm -f ".loki/sessions/${LOKI_SESSION_ID}/loki.pid" 2>/dev/null
8288
+ rm -f "$loki_dir/sessions/${LOKI_SESSION_ID}/loki.pid" 2>/dev/null
8150
8289
  fi
8151
8290
  # Mark session.json as stopped
8152
- if [ -f ".loki/session.json" ]; then
8153
- python3 -c "
8154
- import json
8291
+ if [ -f "$loki_dir/session.json" ]; then
8292
+ _LOKI_SESSION_FILE="$loki_dir/session.json" python3 -c "
8293
+ import json, os
8294
+ sf = os.environ['_LOKI_SESSION_FILE']
8155
8295
  try:
8156
- with open('.loki/session.json', 'r+') as f:
8296
+ with open(sf, 'r+') as f:
8157
8297
  d = json.load(f); d['status'] = 'stopped'
8158
8298
  f.seek(0); f.truncate(); json.dump(d, f)
8159
8299
  except (json.JSONDecodeError, OSError): pass
@@ -8277,7 +8417,7 @@ main() {
8277
8417
  fi
8278
8418
  shift 2
8279
8419
  else
8280
- log_error "--provider requires a value (claude, codex, gemini)"
8420
+ log_error "--provider requires a value (claude, codex, gemini, cline, aider)"
8281
8421
  exit 1
8282
8422
  fi
8283
8423
  ;;
@@ -8311,7 +8451,7 @@ main() {
8311
8451
  echo "Options:"
8312
8452
  echo " --parallel Enable git worktree-based parallel workflows"
8313
8453
  echo " --allow-haiku Enable Haiku model for fast tier (default: disabled)"
8314
- echo " --provider <name> Provider: claude (default), codex, gemini"
8454
+ echo " --provider <name> Provider: claude (default), codex, gemini, cline, aider"
8315
8455
  echo " --bg, --background Run in background mode"
8316
8456
  echo " --interactive-prd Interactive PRD pre-flight analysis"
8317
8457
  echo " --help, -h Show this help message"
@@ -8736,17 +8876,19 @@ main() {
8736
8876
  fi
8737
8877
  stop_dashboard
8738
8878
  stop_status_monitor
8739
- rm -f .loki/loki.pid 2>/dev/null
8879
+ local loki_dir="${TARGET_DIR:-.}/.loki"
8880
+ rm -f "$loki_dir/loki.pid" 2>/dev/null
8740
8881
  # Clean up per-session PID file if running with session ID
8741
8882
  if [ -n "${LOKI_SESSION_ID:-}" ]; then
8742
- rm -f ".loki/sessions/${LOKI_SESSION_ID}/loki.pid" 2>/dev/null
8883
+ rm -f "$loki_dir/sessions/${LOKI_SESSION_ID}/loki.pid" 2>/dev/null
8743
8884
  fi
8744
8885
  # Mark session.json as stopped
8745
- if [ -f ".loki/session.json" ]; then
8746
- python3 -c "
8747
- import json
8886
+ if [ -f "$loki_dir/session.json" ]; then
8887
+ _LOKI_SESSION_FILE="$loki_dir/session.json" python3 -c "
8888
+ import json, os
8889
+ sf = os.environ['_LOKI_SESSION_FILE']
8748
8890
  try:
8749
- with open('.loki/session.json', 'r+') as f:
8891
+ with open(sf, 'r+') as f:
8750
8892
  d = json.load(f); d['status'] = 'stopped'
8751
8893
  f.seek(0); f.truncate(); json.dump(d, f)
8752
8894
  except (json.JSONDecodeError, OSError): pass
@@ -68,28 +68,32 @@ PROMPT_INJECTION_ENABLED="${LOKI_PROMPT_INJECTION:-false}"
68
68
 
69
69
  # Preset definitions: "host_path:container_path:mode"
70
70
  # Container runs as user 'loki' (UID 1000), so mount to /home/loki/
71
- declare -A DOCKER_MOUNT_PRESETS
72
- DOCKER_MOUNT_PRESETS=(
73
- # Note: gh and git are also hardcoded in start_sandbox() defaults
74
- [gh]="$HOME/.config/gh:/home/loki/.config/gh:ro"
75
- [git]="$HOME/.gitconfig:/home/loki/.gitconfig:ro"
76
- [ssh]="$HOME/.ssh/known_hosts:/home/loki/.ssh/known_hosts:ro"
77
- [aws]="$HOME/.aws:/home/loki/.aws:ro"
78
- [azure]="$HOME/.azure:/home/loki/.azure:ro"
79
- [kube]="$HOME/.kube:/home/loki/.kube:ro"
80
- [terraform]="$HOME/.terraform.d:/home/loki/.terraform.d:ro"
81
- [gcloud]="$HOME/.config/gcloud:/home/loki/.config/gcloud:ro"
82
- [npm]="$HOME/.npmrc:/home/loki/.npmrc:ro"
83
- )
71
+ # Uses functions instead of declare -A for bash 3.2 compatibility (macOS default)
72
+ _get_mount_preset() {
73
+ case "$1" in
74
+ gh) echo "$HOME/.config/gh:/home/loki/.config/gh:ro" ;;
75
+ git) echo "$HOME/.gitconfig:/home/loki/.gitconfig:ro" ;;
76
+ ssh) echo "$HOME/.ssh/known_hosts:/home/loki/.ssh/known_hosts:ro" ;;
77
+ aws) echo "$HOME/.aws:/home/loki/.aws:ro" ;;
78
+ azure) echo "$HOME/.azure:/home/loki/.azure:ro" ;;
79
+ kube) echo "$HOME/.kube:/home/loki/.kube:ro" ;;
80
+ terraform) echo "$HOME/.terraform.d:/home/loki/.terraform.d:ro" ;;
81
+ gcloud) echo "$HOME/.config/gcloud:/home/loki/.config/gcloud:ro" ;;
82
+ npm) echo "$HOME/.npmrc:/home/loki/.npmrc:ro" ;;
83
+ *) echo "" ;;
84
+ esac
85
+ }
84
86
 
85
87
  # Environment variables auto-passed per preset (comma-separated)
86
- declare -A DOCKER_ENV_PRESETS
87
- DOCKER_ENV_PRESETS=(
88
- [aws]="AWS_REGION,AWS_PROFILE,AWS_DEFAULT_REGION"
89
- [azure]="AZURE_SUBSCRIPTION_ID,AZURE_TENANT_ID"
90
- [gcloud]="GOOGLE_PROJECT,GOOGLE_REGION,GCLOUD_PROJECT"
91
- [terraform]="TF_VAR_*"
92
- )
88
+ _get_env_preset() {
89
+ case "$1" in
90
+ aws) echo "AWS_REGION,AWS_PROFILE,AWS_DEFAULT_REGION" ;;
91
+ azure) echo "AZURE_SUBSCRIPTION_ID,AZURE_TENANT_ID" ;;
92
+ gcloud) echo "GOOGLE_PROJECT,GOOGLE_REGION,GCLOUD_PROJECT" ;;
93
+ terraform) echo "TF_VAR_*" ;;
94
+ *) echo "" ;;
95
+ esac
96
+ }
93
97
 
94
98
  #===============================================================================
95
99
  # Utility Functions
@@ -160,6 +164,14 @@ warn_missing_api_keys() {
160
164
  log_warn "GOOGLE_API_KEY not set - Gemini commands will fail inside container"
161
165
  fi
162
166
  ;;
167
+ cline)
168
+ log_info "Cline manages its own API keys via 'cline auth' - ensure authentication is configured"
169
+ ;;
170
+ aider)
171
+ if [[ -z "${OPENAI_API_KEY:-}" ]] && [[ -z "${ANTHROPIC_API_KEY:-}" ]]; then
172
+ log_warn "No API key set for Aider - set OPENAI_API_KEY or ANTHROPIC_API_KEY"
173
+ fi
174
+ ;;
163
175
  esac
164
176
  }
165
177
 
@@ -592,6 +604,18 @@ _desktop_install_provider_cli() {
592
604
  docker sandbox exec "$sandbox_name" npm install -g @google/gemini-cli 2>&1 | tail -1
593
605
  fi
594
606
  ;;
607
+ cline)
608
+ if ! docker sandbox exec "$sandbox_name" which cline &>/dev/null 2>&1; then
609
+ log_info "Installing Cline CLI in sandbox (one-time)..."
610
+ docker sandbox exec "$sandbox_name" npm install -g cline 2>&1 | tail -1
611
+ fi
612
+ ;;
613
+ aider)
614
+ if ! docker sandbox exec "$sandbox_name" which aider &>/dev/null 2>&1; then
615
+ log_info "Installing Aider in sandbox (one-time)..."
616
+ docker sandbox exec "$sandbox_name" pip install aider-chat 2>&1 | tail -1
617
+ fi
618
+ ;;
595
619
  # claude is pre-installed in the sandbox template
596
620
  esac
597
621
  }
@@ -836,7 +860,8 @@ except: pass
836
860
  while IFS= read -r name; do
837
861
  [[ -z "$name" ]] && continue
838
862
 
839
- local preset_value="${DOCKER_MOUNT_PRESETS[$name]:-}"
863
+ local preset_value
864
+ preset_value="$(_get_mount_preset "$name")"
840
865
  if [[ -z "$preset_value" ]]; then
841
866
  log_warn "Unknown Docker mount preset: $name"
842
867
  continue
@@ -861,7 +886,8 @@ except: pass
861
886
  fi
862
887
 
863
888
  # Add associated env vars
864
- local env_list="${DOCKER_ENV_PRESETS[$name]:-}"
889
+ local env_list
890
+ env_list="$(_get_env_preset "$name")"
865
891
  if [[ -n "$env_list" ]]; then
866
892
  IFS=',' read -ra env_names <<< "$env_list"
867
893
  local env_name
@@ -52,17 +52,30 @@ loki_telemetry() {
52
52
  os_name=$(uname -s 2>/dev/null || echo "unknown")
53
53
  arch=$(uname -m 2>/dev/null || echo "unknown")
54
54
 
55
- # Build properties from remaining args (key=value pairs)
56
- local extra_props=""
55
+ # Build JSON payload safely using Python to prevent injection
56
+ local extra_args=""
57
57
  for arg in "$@"; do
58
- local key="${arg%%=*}"
59
- local val="${arg#*=}"
60
- extra_props="${extra_props},\"${key}\":\"${val}\""
58
+ extra_args="${extra_args}${extra_args:+ }${arg}"
61
59
  done
62
60
 
63
61
  local payload
64
- payload=$(printf '{"api_key":"%s","event":"%s","distinct_id":"%s","properties":{"os":"%s","arch":"%s","version":"%s","channel":"%s"%s}}' \
65
- "$LOKI_POSTHOG_KEY" "$event" "$distinct_id" "$os_name" "$arch" "$version" "$channel" "$extra_props")
62
+ payload=$(python3 -c "
63
+ import json, sys
64
+ props = {'os': sys.argv[1], 'arch': sys.argv[2], 'version': sys.argv[3], 'channel': sys.argv[4]}
65
+ for arg in sys.argv[5:]:
66
+ if '=' in arg:
67
+ k, v = arg.split('=', 1)
68
+ props[k] = v
69
+ print(json.dumps({'api_key': '$LOKI_POSTHOG_KEY', 'event': sys.argv[5] if len(sys.argv) > 5 else '', 'distinct_id': '$distinct_id', 'properties': props}))
70
+ " "$os_name" "$arch" "$version" "$channel" $extra_args 2>/dev/null) || return 0
71
+ # Re-inject event and distinct_id properly
72
+ payload=$(python3 -c "
73
+ import json, sys
74
+ d = json.loads(sys.argv[1])
75
+ d['event'] = sys.argv[2]
76
+ d['distinct_id'] = sys.argv[3]
77
+ print(json.dumps(d))
78
+ " "$payload" "$event" "$distinct_id" 2>/dev/null) || return 0
66
79
 
67
80
  (curl -sS --max-time 3 -X POST "${LOKI_POSTHOG_HOST}/capture/" \
68
81
  -H "Content-Type: application/json" \
@@ -7,7 +7,7 @@ Modules:
7
7
  control: Session control API (start/stop/pause/resume)
8
8
  """
9
9
 
10
- __version__ = "6.4.0"
10
+ __version__ = "6.6.0"
11
11
 
12
12
  # Expose the control app for easy import
13
13
  try:
@@ -132,7 +132,7 @@ class StartRequest(BaseModel):
132
132
 
133
133
  def validate_provider(self) -> None:
134
134
  """Validate provider is from allowed list."""
135
- allowed_providers = ["claude", "codex", "gemini"]
135
+ allowed_providers = ["claude", "codex", "gemini", "cline", "aider"]
136
136
  if self.provider not in allowed_providers:
137
137
  raise ValueError(f"Invalid provider: {self.provider}. Must be one of: {', '.join(allowed_providers)}")
138
138
 
@@ -886,7 +886,7 @@ Summary: {summary}
886
886
  if len(entries) > 50:
887
887
  header = entries[0]
888
888
  recent = entries[-50:]
889
- content = header + "\n## Session:".join(recent)
889
+ content = header + "\n## Session:" + "\n## Session:".join(recent)
890
890
  else:
891
891
  content = existing
892
892
  content += entry
@@ -391,7 +391,7 @@ async def agent_card() -> dict:
391
391
  "agents": 41,
392
392
  "swarms": 8,
393
393
  "quality_gates": 9,
394
- "providers": ["claude", "codex", "gemini"],
394
+ "providers": ["claude", "codex", "gemini", "cline", "aider"],
395
395
  "streaming": True,
396
396
  "pushNotifications": False,
397
397
  "stateTransitionHistory": True,
@@ -536,9 +536,13 @@ async def get_status() -> StatusResponse:
536
536
 
537
537
  # Global session
538
538
  if running:
539
+ try:
540
+ _global_pid = int(pid_str) if pid_str else 0
541
+ except (ValueError, TypeError):
542
+ _global_pid = 0
539
543
  active_session_list.append(SessionInfo(
540
544
  session_id="global",
541
- pid=int(pid_str) if pid_str else 0,
545
+ pid=_global_pid,
542
546
  status=status,
543
547
  ))
544
548
 
@@ -1441,7 +1445,7 @@ async def query_audit_logs(
1441
1445
  action: Optional[str] = None,
1442
1446
  resource_type: Optional[str] = None,
1443
1447
  resource_id: Optional[str] = None,
1444
- limit: int = 100,
1448
+ limit: int = Query(default=100, ge=1, le=1000),
1445
1449
  offset: int = 0,
1446
1450
  ):
1447
1451
  """
@@ -1561,7 +1565,7 @@ async def get_memory_summary():
1561
1565
 
1562
1566
 
1563
1567
  @app.get("/api/memory/episodes")
1564
- async def list_episodes(limit: int = 50):
1568
+ async def list_episodes(limit: int = Query(default=50, ge=1, le=1000)):
1565
1569
  """List episodic memory entries."""
1566
1570
  ep_dir = _get_loki_dir() / "memory" / "episodic"
1567
1571
  episodes = []
@@ -1578,11 +1582,15 @@ async def list_episodes(limit: int = 50):
1578
1582
  @app.get("/api/memory/episodes/{episode_id}")
1579
1583
  async def get_episode(episode_id: str):
1580
1584
  """Get a specific episodic memory entry."""
1581
- ep_dir = _get_loki_dir() / "memory" / "episodic"
1585
+ loki_dir = _get_loki_dir()
1586
+ ep_dir = loki_dir / "memory" / "episodic"
1582
1587
  if not ep_dir.exists():
1583
1588
  raise HTTPException(status_code=404, detail="Episode not found")
1584
1589
  # Try direct filename match
1585
1590
  for f in ep_dir.glob("*.json"):
1591
+ resolved = os.path.realpath(f)
1592
+ if not resolved.startswith(os.path.realpath(str(loki_dir))):
1593
+ raise HTTPException(status_code=403, detail="Access denied")
1586
1594
  try:
1587
1595
  data = json.loads(f.read_text())
1588
1596
  if data.get("id") == episode_id or f.stem == episode_id:
@@ -1633,10 +1641,14 @@ async def list_skills():
1633
1641
  @app.get("/api/memory/skills/{skill_id}")
1634
1642
  async def get_skill(skill_id: str):
1635
1643
  """Get a specific procedural skill."""
1636
- skills_dir = _get_loki_dir() / "memory" / "skills"
1644
+ loki_dir = _get_loki_dir()
1645
+ skills_dir = loki_dir / "memory" / "skills"
1637
1646
  if not skills_dir.exists():
1638
1647
  raise HTTPException(status_code=404, detail="Skill not found")
1639
1648
  for f in skills_dir.glob("*.json"):
1649
+ resolved = os.path.realpath(f)
1650
+ if not resolved.startswith(os.path.realpath(str(loki_dir))):
1651
+ raise HTTPException(status_code=403, detail="Access denied")
1640
1652
  try:
1641
1653
  data = json.loads(f.read_text())
1642
1654
  if data.get("id") == skill_id or f.stem == skill_id:
@@ -1706,6 +1718,7 @@ def _read_learning_signals(signal_type: Optional[str] = None, limit: int = 50) -
1706
1718
  (learning/emitter.py). Each file contains a single signal object with fields:
1707
1719
  id, type, source, action, timestamp, confidence, outcome, data, context.
1708
1720
  """
1721
+ limit = min(limit, 1000)
1709
1722
  signals_dir = _get_loki_dir() / "learning" / "signals"
1710
1723
  if not signals_dir.exists() or not signals_dir.is_dir():
1711
1724
  return []
@@ -1825,7 +1838,7 @@ async def get_learning_signals(
1825
1838
  timeRange: str = "7d",
1826
1839
  signalType: Optional[str] = None,
1827
1840
  source: Optional[str] = None,
1828
- limit: int = 50,
1841
+ limit: int = Query(default=50, ge=1, le=1000),
1829
1842
  offset: int = 0,
1830
1843
  ):
1831
1844
  """Get raw learning signals from both events.jsonl and learning signals directory."""
@@ -2030,7 +2043,7 @@ async def trigger_aggregation():
2030
2043
 
2031
2044
 
2032
2045
  @app.get("/api/learning/preferences")
2033
- async def get_learning_preferences(limit: int = 50):
2046
+ async def get_learning_preferences(limit: int = Query(default=50, ge=1, le=1000)):
2034
2047
  """Get aggregated user preferences from events and learning signals directory."""
2035
2048
  events = _read_events("30d")
2036
2049
  prefs = [e for e in events if e.get("type") == "user_preference"]
@@ -2042,7 +2055,7 @@ async def get_learning_preferences(limit: int = 50):
2042
2055
 
2043
2056
 
2044
2057
  @app.get("/api/learning/errors")
2045
- async def get_learning_errors(limit: int = 50):
2058
+ async def get_learning_errors(limit: int = Query(default=50, ge=1, le=1000)):
2046
2059
  """Get aggregated error patterns from events and learning signals directory."""
2047
2060
  events = _read_events("30d")
2048
2061
  errors = [e for e in events if e.get("type") == "error_pattern"]
@@ -2054,7 +2067,7 @@ async def get_learning_errors(limit: int = 50):
2054
2067
 
2055
2068
 
2056
2069
  @app.get("/api/learning/success")
2057
- async def get_learning_success(limit: int = 50):
2070
+ async def get_learning_success(limit: int = Query(default=50, ge=1, le=1000)):
2058
2071
  """Get aggregated success patterns from events and learning signals directory."""
2059
2072
  events = _read_events("30d")
2060
2073
  successes = [e for e in events if e.get("type") == "success_pattern"]
@@ -2066,7 +2079,7 @@ async def get_learning_success(limit: int = 50):
2066
2079
 
2067
2080
 
2068
2081
  @app.get("/api/learning/tools")
2069
- async def get_tool_efficiency(limit: int = 50):
2082
+ async def get_tool_efficiency(limit: int = Query(default=50, ge=1, le=1000)):
2070
2083
  """Get tool efficiency rankings from events and learning signals directory."""
2071
2084
  events = _read_events("30d")
2072
2085
  tools = [e for e in events if e.get("type") == "tool_efficiency"]
@@ -2442,6 +2455,8 @@ _MODEL_PROVIDERS = {
2442
2455
  "gpt-5.3-codex": "codex",
2443
2456
  "gemini-3-pro": "gemini",
2444
2457
  "gemini-3-flash": "gemini",
2458
+ "cline-default": "cline",
2459
+ "aider-default": "aider",
2445
2460
  }
2446
2461
 
2447
2462
 
@@ -2505,7 +2520,7 @@ async def get_council_state():
2505
2520
 
2506
2521
 
2507
2522
  @app.get("/api/council/verdicts")
2508
- async def get_council_verdicts(limit: int = 20):
2523
+ async def get_council_verdicts(limit: int = Query(default=20, ge=1, le=1000)):
2509
2524
  """Get council vote history (decision log)."""
2510
2525
  state_file = _get_loki_dir() / "council" / "state.json"
2511
2526
  verdicts = []
@@ -2,11 +2,11 @@
2
2
 
3
3
  The flagship product of [Autonomi](https://www.autonomi.dev/). Complete installation instructions for all platforms and use cases.
4
4
 
5
- **Version:** v6.4.0
5
+ **Version:** v6.6.0
6
6
 
7
7
  ---
8
8
 
9
- ## What's New in v6.4.0
9
+ ## What's New in v6.6.0
10
10
 
11
11
  ### Dual-Mode Architecture (v6.0.0)
12
12
  - `loki run` command for direct autonomous execution