loki-mode 5.31.0 → 5.32.1
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/README.md +62 -0
- package/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/loki +462 -6
- package/dashboard/__init__.py +1 -1
- package/dashboard/server.py +12 -1
- package/dashboard/static/favicon.svg +5 -0
- package/dashboard/static/index.html +260 -7
- package/docs/INSTALLATION.md +8 -8
- package/package.json +1 -1
- package/templates/README.md +2 -1
- package/templates/rest-api-auth.md +252 -0
package/README.md
CHANGED
|
@@ -426,6 +426,68 @@ Go get coffee. It'll be deployed when you get back.
|
|
|
426
426
|
|
|
427
427
|
---
|
|
428
428
|
|
|
429
|
+
## Architecture
|
|
430
|
+
|
|
431
|
+
```mermaid
|
|
432
|
+
graph TB
|
|
433
|
+
PRD["PRD Document"] --> REASON
|
|
434
|
+
|
|
435
|
+
subgraph RARVC["RARV+C Cycle"]
|
|
436
|
+
direction TB
|
|
437
|
+
REASON["1. Reason"] --> ACT["2. Act"]
|
|
438
|
+
ACT --> REFLECT["3. Reflect"]
|
|
439
|
+
REFLECT --> VERIFY["4. Verify"]
|
|
440
|
+
VERIFY -->|"pass"| COMPOUND["5. Compound"]
|
|
441
|
+
VERIFY -->|"fail"| REASON
|
|
442
|
+
COMPOUND --> REASON
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
subgraph PROVIDERS["Provider Layer"]
|
|
446
|
+
CLAUDE["Claude Code<br/>(full features)"]
|
|
447
|
+
CODEX["Codex CLI<br/>(degraded)"]
|
|
448
|
+
GEMINI["Gemini CLI<br/>(degraded)"]
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
ACT --> PROVIDERS
|
|
452
|
+
|
|
453
|
+
subgraph AGENTS["Agent Swarms (41 types)"]
|
|
454
|
+
ENG["Engineering (8)"]
|
|
455
|
+
OPS["Operations (8)"]
|
|
456
|
+
BIZ["Business (8)"]
|
|
457
|
+
DATA["Data (3)"]
|
|
458
|
+
PROD["Product (3)"]
|
|
459
|
+
GROWTH["Growth (4)"]
|
|
460
|
+
REVIEW["Review (3)"]
|
|
461
|
+
ORCH["Orchestration (4)"]
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
PROVIDERS --> AGENTS
|
|
465
|
+
|
|
466
|
+
subgraph INFRA["Infrastructure"]
|
|
467
|
+
DASHBOARD["Dashboard<br/>(FastAPI + Web UI)"]
|
|
468
|
+
MEMORY["Memory System<br/>(Episodic/Semantic/Procedural)"]
|
|
469
|
+
COUNCIL["Completion Council<br/>(3-member voting)"]
|
|
470
|
+
QUEUE["Task Queue<br/>(.loki/queue/)"]
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
AGENTS --> QUEUE
|
|
474
|
+
VERIFY --> COUNCIL
|
|
475
|
+
REFLECT --> MEMORY
|
|
476
|
+
COMPOUND --> MEMORY
|
|
477
|
+
DASHBOARD -.->|"reads"| QUEUE
|
|
478
|
+
DASHBOARD -.->|"reads"| MEMORY
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
**Key components:**
|
|
482
|
+
- **RARV+C Cycle** -- Reason, Act, Reflect, Verify, Compound. Every iteration follows this loop. Failed verification triggers retry from Reason.
|
|
483
|
+
- **Provider Layer** -- Claude Code (full parallel agents, Task tool, MCP), Codex CLI and Gemini CLI (sequential, degraded mode).
|
|
484
|
+
- **Agent Swarms** -- 41 specialized agent types across 7 swarms, spawned on demand based on project complexity.
|
|
485
|
+
- **Completion Council** -- 3 members vote on whether the project is done. Anti-sycophancy devil's advocate on unanimous votes.
|
|
486
|
+
- **Memory System** -- Episodic traces, semantic patterns, procedural skills. Progressive disclosure reduces context usage by 60-80%.
|
|
487
|
+
- **Dashboard** -- FastAPI server reading `.loki/` flat files, with real-time web UI for task queue, agents, logs, and council state.
|
|
488
|
+
|
|
489
|
+
---
|
|
490
|
+
|
|
429
491
|
## CLI Commands (v4.1.0)
|
|
430
492
|
|
|
431
493
|
The `loki` CLI provides easy access to all Loki Mode features:
|
package/SKILL.md
CHANGED
|
@@ -3,7 +3,7 @@ name: loki-mode
|
|
|
3
3
|
description: Multi-agent autonomous startup system. Triggers on "Loki Mode". Takes PRD to deployed product with zero human intervention. Requires --dangerously-skip-permissions flag.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Loki Mode v5.
|
|
6
|
+
# Loki Mode v5.32.1
|
|
7
7
|
|
|
8
8
|
**You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
|
|
9
9
|
|
|
@@ -267,4 +267,4 @@ Auto-detected or force with `LOKI_COMPLEXITY`:
|
|
|
267
267
|
|
|
268
268
|
---
|
|
269
269
|
|
|
270
|
-
**v5.
|
|
270
|
+
**v5.32.1 | action.yml API key verification, fail-fast on missing key | ~280 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
5.
|
|
1
|
+
5.32.1
|
package/autonomy/loki
CHANGED
|
@@ -306,7 +306,7 @@ show_help() {
|
|
|
306
306
|
echo " stop Stop execution immediately"
|
|
307
307
|
echo " pause Pause after current session"
|
|
308
308
|
echo " resume Resume paused execution"
|
|
309
|
-
echo " status
|
|
309
|
+
echo " status [--json] Show current status (--json for machine-readable)"
|
|
310
310
|
echo " logs Show recent log output"
|
|
311
311
|
echo " dashboard [cmd] Dashboard server (start|stop|status|url|open)"
|
|
312
312
|
echo " provider [cmd] Manage AI provider (show|set|list|info)"
|
|
@@ -323,6 +323,7 @@ show_help() {
|
|
|
323
323
|
echo " council [cmd] Completion council (status|verdicts|convergence|force-review|report)"
|
|
324
324
|
echo " dogfood Show self-development statistics"
|
|
325
325
|
echo " reset [target] Reset session state (all|retries|failed)"
|
|
326
|
+
echo " doctor [--json] Check system prerequisites"
|
|
326
327
|
echo " version Show version"
|
|
327
328
|
echo " help Show this help"
|
|
328
329
|
echo ""
|
|
@@ -394,8 +395,8 @@ cmd_start() {
|
|
|
394
395
|
echo ""
|
|
395
396
|
echo "Environment Variables:"
|
|
396
397
|
echo " LOKI_PRD_FILE Path to PRD file (alternative to positional arg)"
|
|
397
|
-
echo " LOKI_AUTO_CONFIRM Set to 'true' to
|
|
398
|
-
echo " CI
|
|
398
|
+
echo " LOKI_AUTO_CONFIRM Set to 'true'/'false' to control prompts (takes precedence over CI)"
|
|
399
|
+
echo " CI Fallback: auto-confirms when 'true' and LOKI_AUTO_CONFIRM is unset"
|
|
399
400
|
echo " LOKI_MAX_ITERATIONS Max iteration count"
|
|
400
401
|
echo " LOKI_BUDGET_LIMIT Cost budget limit in USD"
|
|
401
402
|
echo ""
|
|
@@ -502,7 +503,10 @@ cmd_start() {
|
|
|
502
503
|
else
|
|
503
504
|
# No PRD file specified -- warn and confirm before consuming API credits
|
|
504
505
|
# Auto-confirm in CI environments or when LOKI_AUTO_CONFIRM is set
|
|
505
|
-
|
|
506
|
+
# LOKI_AUTO_CONFIRM takes precedence when explicitly set;
|
|
507
|
+
# fall back to CI env var only when LOKI_AUTO_CONFIRM is unset
|
|
508
|
+
local _auto_confirm="${LOKI_AUTO_CONFIRM:-${CI:-false}}"
|
|
509
|
+
if [[ "$_auto_confirm" == "true" ]]; then
|
|
506
510
|
echo -e "${YELLOW}Warning: No PRD file specified. Auto-confirming (CI mode).${NC}"
|
|
507
511
|
else
|
|
508
512
|
echo -e "${YELLOW}Warning: No PRD file specified.${NC}"
|
|
@@ -886,6 +890,167 @@ cmd_status() {
|
|
|
886
890
|
fi
|
|
887
891
|
}
|
|
888
892
|
|
|
893
|
+
# JSON output for loki status --json
|
|
894
|
+
cmd_status_json() {
|
|
895
|
+
local skill_dir="$SKILL_DIR"
|
|
896
|
+
local loki_dir="$LOKI_DIR"
|
|
897
|
+
local dashboard_port="${LOKI_DASHBOARD_PORT:-57374}"
|
|
898
|
+
local env_provider="${LOKI_PROVIDER:-claude}"
|
|
899
|
+
|
|
900
|
+
python3 -c "
|
|
901
|
+
import json, os, sys, time
|
|
902
|
+
|
|
903
|
+
skill_dir = sys.argv[1]
|
|
904
|
+
loki_dir = sys.argv[2]
|
|
905
|
+
dashboard_port = sys.argv[3]
|
|
906
|
+
env_provider = sys.argv[4]
|
|
907
|
+
result = {}
|
|
908
|
+
|
|
909
|
+
# Version
|
|
910
|
+
version_file = os.path.join(skill_dir, 'VERSION')
|
|
911
|
+
if os.path.isfile(version_file):
|
|
912
|
+
with open(version_file) as f:
|
|
913
|
+
result['version'] = f.read().strip()
|
|
914
|
+
else:
|
|
915
|
+
result['version'] = 'unknown'
|
|
916
|
+
|
|
917
|
+
# Check if session exists
|
|
918
|
+
if not os.path.isdir(loki_dir):
|
|
919
|
+
result['status'] = 'inactive'
|
|
920
|
+
result['phase'] = None
|
|
921
|
+
result['iteration'] = 0
|
|
922
|
+
result['provider'] = env_provider
|
|
923
|
+
result['dashboard_url'] = None
|
|
924
|
+
result['pid'] = None
|
|
925
|
+
result['elapsed_time'] = 0
|
|
926
|
+
result['task_counts'] = {'total': 0, 'completed': 0, 'failed': 0, 'pending': 0}
|
|
927
|
+
print(json.dumps(result, indent=2))
|
|
928
|
+
sys.exit(0)
|
|
929
|
+
|
|
930
|
+
# Status from signals and session.json
|
|
931
|
+
if os.path.isfile(os.path.join(loki_dir, 'PAUSE')):
|
|
932
|
+
result['status'] = 'paused'
|
|
933
|
+
elif os.path.isfile(os.path.join(loki_dir, 'STOP')):
|
|
934
|
+
result['status'] = 'stopped'
|
|
935
|
+
else:
|
|
936
|
+
session_file = os.path.join(loki_dir, 'session.json')
|
|
937
|
+
if os.path.isfile(session_file):
|
|
938
|
+
try:
|
|
939
|
+
with open(session_file) as f:
|
|
940
|
+
session = json.load(f)
|
|
941
|
+
result['status'] = session.get('status', 'unknown')
|
|
942
|
+
except Exception:
|
|
943
|
+
result['status'] = 'unknown'
|
|
944
|
+
else:
|
|
945
|
+
result['status'] = 'unknown'
|
|
946
|
+
|
|
947
|
+
# Phase and iteration from dashboard-state.json
|
|
948
|
+
ds_file = os.path.join(loki_dir, 'dashboard-state.json')
|
|
949
|
+
if os.path.isfile(ds_file):
|
|
950
|
+
try:
|
|
951
|
+
with open(ds_file) as f:
|
|
952
|
+
ds = json.load(f)
|
|
953
|
+
result['phase'] = ds.get('phase', ds.get('currentPhase'))
|
|
954
|
+
result['iteration'] = ds.get('iteration', ds.get('currentIteration', 0))
|
|
955
|
+
except Exception:
|
|
956
|
+
result['phase'] = None
|
|
957
|
+
result['iteration'] = 0
|
|
958
|
+
else:
|
|
959
|
+
orch_file = os.path.join(loki_dir, 'state', 'orchestrator.json')
|
|
960
|
+
if os.path.isfile(orch_file):
|
|
961
|
+
try:
|
|
962
|
+
with open(orch_file) as f:
|
|
963
|
+
orch = json.load(f)
|
|
964
|
+
result['phase'] = orch.get('currentPhase')
|
|
965
|
+
result['iteration'] = orch.get('currentIteration', 0)
|
|
966
|
+
except Exception:
|
|
967
|
+
result['phase'] = None
|
|
968
|
+
result['iteration'] = 0
|
|
969
|
+
else:
|
|
970
|
+
result['phase'] = None
|
|
971
|
+
result['iteration'] = 0
|
|
972
|
+
|
|
973
|
+
# Provider
|
|
974
|
+
provider_file = os.path.join(loki_dir, 'state', 'provider')
|
|
975
|
+
if os.path.isfile(provider_file):
|
|
976
|
+
with open(provider_file) as f:
|
|
977
|
+
result['provider'] = f.read().strip()
|
|
978
|
+
else:
|
|
979
|
+
result['provider'] = env_provider
|
|
980
|
+
|
|
981
|
+
# PID
|
|
982
|
+
pid_file = os.path.join(loki_dir, 'loki.pid')
|
|
983
|
+
if os.path.isfile(pid_file):
|
|
984
|
+
try:
|
|
985
|
+
with open(pid_file) as f:
|
|
986
|
+
result['pid'] = int(f.read().strip())
|
|
987
|
+
except (ValueError, Exception):
|
|
988
|
+
result['pid'] = None
|
|
989
|
+
else:
|
|
990
|
+
result['pid'] = None
|
|
991
|
+
|
|
992
|
+
# Elapsed time from session.json
|
|
993
|
+
session_file = os.path.join(loki_dir, 'session.json')
|
|
994
|
+
if os.path.isfile(session_file):
|
|
995
|
+
try:
|
|
996
|
+
with open(session_file) as f:
|
|
997
|
+
session = json.load(f)
|
|
998
|
+
start_time = session.get('start_time', session.get('startTime'))
|
|
999
|
+
if start_time:
|
|
1000
|
+
if isinstance(start_time, (int, float)):
|
|
1001
|
+
result['elapsed_time'] = int(time.time() - start_time)
|
|
1002
|
+
else:
|
|
1003
|
+
from datetime import datetime
|
|
1004
|
+
dt = datetime.fromisoformat(start_time.replace('Z', '+00:00'))
|
|
1005
|
+
result['elapsed_time'] = int(time.time() - dt.timestamp())
|
|
1006
|
+
else:
|
|
1007
|
+
result['elapsed_time'] = 0
|
|
1008
|
+
except Exception:
|
|
1009
|
+
result['elapsed_time'] = 0
|
|
1010
|
+
else:
|
|
1011
|
+
result['elapsed_time'] = 0
|
|
1012
|
+
|
|
1013
|
+
# Dashboard URL
|
|
1014
|
+
dashboard_pid_file = os.path.join(loki_dir, 'dashboard', 'dashboard.pid')
|
|
1015
|
+
dashboard_url = None
|
|
1016
|
+
if os.path.isfile(dashboard_pid_file):
|
|
1017
|
+
try:
|
|
1018
|
+
with open(dashboard_pid_file) as f:
|
|
1019
|
+
dpid = int(f.read().strip())
|
|
1020
|
+
os.kill(dpid, 0)
|
|
1021
|
+
dashboard_url = 'http://127.0.0.1:' + dashboard_port + '/'
|
|
1022
|
+
except (ProcessLookupError, PermissionError, ValueError, Exception):
|
|
1023
|
+
pass
|
|
1024
|
+
result['dashboard_url'] = dashboard_url
|
|
1025
|
+
|
|
1026
|
+
# Task counts from queue files
|
|
1027
|
+
task_counts = {'total': 0, 'completed': 0, 'failed': 0, 'pending': 0}
|
|
1028
|
+
queue_dir = os.path.join(loki_dir, 'queue')
|
|
1029
|
+
if os.path.isdir(queue_dir):
|
|
1030
|
+
for name, key in [('pending.json', 'pending'), ('completed.json', 'completed'), ('failed.json', 'failed')]:
|
|
1031
|
+
fpath = os.path.join(queue_dir, name)
|
|
1032
|
+
if os.path.isfile(fpath):
|
|
1033
|
+
try:
|
|
1034
|
+
with open(fpath) as f:
|
|
1035
|
+
data = json.load(f)
|
|
1036
|
+
if isinstance(data, list):
|
|
1037
|
+
task_counts[key] = len(data)
|
|
1038
|
+
elif isinstance(data, dict) and 'tasks' in data:
|
|
1039
|
+
task_counts[key] = len(data['tasks'])
|
|
1040
|
+
except Exception:
|
|
1041
|
+
pass
|
|
1042
|
+
task_counts['total'] = task_counts['pending'] + task_counts['completed'] + task_counts['failed']
|
|
1043
|
+
result['task_counts'] = task_counts
|
|
1044
|
+
|
|
1045
|
+
print(json.dumps(result, indent=2))
|
|
1046
|
+
" "$skill_dir" "$loki_dir" "$dashboard_port" "$env_provider"
|
|
1047
|
+
|
|
1048
|
+
if [ $? -ne 0 ]; then
|
|
1049
|
+
echo '{"error": "Failed to generate JSON status. Ensure python3 is available."}' >&2
|
|
1050
|
+
return 1
|
|
1051
|
+
fi
|
|
1052
|
+
}
|
|
1053
|
+
|
|
889
1054
|
# Provider management
|
|
890
1055
|
cmd_provider() {
|
|
891
1056
|
local subcommand="${1:-show}"
|
|
@@ -2293,6 +2458,290 @@ cmd_config_path() {
|
|
|
2293
2458
|
echo "Create a config file with: loki config init"
|
|
2294
2459
|
}
|
|
2295
2460
|
|
|
2461
|
+
# Check system prerequisites
|
|
2462
|
+
cmd_doctor() {
|
|
2463
|
+
local json_output=false
|
|
2464
|
+
|
|
2465
|
+
while [[ $# -gt 0 ]]; do
|
|
2466
|
+
case "$1" in
|
|
2467
|
+
--json)
|
|
2468
|
+
json_output=true
|
|
2469
|
+
shift
|
|
2470
|
+
;;
|
|
2471
|
+
--help|-h)
|
|
2472
|
+
echo -e "${BOLD}loki doctor${NC} - Check system prerequisites"
|
|
2473
|
+
echo ""
|
|
2474
|
+
echo "Usage: loki doctor [--json]"
|
|
2475
|
+
echo ""
|
|
2476
|
+
echo "Options:"
|
|
2477
|
+
echo " --json Output machine-readable JSON"
|
|
2478
|
+
echo ""
|
|
2479
|
+
echo "Checks: node, python3, jq, git, curl, bash version,"
|
|
2480
|
+
echo " claude/codex/gemini CLIs, and disk space."
|
|
2481
|
+
return 0
|
|
2482
|
+
;;
|
|
2483
|
+
*)
|
|
2484
|
+
echo -e "${RED}Unknown option: $1${NC}"
|
|
2485
|
+
echo "Usage: loki doctor [--json]"
|
|
2486
|
+
return 1
|
|
2487
|
+
;;
|
|
2488
|
+
esac
|
|
2489
|
+
done
|
|
2490
|
+
|
|
2491
|
+
if [ "$json_output" = true ]; then
|
|
2492
|
+
cmd_doctor_json
|
|
2493
|
+
return $?
|
|
2494
|
+
fi
|
|
2495
|
+
|
|
2496
|
+
echo -e "${BOLD}Loki Mode Doctor${NC}"
|
|
2497
|
+
echo ""
|
|
2498
|
+
echo "Checking system prerequisites..."
|
|
2499
|
+
echo ""
|
|
2500
|
+
|
|
2501
|
+
local pass_count=0
|
|
2502
|
+
local fail_count=0
|
|
2503
|
+
local warn_count=0
|
|
2504
|
+
|
|
2505
|
+
# Helper: check command exists and optionally check version
|
|
2506
|
+
doctor_check() {
|
|
2507
|
+
local name="$1"
|
|
2508
|
+
local cmd="$2"
|
|
2509
|
+
local required="$3" # required, recommended, optional
|
|
2510
|
+
local min_version="${4:-}"
|
|
2511
|
+
|
|
2512
|
+
if ! command -v "$cmd" &> /dev/null; then
|
|
2513
|
+
if [ "$required" = "required" ]; then
|
|
2514
|
+
echo -e " ${RED}FAIL${NC} $name - not found"
|
|
2515
|
+
fail_count=$((fail_count + 1))
|
|
2516
|
+
elif [ "$required" = "recommended" ]; then
|
|
2517
|
+
echo -e " ${YELLOW}WARN${NC} $name - not found (recommended)"
|
|
2518
|
+
warn_count=$((warn_count + 1))
|
|
2519
|
+
else
|
|
2520
|
+
echo -e " ${YELLOW}WARN${NC} $name - not found (optional)"
|
|
2521
|
+
warn_count=$((warn_count + 1))
|
|
2522
|
+
fi
|
|
2523
|
+
return 1
|
|
2524
|
+
fi
|
|
2525
|
+
|
|
2526
|
+
local version=""
|
|
2527
|
+
case "$cmd" in
|
|
2528
|
+
node)
|
|
2529
|
+
version=$(node --version 2>/dev/null | tr -d 'v')
|
|
2530
|
+
;;
|
|
2531
|
+
python3)
|
|
2532
|
+
version=$(python3 --version 2>/dev/null | awk '{print $2}')
|
|
2533
|
+
;;
|
|
2534
|
+
git)
|
|
2535
|
+
version=$(git --version 2>/dev/null | awk '{print $3}')
|
|
2536
|
+
;;
|
|
2537
|
+
bash)
|
|
2538
|
+
version=$("$cmd" --version 2>/dev/null | head -1 | sed 's/.*version \([0-9.]*\).*/\1/')
|
|
2539
|
+
;;
|
|
2540
|
+
jq)
|
|
2541
|
+
version=$(jq --version 2>/dev/null | tr -d 'jq-')
|
|
2542
|
+
;;
|
|
2543
|
+
curl)
|
|
2544
|
+
version=$(curl --version 2>/dev/null | head -1 | awk '{print $2}')
|
|
2545
|
+
;;
|
|
2546
|
+
claude)
|
|
2547
|
+
version=$(claude --version 2>/dev/null | head -1 | sed 's/[^0-9.]//g' | head -1)
|
|
2548
|
+
;;
|
|
2549
|
+
codex)
|
|
2550
|
+
version=$(codex --version 2>/dev/null | head -1 | sed 's/[^0-9.]//g' | head -1)
|
|
2551
|
+
;;
|
|
2552
|
+
gemini)
|
|
2553
|
+
version=$(gemini --version 2>/dev/null | head -1 | sed 's/[^0-9.]//g' | head -1)
|
|
2554
|
+
;;
|
|
2555
|
+
esac
|
|
2556
|
+
|
|
2557
|
+
local version_display=""
|
|
2558
|
+
if [ -n "$version" ]; then
|
|
2559
|
+
version_display=" (v$version)"
|
|
2560
|
+
fi
|
|
2561
|
+
|
|
2562
|
+
# Simple major version check if min_version is specified
|
|
2563
|
+
if [ -n "$min_version" ] && [ -n "$version" ]; then
|
|
2564
|
+
local cur_major cur_minor min_major min_minor
|
|
2565
|
+
cur_major=$(echo "$version" | cut -d. -f1)
|
|
2566
|
+
min_major=$(echo "$min_version" | cut -d. -f1)
|
|
2567
|
+
cur_minor=$(echo "$version" | cut -d. -f2)
|
|
2568
|
+
min_minor=$(echo "$min_version" | cut -d. -f2)
|
|
2569
|
+
|
|
2570
|
+
if [ "$cur_major" -lt "$min_major" ] 2>/dev/null || \
|
|
2571
|
+
{ [ "$cur_major" -eq "$min_major" ] 2>/dev/null && [ "$cur_minor" -lt "$min_minor" ] 2>/dev/null; }; then
|
|
2572
|
+
if [ "$required" = "required" ]; then
|
|
2573
|
+
echo -e " ${RED}FAIL${NC} $name$version_display - requires >= $min_version"
|
|
2574
|
+
fail_count=$((fail_count + 1))
|
|
2575
|
+
else
|
|
2576
|
+
echo -e " ${YELLOW}WARN${NC} $name$version_display - recommended >= $min_version"
|
|
2577
|
+
warn_count=$((warn_count + 1))
|
|
2578
|
+
fi
|
|
2579
|
+
return 1
|
|
2580
|
+
fi
|
|
2581
|
+
fi
|
|
2582
|
+
|
|
2583
|
+
echo -e " ${GREEN}PASS${NC} $name$version_display"
|
|
2584
|
+
pass_count=$((pass_count + 1))
|
|
2585
|
+
return 0
|
|
2586
|
+
}
|
|
2587
|
+
|
|
2588
|
+
echo -e "${CYAN}Required:${NC}"
|
|
2589
|
+
doctor_check "Node.js (>= 18)" node required 18.0
|
|
2590
|
+
doctor_check "Python 3 (>= 3.8)" python3 required 3.8
|
|
2591
|
+
doctor_check "jq" jq required
|
|
2592
|
+
doctor_check "git" git required
|
|
2593
|
+
doctor_check "curl" curl required
|
|
2594
|
+
echo ""
|
|
2595
|
+
|
|
2596
|
+
echo -e "${CYAN}AI Providers:${NC}"
|
|
2597
|
+
doctor_check "Claude CLI" claude optional
|
|
2598
|
+
doctor_check "Codex CLI" codex optional
|
|
2599
|
+
doctor_check "Gemini CLI" gemini optional
|
|
2600
|
+
echo ""
|
|
2601
|
+
|
|
2602
|
+
echo -e "${CYAN}System:${NC}"
|
|
2603
|
+
doctor_check "bash (>= 4.0)" bash recommended 4.0
|
|
2604
|
+
|
|
2605
|
+
# Disk space check
|
|
2606
|
+
local disk_avail
|
|
2607
|
+
if command -v df &> /dev/null; then
|
|
2608
|
+
# Get available space in GB (works on macOS and Linux)
|
|
2609
|
+
disk_avail=$(df -g "$HOME" 2>/dev/null | tail -1 | awk '{print $4}')
|
|
2610
|
+
if [ -z "$disk_avail" ]; then
|
|
2611
|
+
# Linux fallback (df -g may not work)
|
|
2612
|
+
disk_avail=$(df -BG "$HOME" 2>/dev/null | tail -1 | awk '{print $4}' | tr -d 'G')
|
|
2613
|
+
fi
|
|
2614
|
+
if [ -n "$disk_avail" ] && [ "$disk_avail" -gt 0 ] 2>/dev/null; then
|
|
2615
|
+
if [ "$disk_avail" -lt 1 ]; then
|
|
2616
|
+
echo -e " ${RED}FAIL${NC} Disk space: ${disk_avail}GB available (need >= 1GB)"
|
|
2617
|
+
fail_count=$((fail_count + 1))
|
|
2618
|
+
elif [ "$disk_avail" -lt 5 ]; then
|
|
2619
|
+
echo -e " ${YELLOW}WARN${NC} Disk space: ${disk_avail}GB available (low)"
|
|
2620
|
+
warn_count=$((warn_count + 1))
|
|
2621
|
+
else
|
|
2622
|
+
echo -e " ${GREEN}PASS${NC} Disk space: ${disk_avail}GB available"
|
|
2623
|
+
pass_count=$((pass_count + 1))
|
|
2624
|
+
fi
|
|
2625
|
+
else
|
|
2626
|
+
echo -e " ${YELLOW}WARN${NC} Disk space: unable to determine"
|
|
2627
|
+
warn_count=$((warn_count + 1))
|
|
2628
|
+
fi
|
|
2629
|
+
fi
|
|
2630
|
+
echo ""
|
|
2631
|
+
|
|
2632
|
+
# Summary
|
|
2633
|
+
echo -e "${BOLD}Summary:${NC} ${GREEN}$pass_count passed${NC}, ${RED}$fail_count failed${NC}, ${YELLOW}$warn_count warnings${NC}"
|
|
2634
|
+
echo ""
|
|
2635
|
+
|
|
2636
|
+
if [ "$fail_count" -gt 0 ]; then
|
|
2637
|
+
echo -e "${RED}Some required prerequisites are missing.${NC}"
|
|
2638
|
+
echo "Install missing dependencies and run 'loki doctor' again."
|
|
2639
|
+
return 1
|
|
2640
|
+
elif [ "$warn_count" -gt 0 ]; then
|
|
2641
|
+
echo -e "${YELLOW}All required checks passed with some warnings.${NC}"
|
|
2642
|
+
return 0
|
|
2643
|
+
else
|
|
2644
|
+
echo -e "${GREEN}All checks passed. System is ready for Loki Mode.${NC}"
|
|
2645
|
+
return 0
|
|
2646
|
+
fi
|
|
2647
|
+
}
|
|
2648
|
+
|
|
2649
|
+
# JSON output for loki doctor --json
|
|
2650
|
+
cmd_doctor_json() {
|
|
2651
|
+
python3 -c "
|
|
2652
|
+
import json, os, subprocess, sys, shutil
|
|
2653
|
+
|
|
2654
|
+
def get_version(cmd, args=None):
|
|
2655
|
+
try:
|
|
2656
|
+
if args is None:
|
|
2657
|
+
args = ['--version']
|
|
2658
|
+
result = subprocess.run([cmd] + args, capture_output=True, text=True, timeout=5)
|
|
2659
|
+
output = result.stdout.strip() or result.stderr.strip()
|
|
2660
|
+
# Extract version number
|
|
2661
|
+
import re
|
|
2662
|
+
match = re.search(r'(\d+\.\d+[\.\d]*)', output)
|
|
2663
|
+
return match.group(1) if match else None
|
|
2664
|
+
except (FileNotFoundError, subprocess.TimeoutExpired, Exception):
|
|
2665
|
+
return None
|
|
2666
|
+
|
|
2667
|
+
def check_tool(name, cmd, required, min_version=None):
|
|
2668
|
+
found = shutil.which(cmd) is not None
|
|
2669
|
+
version = get_version(cmd) if found else None
|
|
2670
|
+
status = 'pass'
|
|
2671
|
+
|
|
2672
|
+
if not found:
|
|
2673
|
+
status = 'fail' if required == 'required' else 'warn'
|
|
2674
|
+
elif min_version and version:
|
|
2675
|
+
cur_parts = [int(x) for x in version.split('.')[:2]]
|
|
2676
|
+
min_parts = [int(x) for x in min_version.split('.')[:2]]
|
|
2677
|
+
while len(cur_parts) < 2: cur_parts.append(0)
|
|
2678
|
+
while len(min_parts) < 2: min_parts.append(0)
|
|
2679
|
+
if cur_parts < min_parts:
|
|
2680
|
+
status = 'fail' if required == 'required' else 'warn'
|
|
2681
|
+
|
|
2682
|
+
return {
|
|
2683
|
+
'name': name,
|
|
2684
|
+
'command': cmd,
|
|
2685
|
+
'found': found,
|
|
2686
|
+
'version': version,
|
|
2687
|
+
'required': required,
|
|
2688
|
+
'min_version': min_version,
|
|
2689
|
+
'status': status,
|
|
2690
|
+
'path': shutil.which(cmd)
|
|
2691
|
+
}
|
|
2692
|
+
|
|
2693
|
+
checks = []
|
|
2694
|
+
checks.append(check_tool('Node.js', 'node', 'required', '18.0'))
|
|
2695
|
+
checks.append(check_tool('Python 3', 'python3', 'required', '3.8'))
|
|
2696
|
+
checks.append(check_tool('jq', 'jq', 'required'))
|
|
2697
|
+
checks.append(check_tool('git', 'git', 'required'))
|
|
2698
|
+
checks.append(check_tool('curl', 'curl', 'required'))
|
|
2699
|
+
checks.append(check_tool('bash', 'bash', 'recommended', '4.0'))
|
|
2700
|
+
checks.append(check_tool('Claude CLI', 'claude', 'optional'))
|
|
2701
|
+
checks.append(check_tool('Codex CLI', 'codex', 'optional'))
|
|
2702
|
+
checks.append(check_tool('Gemini CLI', 'gemini', 'optional'))
|
|
2703
|
+
|
|
2704
|
+
# Disk space
|
|
2705
|
+
disk_gb = None
|
|
2706
|
+
try:
|
|
2707
|
+
stat = os.statvfs(os.path.expanduser('~'))
|
|
2708
|
+
disk_gb = round((stat.f_bavail * stat.f_frsize) / (1024**3), 1)
|
|
2709
|
+
except Exception:
|
|
2710
|
+
pass
|
|
2711
|
+
|
|
2712
|
+
disk_status = 'pass'
|
|
2713
|
+
if disk_gb is not None:
|
|
2714
|
+
if disk_gb < 1:
|
|
2715
|
+
disk_status = 'fail'
|
|
2716
|
+
elif disk_gb < 5:
|
|
2717
|
+
disk_status = 'warn'
|
|
2718
|
+
|
|
2719
|
+
pass_count = sum(1 for c in checks if c['status'] == 'pass')
|
|
2720
|
+
fail_count = sum(1 for c in checks if c['status'] == 'fail')
|
|
2721
|
+
warn_count = sum(1 for c in checks if c['status'] == 'warn')
|
|
2722
|
+
|
|
2723
|
+
if disk_status == 'pass': pass_count += 1
|
|
2724
|
+
elif disk_status == 'fail': fail_count += 1
|
|
2725
|
+
elif disk_status == 'warn': warn_count += 1
|
|
2726
|
+
|
|
2727
|
+
result = {
|
|
2728
|
+
'checks': checks,
|
|
2729
|
+
'disk': {
|
|
2730
|
+
'available_gb': disk_gb,
|
|
2731
|
+
'status': disk_status
|
|
2732
|
+
},
|
|
2733
|
+
'summary': {
|
|
2734
|
+
'passed': pass_count,
|
|
2735
|
+
'failed': fail_count,
|
|
2736
|
+
'warnings': warn_count,
|
|
2737
|
+
'ok': fail_count == 0
|
|
2738
|
+
}
|
|
2739
|
+
}
|
|
2740
|
+
|
|
2741
|
+
print(json.dumps(result, indent=2))
|
|
2742
|
+
"
|
|
2743
|
+
}
|
|
2744
|
+
|
|
2296
2745
|
# Show version
|
|
2297
2746
|
cmd_version() {
|
|
2298
2747
|
echo "Loki Mode v$(get_version)"
|
|
@@ -3534,7 +3983,7 @@ main() {
|
|
|
3534
3983
|
cmd_resume
|
|
3535
3984
|
;;
|
|
3536
3985
|
status)
|
|
3537
|
-
cmd_status
|
|
3986
|
+
cmd_status "$@"
|
|
3538
3987
|
;;
|
|
3539
3988
|
dashboard)
|
|
3540
3989
|
cmd_dashboard "$@"
|
|
@@ -3590,6 +4039,9 @@ main() {
|
|
|
3590
4039
|
voice)
|
|
3591
4040
|
cmd_voice "$@"
|
|
3592
4041
|
;;
|
|
4042
|
+
doctor)
|
|
4043
|
+
cmd_doctor "$@"
|
|
4044
|
+
;;
|
|
3593
4045
|
version|--version|-v)
|
|
3594
4046
|
cmd_version
|
|
3595
4047
|
;;
|
|
@@ -3871,9 +4323,13 @@ for filename in ['patterns.jsonl', 'mistakes.jsonl', 'successes.jsonl']:
|
|
|
3871
4323
|
|
|
3872
4324
|
clear)
|
|
3873
4325
|
local type="${2:-}"
|
|
4326
|
+
local confirm=""
|
|
3874
4327
|
|
|
3875
4328
|
if [ -z "$type" ]; then
|
|
3876
|
-
|
|
4329
|
+
# LOKI_AUTO_CONFIRM takes precedence when explicitly set;
|
|
4330
|
+
# fall back to CI env var only when LOKI_AUTO_CONFIRM is unset
|
|
4331
|
+
local _auto_confirm="${LOKI_AUTO_CONFIRM:-${CI:-false}}"
|
|
4332
|
+
if [[ "$_auto_confirm" == "true" ]]; then
|
|
3877
4333
|
confirm="yes"
|
|
3878
4334
|
else
|
|
3879
4335
|
echo -e "${YELLOW}This will delete ALL cross-project learnings.${NC}"
|
package/dashboard/__init__.py
CHANGED
package/dashboard/server.py
CHANGED
|
@@ -2200,7 +2200,7 @@ except ImportError as e:
|
|
|
2200
2200
|
# =============================================================================
|
|
2201
2201
|
# Must be configured AFTER all API routes to avoid conflicts
|
|
2202
2202
|
|
|
2203
|
-
from fastapi.responses import FileResponse, HTMLResponse
|
|
2203
|
+
from fastapi.responses import FileResponse, HTMLResponse, Response
|
|
2204
2204
|
|
|
2205
2205
|
# Find static files in multiple possible locations
|
|
2206
2206
|
DASHBOARD_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
@@ -2240,6 +2240,17 @@ if STATIC_DIR:
|
|
|
2240
2240
|
if os.path.isdir(ASSETS_DIR):
|
|
2241
2241
|
app.mount("/assets", StaticFiles(directory=ASSETS_DIR), name="assets")
|
|
2242
2242
|
|
|
2243
|
+
# Serve favicon.svg from static directory
|
|
2244
|
+
@app.get("/favicon.svg", include_in_schema=False)
|
|
2245
|
+
async def serve_favicon():
|
|
2246
|
+
"""Serve the dashboard favicon."""
|
|
2247
|
+
if STATIC_DIR:
|
|
2248
|
+
favicon_path = os.path.join(STATIC_DIR, "favicon.svg")
|
|
2249
|
+
if os.path.isfile(favicon_path):
|
|
2250
|
+
return FileResponse(favicon_path, media_type="image/svg+xml")
|
|
2251
|
+
return Response(status_code=404)
|
|
2252
|
+
|
|
2253
|
+
|
|
2243
2254
|
# Serve index.html or standalone HTML for root
|
|
2244
2255
|
@app.get("/", include_in_schema=False)
|
|
2245
2256
|
async def serve_index():
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32">
|
|
2
|
+
<path d="M16 6C8 6 2 16 2 16s6 10 14 10 14-10 14-10S24 6 16 6z" fill="none" stroke="#7c3aed" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
3
|
+
<circle cx="16" cy="16" r="5" fill="#7c3aed"/>
|
|
4
|
+
<circle cx="16" cy="16" r="2" fill="#fff"/>
|
|
5
|
+
</svg>
|