loki-mode 7.5.31 → 7.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/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/loki +6 -6
- package/autonomy/run.sh +13 -4
- package/dashboard/__init__.py +1 -1
- package/dashboard/server.py +161 -0
- package/dashboard/static/index.html +83 -0
- package/docs/INSTALLATION.md +1 -1
- package/loki-ts/dist/loki.js +2 -2
- package/mcp/__init__.py +1 -1
- package/package.json +1 -1
- package/web-app/server.py +46 -28
package/SKILL.md
CHANGED
|
@@ -3,7 +3,7 @@ name: loki-mode
|
|
|
3
3
|
description: Multi-agent autonomous startup system. Triggers on "Loki Mode". Takes a spec (PRD, GitHub issue, OpenAPI doc, etc.) to deployed product with minimal human intervention. Requires --dangerously-skip-permissions flag.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Loki Mode v7.
|
|
6
|
+
# Loki Mode v7.6.0
|
|
7
7
|
|
|
8
8
|
**You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
|
|
9
9
|
|
|
@@ -381,4 +381,4 @@ See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.13] for the per-fix list and r
|
|
|
381
381
|
|
|
382
382
|
---
|
|
383
383
|
|
|
384
|
-
**v7.
|
|
384
|
+
**v7.6.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
7.
|
|
1
|
+
7.6.0
|
package/autonomy/loki
CHANGED
|
@@ -13999,7 +13999,7 @@ if total_removed > 0:
|
|
|
13999
13999
|
index)
|
|
14000
14000
|
# Show or rebuild the memory index layer
|
|
14001
14001
|
if [ "${2:-}" = "rebuild" ]; then
|
|
14002
|
-
python3 -c "
|
|
14002
|
+
PYTHONPATH="${SKILL_DIR}${PYTHONPATH:+:$PYTHONPATH}" python3 -c "
|
|
14003
14003
|
try:
|
|
14004
14004
|
from memory.layers import IndexLayer
|
|
14005
14005
|
layer = IndexLayer('.loki/memory')
|
|
@@ -14023,7 +14023,7 @@ except Exception as e:
|
|
|
14023
14023
|
consolidate)
|
|
14024
14024
|
# Run consolidation pipeline
|
|
14025
14025
|
local hours="${2:-24}"
|
|
14026
|
-
LOKI_HOURS="$hours" python3 -c "
|
|
14026
|
+
LOKI_HOURS="$hours" PYTHONPATH="${SKILL_DIR}${PYTHONPATH:+:$PYTHONPATH}" python3 -c "
|
|
14027
14027
|
import os
|
|
14028
14028
|
try:
|
|
14029
14029
|
from memory.consolidation import ConsolidationPipeline
|
|
@@ -14060,7 +14060,7 @@ except Exception as e:
|
|
|
14060
14060
|
echo "Usage: loki memory retrieve <query>"
|
|
14061
14061
|
exit 1
|
|
14062
14062
|
fi
|
|
14063
|
-
LOKI_QUERY="$query" python3 -c "
|
|
14063
|
+
LOKI_QUERY="$query" PYTHONPATH="${SKILL_DIR}${PYTHONPATH:+:$PYTHONPATH}" python3 -c "
|
|
14064
14064
|
import os
|
|
14065
14065
|
try:
|
|
14066
14066
|
from memory.retrieval import MemoryRetrieval
|
|
@@ -14091,7 +14091,7 @@ except Exception as e:
|
|
|
14091
14091
|
echo "Usage: loki memory episode <id>"
|
|
14092
14092
|
exit 1
|
|
14093
14093
|
fi
|
|
14094
|
-
LOKI_ID="$id" python3 -c "
|
|
14094
|
+
LOKI_ID="$id" PYTHONPATH="${SKILL_DIR}${PYTHONPATH:+:$PYTHONPATH}" python3 -c "
|
|
14095
14095
|
import os
|
|
14096
14096
|
try:
|
|
14097
14097
|
from memory.engine import MemoryEngine
|
|
@@ -14115,7 +14115,7 @@ except Exception as e:
|
|
|
14115
14115
|
local id="${2:-}"
|
|
14116
14116
|
if [ -z "$id" ]; then
|
|
14117
14117
|
# List all patterns
|
|
14118
|
-
python3 -c "
|
|
14118
|
+
PYTHONPATH="${SKILL_DIR}${PYTHONPATH:+:$PYTHONPATH}" python3 -c "
|
|
14119
14119
|
try:
|
|
14120
14120
|
from memory.engine import MemoryEngine
|
|
14121
14121
|
engine = MemoryEngine(base_path='.loki/memory')
|
|
@@ -14133,7 +14133,7 @@ except Exception as e:
|
|
|
14133
14133
|
print(f'Error: {e}')
|
|
14134
14134
|
" 2>/dev/null
|
|
14135
14135
|
else
|
|
14136
|
-
LOKI_ID="$id" python3 -c "
|
|
14136
|
+
LOKI_ID="$id" PYTHONPATH="${SKILL_DIR}${PYTHONPATH:+:$PYTHONPATH}" python3 -c "
|
|
14137
14137
|
import os
|
|
14138
14138
|
try:
|
|
14139
14139
|
from memory.engine import MemoryEngine
|
package/autonomy/run.sh
CHANGED
|
@@ -9189,6 +9189,13 @@ build_prompt() {
|
|
|
9189
9189
|
# Context Memory Instructions (integrated with new memory system)
|
|
9190
9190
|
local memory_instruction="MEMORY SYSTEM: Relevant context from past sessions is provided below (if any). Your actions will be automatically recorded for future reference. For complex handoffs: create .loki/memory/handoffs/{timestamp}.md. For important decisions: they will be captured in the timeline. Check .loki/CONTINUITY.md for session-level working memory."
|
|
9191
9191
|
|
|
9192
|
+
# USAGE.md instruction (v7.6.0) -- always-on end-user handoff doc.
|
|
9193
|
+
# REGARDLESS of whether the PRD mentions it, the agent MUST write USAGE.md
|
|
9194
|
+
# at the project root before signaling completion. This becomes the
|
|
9195
|
+
# canonical "how do I run and verify this" artifact surfaced to the user
|
|
9196
|
+
# and to the dashboard/Purple Lab UI.
|
|
9197
|
+
local usage_doc_instruction="USAGE_DOC_REQUIRED: Before invoking loki_complete_task (or touching .loki/signals/COMPLETION_REQUESTED), write USAGE.md at the project root. Detect the stack from package.json/requirements.txt/Cargo.toml/go.mod/etc. and include these sections: (1) Prerequisites (runtimes, ports, env vars), (2) Install (exact command, e.g. 'npm install' or 'pip install -r requirements.txt'), (3) Start (exact command, e.g. 'npm start' or 'python server.py'), (4) Verify -- 2 to 3 copy-paste commands the user can run to confirm it works (curl examples for APIs with expected output, browser URL for web UIs, command invocation for CLIs), (5) Stop (Ctrl+C or 'lsof -ti:PORT | xargs kill -9' for backgrounded servers). Keep it under 100 lines, plain Markdown, no emojis. If USAGE.md already exists and is accurate, leave it; otherwise create or update it."
|
|
9198
|
+
|
|
9192
9199
|
# Load existing context if resuming
|
|
9193
9200
|
local context_injection=""
|
|
9194
9201
|
if [ $retry -gt 0 ]; then
|
|
@@ -9518,15 +9525,15 @@ except Exception:
|
|
|
9518
9525
|
else
|
|
9519
9526
|
if [ $retry -eq 0 ]; then
|
|
9520
9527
|
if [ -n "$prd" ]; then
|
|
9521
|
-
echo "Loki Mode with PRD at $prd. $human_directive $gate_failure_context $queue_tasks $bmad_context $openspec_context $mirofish_context $magic_context $checklist_status $app_runner_info $playwright_info $memory_context_section $rarv_instruction $memory_instruction $completion_instruction $sdlc_instruction $autonomous_suffix"
|
|
9528
|
+
echo "Loki Mode with PRD at $prd. $human_directive $gate_failure_context $queue_tasks $bmad_context $openspec_context $mirofish_context $magic_context $checklist_status $app_runner_info $playwright_info $memory_context_section $rarv_instruction $memory_instruction $usage_doc_instruction $completion_instruction $sdlc_instruction $autonomous_suffix"
|
|
9522
9529
|
else
|
|
9523
|
-
echo "Loki Mode. $human_directive $gate_failure_context $queue_tasks $bmad_context $openspec_context $mirofish_context $magic_context $checklist_status $app_runner_info $playwright_info $memory_context_section $analysis_instruction $rarv_instruction $memory_instruction $completion_instruction $sdlc_instruction $autonomous_suffix"
|
|
9530
|
+
echo "Loki Mode. $human_directive $gate_failure_context $queue_tasks $bmad_context $openspec_context $mirofish_context $magic_context $checklist_status $app_runner_info $playwright_info $memory_context_section $analysis_instruction $rarv_instruction $memory_instruction $usage_doc_instruction $completion_instruction $sdlc_instruction $autonomous_suffix"
|
|
9524
9531
|
fi
|
|
9525
9532
|
else
|
|
9526
9533
|
if [ -n "$prd" ]; then
|
|
9527
|
-
echo "Loki Mode - Resume iteration #$iteration (retry #$retry). PRD: $prd. $human_directive $gate_failure_context $queue_tasks $bmad_context $openspec_context $mirofish_context $magic_context $checklist_status $app_runner_info $playwright_info $memory_context_section $rarv_instruction $memory_instruction $completion_instruction $sdlc_instruction $autonomous_suffix"
|
|
9534
|
+
echo "Loki Mode - Resume iteration #$iteration (retry #$retry). PRD: $prd. $human_directive $gate_failure_context $queue_tasks $bmad_context $openspec_context $mirofish_context $magic_context $checklist_status $app_runner_info $playwright_info $memory_context_section $rarv_instruction $memory_instruction $usage_doc_instruction $completion_instruction $sdlc_instruction $autonomous_suffix"
|
|
9528
9535
|
else
|
|
9529
|
-
echo "Loki Mode - Resume iteration #$iteration (retry #$retry). $human_directive $gate_failure_context $queue_tasks $bmad_context $openspec_context $mirofish_context $magic_context $checklist_status $app_runner_info $playwright_info $memory_context_section Use .loki/generated-prd.md if exists. $rarv_instruction $memory_instruction $completion_instruction $sdlc_instruction $autonomous_suffix"
|
|
9536
|
+
echo "Loki Mode - Resume iteration #$iteration (retry #$retry). $human_directive $gate_failure_context $queue_tasks $bmad_context $openspec_context $mirofish_context $magic_context $checklist_status $app_runner_info $playwright_info $memory_context_section Use .loki/generated-prd.md if exists. $rarv_instruction $memory_instruction $usage_doc_instruction $completion_instruction $sdlc_instruction $autonomous_suffix"
|
|
9530
9537
|
fi
|
|
9531
9538
|
fi
|
|
9532
9539
|
fi
|
|
@@ -9567,6 +9574,7 @@ except Exception:
|
|
|
9567
9574
|
else
|
|
9568
9575
|
printf 'You are a coding assistant. Analyze this codebase and suggest improvements. Write working code and commit changes.\n'
|
|
9569
9576
|
fi
|
|
9577
|
+
printf '%s\n' "$usage_doc_instruction"
|
|
9570
9578
|
printf '</loki_system>\n'
|
|
9571
9579
|
printf '[CACHE_BREAKPOINT]\n'
|
|
9572
9580
|
|
|
@@ -9597,6 +9605,7 @@ except Exception:
|
|
|
9597
9605
|
printf '%s\n' "$sdlc_instruction"
|
|
9598
9606
|
printf '%s\n' "$autonomous_suffix"
|
|
9599
9607
|
printf '%s\n' "$memory_instruction"
|
|
9608
|
+
printf '%s\n' "$usage_doc_instruction"
|
|
9600
9609
|
# For codebase-analysis mode (no PRD), analysis_instruction is part of the
|
|
9601
9610
|
# static prefix so it remains cache-stable.
|
|
9602
9611
|
if [ -z "$prd" ]; then
|
package/dashboard/__init__.py
CHANGED
package/dashboard/server.py
CHANGED
|
@@ -2688,6 +2688,167 @@ async def get_memory_timeline():
|
|
|
2688
2688
|
return {"entries": episodes, "lastUpdated": None}
|
|
2689
2689
|
|
|
2690
2690
|
|
|
2691
|
+
# ---------------------------------------------------------------------------
|
|
2692
|
+
# Memory File Browser (v7.6.0) - generic drill-down into .loki/memory/
|
|
2693
|
+
# ---------------------------------------------------------------------------
|
|
2694
|
+
# Exposes raw episodic/learnings/ledgers/handoffs files plus root-level
|
|
2695
|
+
# notes (decisions.md, mistakes.md, patterns.md, investigation-*.md, etc.)
|
|
2696
|
+
# so the dashboard can let users click through what the agent has stored.
|
|
2697
|
+
#
|
|
2698
|
+
# Safety: type is a whitelisted enum; path is validated by resolving against
|
|
2699
|
+
# the memory directory and rejecting anything outside it (also rejects
|
|
2700
|
+
# absolute paths and ".." segments before resolution).
|
|
2701
|
+
|
|
2702
|
+
_MEMORY_FILE_TYPES = {
|
|
2703
|
+
"episodic": "episodic",
|
|
2704
|
+
"learnings": "learnings",
|
|
2705
|
+
"ledgers": "ledgers",
|
|
2706
|
+
"handoffs": "handoffs",
|
|
2707
|
+
"semantic": "semantic",
|
|
2708
|
+
"skills": "skills",
|
|
2709
|
+
"root": "", # files directly under .loki/memory/
|
|
2710
|
+
}
|
|
2711
|
+
|
|
2712
|
+
_MEMORY_FILE_MAX_BYTES = 2 * 1024 * 1024 # 2 MiB cap per file read
|
|
2713
|
+
|
|
2714
|
+
|
|
2715
|
+
def _safe_memory_path(rel_path: str) -> _Path:
|
|
2716
|
+
"""Resolve rel_path under .loki/memory/ and reject traversal attempts.
|
|
2717
|
+
|
|
2718
|
+
Raises HTTPException(400) on bad input, HTTPException(403) on traversal.
|
|
2719
|
+
"""
|
|
2720
|
+
if not rel_path or not isinstance(rel_path, str):
|
|
2721
|
+
raise HTTPException(status_code=400, detail="path required")
|
|
2722
|
+
# Reject NULs and absolute paths up front
|
|
2723
|
+
if "\x00" in rel_path or rel_path.startswith("/") or rel_path.startswith("\\"):
|
|
2724
|
+
raise HTTPException(status_code=400, detail="invalid path")
|
|
2725
|
+
# Reject explicit traversal segments before touching the filesystem
|
|
2726
|
+
parts = rel_path.replace("\\", "/").split("/")
|
|
2727
|
+
if any(p == ".." for p in parts):
|
|
2728
|
+
raise HTTPException(status_code=403, detail="path traversal blocked")
|
|
2729
|
+
memory_dir = _get_loki_dir() / "memory"
|
|
2730
|
+
try:
|
|
2731
|
+
real_memory = os.path.realpath(str(memory_dir))
|
|
2732
|
+
except Exception:
|
|
2733
|
+
raise HTTPException(status_code=500, detail="memory dir unavailable")
|
|
2734
|
+
candidate = memory_dir / rel_path
|
|
2735
|
+
try:
|
|
2736
|
+
resolved = os.path.realpath(str(candidate))
|
|
2737
|
+
except Exception:
|
|
2738
|
+
raise HTTPException(status_code=400, detail="invalid path")
|
|
2739
|
+
# Must live strictly under real_memory (not be it, not escape via symlink)
|
|
2740
|
+
if not (resolved == real_memory or resolved.startswith(real_memory + os.sep)):
|
|
2741
|
+
raise HTTPException(status_code=403, detail="path outside memory dir")
|
|
2742
|
+
return _Path(resolved)
|
|
2743
|
+
|
|
2744
|
+
|
|
2745
|
+
@app.get("/api/memory/files", dependencies=[Depends(auth.require_scope("read"))])
|
|
2746
|
+
async def list_memory_files(
|
|
2747
|
+
type: str = Query(default="root", description="One of: episodic, learnings, ledgers, handoffs, semantic, skills, root"),
|
|
2748
|
+
limit: int = Query(default=500, ge=1, le=2000),
|
|
2749
|
+
):
|
|
2750
|
+
"""List files under a memory subdirectory.
|
|
2751
|
+
|
|
2752
|
+
Returns: {type, dir, files: [{path, name, size, modified, kind}]}
|
|
2753
|
+
`path` is relative to .loki/memory/ and safe to pass back to /api/memory/file.
|
|
2754
|
+
"""
|
|
2755
|
+
if type not in _MEMORY_FILE_TYPES:
|
|
2756
|
+
raise HTTPException(status_code=400, detail=f"unknown type; expected one of {sorted(_MEMORY_FILE_TYPES)}")
|
|
2757
|
+
sub = _MEMORY_FILE_TYPES[type]
|
|
2758
|
+
memory_dir = _get_loki_dir() / "memory"
|
|
2759
|
+
target_dir = memory_dir / sub if sub else memory_dir
|
|
2760
|
+
if not target_dir.exists():
|
|
2761
|
+
return {"type": type, "dir": str(target_dir), "files": []}
|
|
2762
|
+
|
|
2763
|
+
real_memory = os.path.realpath(str(memory_dir))
|
|
2764
|
+
entries: list[dict[str, Any]] = []
|
|
2765
|
+
|
|
2766
|
+
if type == "root":
|
|
2767
|
+
# Only files directly under .loki/memory/ (don't descend into subdirs)
|
|
2768
|
+
iterator = (p for p in target_dir.iterdir() if p.is_file())
|
|
2769
|
+
elif type == "episodic":
|
|
2770
|
+
# Episodic is organized by date subdirectory; walk one level.
|
|
2771
|
+
def _ep_iter():
|
|
2772
|
+
for child in target_dir.iterdir():
|
|
2773
|
+
if child.is_file():
|
|
2774
|
+
yield child
|
|
2775
|
+
elif child.is_dir():
|
|
2776
|
+
for f in child.iterdir():
|
|
2777
|
+
if f.is_file():
|
|
2778
|
+
yield f
|
|
2779
|
+
iterator = _ep_iter()
|
|
2780
|
+
else:
|
|
2781
|
+
# Flat directory listing (learnings, ledgers, handoffs, semantic, skills)
|
|
2782
|
+
iterator = (p for p in target_dir.rglob("*") if p.is_file())
|
|
2783
|
+
|
|
2784
|
+
for f in iterator:
|
|
2785
|
+
try:
|
|
2786
|
+
resolved = os.path.realpath(str(f))
|
|
2787
|
+
if not resolved.startswith(real_memory + os.sep):
|
|
2788
|
+
continue # skip symlinks escaping the memory dir
|
|
2789
|
+
rel = os.path.relpath(resolved, real_memory)
|
|
2790
|
+
st = f.stat()
|
|
2791
|
+
entries.append({
|
|
2792
|
+
"path": rel,
|
|
2793
|
+
"name": f.name,
|
|
2794
|
+
"size": st.st_size,
|
|
2795
|
+
"modified": st.st_mtime,
|
|
2796
|
+
"kind": f.suffix.lstrip(".").lower() or "txt",
|
|
2797
|
+
})
|
|
2798
|
+
except Exception:
|
|
2799
|
+
continue
|
|
2800
|
+
if len(entries) >= limit:
|
|
2801
|
+
break
|
|
2802
|
+
|
|
2803
|
+
# Newest first
|
|
2804
|
+
entries.sort(key=lambda e: e["modified"], reverse=True)
|
|
2805
|
+
return {"type": type, "dir": str(target_dir), "files": entries}
|
|
2806
|
+
|
|
2807
|
+
|
|
2808
|
+
@app.get("/api/memory/file", dependencies=[Depends(auth.require_scope("read"))])
|
|
2809
|
+
async def get_memory_file(
|
|
2810
|
+
path: str = Query(..., min_length=1, max_length=512, description="Path relative to .loki/memory/"),
|
|
2811
|
+
):
|
|
2812
|
+
"""Read a single file under .loki/memory/ with strict path-traversal guards.
|
|
2813
|
+
|
|
2814
|
+
Returns: {path, name, size, modified, kind, content, truncated}
|
|
2815
|
+
JSON files are returned with content as a string; the caller can JSON.parse.
|
|
2816
|
+
"""
|
|
2817
|
+
target = _safe_memory_path(path)
|
|
2818
|
+
if not target.exists():
|
|
2819
|
+
raise HTTPException(status_code=404, detail="file not found")
|
|
2820
|
+
if not target.is_file():
|
|
2821
|
+
raise HTTPException(status_code=400, detail="not a file")
|
|
2822
|
+
try:
|
|
2823
|
+
st = target.stat()
|
|
2824
|
+
except Exception:
|
|
2825
|
+
raise HTTPException(status_code=500, detail="stat failed")
|
|
2826
|
+
truncated = False
|
|
2827
|
+
try:
|
|
2828
|
+
if st.st_size > _MEMORY_FILE_MAX_BYTES:
|
|
2829
|
+
with open(target, "rb") as fh:
|
|
2830
|
+
raw = fh.read(_MEMORY_FILE_MAX_BYTES)
|
|
2831
|
+
truncated = True
|
|
2832
|
+
else:
|
|
2833
|
+
with open(target, "rb") as fh:
|
|
2834
|
+
raw = fh.read()
|
|
2835
|
+
# Decode as UTF-8 with replacement so we never 500 on a stray byte.
|
|
2836
|
+
content = raw.decode("utf-8", errors="replace")
|
|
2837
|
+
except HTTPException:
|
|
2838
|
+
raise
|
|
2839
|
+
except Exception as e:
|
|
2840
|
+
raise HTTPException(status_code=500, detail=f"read failed: {e}")
|
|
2841
|
+
return {
|
|
2842
|
+
"path": path,
|
|
2843
|
+
"name": target.name,
|
|
2844
|
+
"size": st.st_size,
|
|
2845
|
+
"modified": st.st_mtime,
|
|
2846
|
+
"kind": target.suffix.lstrip(".").lower() or "txt",
|
|
2847
|
+
"content": content,
|
|
2848
|
+
"truncated": truncated,
|
|
2849
|
+
}
|
|
2850
|
+
|
|
2851
|
+
|
|
2691
2852
|
# ---------------------------------------------------------------------------
|
|
2692
2853
|
# Memory Search & Stats (v6.15.0) - SQLite FTS5 powered
|
|
2693
2854
|
# ---------------------------------------------------------------------------
|
|
@@ -615,6 +615,89 @@
|
|
|
615
615
|
<h3 style="font-family: 'DM Serif Display', Georgia, serif; font-size: 1.15rem; font-weight: 400; color: var(--loki-text-primary); margin-bottom: 12px;">Memory</h3>
|
|
616
616
|
<loki-memory-browser id="memory-browser" tab="summary"></loki-memory-browser>
|
|
617
617
|
</div>
|
|
618
|
+
<div>
|
|
619
|
+
<h3 style="font-family: 'DM Serif Display', Georgia, serif; font-size: 1.15rem; font-weight: 400; color: var(--loki-text-primary); margin-bottom: 12px;">Memory Files</h3>
|
|
620
|
+
<div id="memory-files-panel" style="background: var(--loki-bg-card, #1a1a1a); border: 1px solid var(--loki-border, #333); border-radius: 5px; padding: 12px;">
|
|
621
|
+
<div style="display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 10px;" id="memory-files-tabs"></div>
|
|
622
|
+
<div style="display: grid; grid-template-columns: minmax(220px, 320px) 1fr; gap: 12px; min-height: 280px;">
|
|
623
|
+
<div id="memory-files-list" style="border: 1px solid var(--loki-border, #333); border-radius: 4px; padding: 8px; max-height: 480px; overflow-y: auto; font-size: 12px;">Loading...</div>
|
|
624
|
+
<div id="memory-files-viewer" style="border: 1px solid var(--loki-border, #333); border-radius: 4px; padding: 12px; max-height: 480px; overflow: auto; font-family: 'JetBrains Mono', monospace; font-size: 12px; white-space: pre-wrap; word-break: break-word; color: var(--loki-text-primary, #eee);">Select a file to view its contents.</div>
|
|
625
|
+
</div>
|
|
626
|
+
</div>
|
|
627
|
+
<script>
|
|
628
|
+
(function(){
|
|
629
|
+
var TYPES = [
|
|
630
|
+
{id: 'root', label: 'Notes'},
|
|
631
|
+
{id: 'episodic', label: 'Episodes'},
|
|
632
|
+
{id: 'learnings', label: 'Learnings'},
|
|
633
|
+
{id: 'ledgers', label: 'Ledgers'},
|
|
634
|
+
{id: 'handoffs', label: 'Handoffs'},
|
|
635
|
+
{id: 'semantic', label: 'Semantic'},
|
|
636
|
+
{id: 'skills', label: 'Skills'}
|
|
637
|
+
];
|
|
638
|
+
var state = {type: 'root', files: [], selected: null};
|
|
639
|
+
var tabsEl = document.getElementById('memory-files-tabs');
|
|
640
|
+
var listEl = document.getElementById('memory-files-list');
|
|
641
|
+
var viewEl = document.getElementById('memory-files-viewer');
|
|
642
|
+
function esc(s){ var d = document.createElement('div'); d.textContent = String(s == null ? '' : s); return d.innerHTML; }
|
|
643
|
+
function fmtSize(n){ if (n < 1024) return n + ' B'; if (n < 1024*1024) return (n/1024).toFixed(1) + ' KB'; return (n/1024/1024).toFixed(2) + ' MB'; }
|
|
644
|
+
function fmtTime(ts){ try { return new Date(ts*1000).toLocaleString(); } catch(e){ return ''; } }
|
|
645
|
+
function renderTabs(){
|
|
646
|
+
tabsEl.innerHTML = TYPES.map(function(t){
|
|
647
|
+
var active = t.id === state.type;
|
|
648
|
+
return '<button data-type="' + t.id + '" style="padding: 5px 10px; font-size: 11px; border-radius: 4px; border: 1px solid ' + (active ? 'var(--loki-accent, #553de9)' : 'var(--loki-border, #333)') + '; background: ' + (active ? 'var(--loki-accent, #553de9)' : 'transparent') + '; color: ' + (active ? '#fff' : 'var(--loki-text-primary, #ccc)') + '; cursor: pointer;">' + esc(t.label) + '</button>';
|
|
649
|
+
}).join('');
|
|
650
|
+
Array.prototype.forEach.call(tabsEl.querySelectorAll('button'), function(b){
|
|
651
|
+
b.addEventListener('click', function(){ loadType(b.getAttribute('data-type')); });
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
function renderList(){
|
|
655
|
+
if (!state.files.length){ listEl.innerHTML = '<div style="color: var(--loki-text-muted, #888); padding: 8px;">No files in this section.</div>'; return; }
|
|
656
|
+
listEl.innerHTML = state.files.map(function(f){
|
|
657
|
+
var active = state.selected && state.selected.path === f.path;
|
|
658
|
+
return '<div data-path="' + esc(f.path) + '" style="padding: 6px 8px; border-radius: 3px; cursor: pointer; margin-bottom: 2px; background: ' + (active ? 'var(--loki-accent, #553de9)' : 'transparent') + '; color: ' + (active ? '#fff' : 'inherit') + ';" title="' + esc(f.path) + '"><div style="font-weight: 500;">' + esc(f.name) + '</div><div style="font-size: 10px; color: ' + (active ? 'rgba(255,255,255,0.8)' : 'var(--loki-text-muted, #888)') + ';">' + fmtSize(f.size) + ' -- ' + esc(fmtTime(f.modified)) + '</div></div>';
|
|
659
|
+
}).join('');
|
|
660
|
+
Array.prototype.forEach.call(listEl.querySelectorAll('div[data-path]'), function(d){
|
|
661
|
+
d.addEventListener('click', function(){ loadFile(d.getAttribute('data-path')); });
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
function renderViewer(file){
|
|
665
|
+
if (!file){ viewEl.textContent = 'Select a file to view its contents.'; return; }
|
|
666
|
+
var header = file.name + ' (' + fmtSize(file.size) + (file.truncated ? ', truncated' : '') + ')
|
|
667
|
+
' + file.path + '
|
|
668
|
+
|
|
669
|
+
';
|
|
670
|
+
var body = file.content || '';
|
|
671
|
+
if (file.kind === 'json'){
|
|
672
|
+
try { body = JSON.stringify(JSON.parse(body), null, 2); } catch(e){ /* leave raw */ }
|
|
673
|
+
}
|
|
674
|
+
viewEl.textContent = header + body;
|
|
675
|
+
}
|
|
676
|
+
function loadType(t){
|
|
677
|
+
state.type = t; state.selected = null;
|
|
678
|
+
renderTabs(); renderViewer(null);
|
|
679
|
+
listEl.innerHTML = 'Loading...';
|
|
680
|
+
fetch('/api/memory/files?type=' + encodeURIComponent(t) + '&limit=500')
|
|
681
|
+
.then(function(r){ if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); })
|
|
682
|
+
.then(function(j){ state.files = (j && j.files) || []; renderList(); })
|
|
683
|
+
.catch(function(e){ listEl.innerHTML = '<div style="color: var(--loki-red, #e74c3c); padding: 8px;">Failed: ' + esc(e.message) + '</div>'; });
|
|
684
|
+
}
|
|
685
|
+
function loadFile(p){
|
|
686
|
+
state.selected = state.files.filter(function(f){ return f.path === p; })[0] || {path: p};
|
|
687
|
+
renderList();
|
|
688
|
+
viewEl.textContent = 'Loading ' + p + '...';
|
|
689
|
+
fetch('/api/memory/file?path=' + encodeURIComponent(p))
|
|
690
|
+
.then(function(r){ if (!r.ok) return r.json().then(function(j){ throw new Error(j.detail || ('HTTP ' + r.status)); }); return r.json(); })
|
|
691
|
+
.then(function(j){ renderViewer(j); })
|
|
692
|
+
.catch(function(e){ viewEl.textContent = 'Failed: ' + e.message; });
|
|
693
|
+
}
|
|
694
|
+
// Initialize when Insights becomes visible (or immediately if already visible)
|
|
695
|
+
function init(){ renderTabs(); loadType('root'); }
|
|
696
|
+
if (document.getElementById('page-insights')){ init(); }
|
|
697
|
+
else { document.addEventListener('DOMContentLoaded', init); }
|
|
698
|
+
})();
|
|
699
|
+
</script>
|
|
700
|
+
</div>
|
|
618
701
|
<div>
|
|
619
702
|
<h3 style="font-family: 'DM Serif Display', Georgia, serif; font-size: 1.15rem; font-weight: 400; color: var(--loki-text-primary); margin-bottom: 12px;">Learning Metrics</h3>
|
|
620
703
|
<loki-learning-dashboard id="learning-dashboard" time-range="7d"></loki-learning-dashboard>
|
package/docs/INSTALLATION.md
CHANGED
package/loki-ts/dist/loki.js
CHANGED
|
@@ -451,7 +451,7 @@ Subcommands:
|
|
|
451
451
|
|
|
452
452
|
This command is invoked by autonomy/run.sh between iterations. Users
|
|
453
453
|
should not run it directly -- run \`loki start\` instead.
|
|
454
|
-
`,iK;var T7=L(()=>{y();_1();iK=o1});y();import{readFileSync as E7}from"fs";import{resolve as x7,dirname as F7}from"path";import{fileURLToPath as w7}from"url";var o=null;function i1(){if(o!==null)return o;let K="7.
|
|
454
|
+
`,iK;var T7=L(()=>{y();_1();iK=o1});y();import{readFileSync as E7}from"fs";import{resolve as x7,dirname as F7}from"path";import{fileURLToPath as w7}from"url";var o=null;function i1(){if(o!==null)return o;let K="7.6.0";if(typeof K==="string"&&K.length>0)return o=K,o;try{let $=F7(w7(import.meta.url)),z=S1($);o=E7(x7(z,"VERSION"),"utf-8").trim()}catch{o="unknown"}return o}function e1(){return process.stdout.write(`Loki Mode v${i1()}
|
|
455
455
|
`),0}p();n();y();import{readFileSync as C7,existsSync as h7}from"fs";import{resolve as b7}from"path";var y7=["claude","codex","cline","aider"];function $0(){let K=b7(P(),"state","provider");if(!h7(K))return"";try{return C7(K,"utf-8").trim()}catch{return""}}function v7(K,$){return K||$||process.env.LOKI_PROVIDER||"claude"}function g7(K){let $=$0(),z=v7(K,$);switch(process.stdout.write(`${k}Current Provider${W}
|
|
456
456
|
`),process.stdout.write(`
|
|
457
457
|
`),process.stdout.write(`${O}Provider:${W} ${z}
|
|
@@ -534,4 +534,4 @@ Set LOKI_LEGACY_BASH=1 to force the bash CLI for every command.
|
|
|
534
534
|
`),2}default:return process.stderr.write(`Unknown command: ${$}
|
|
535
535
|
`),process.stderr.write(A7),2}}process.on("SIGINT",()=>process.exit(130));process.on("SIGTERM",()=>process.exit(143));var $6=await K6(Bun.argv.slice(2));process.exit($6);
|
|
536
536
|
|
|
537
|
-
//# debugId=
|
|
537
|
+
//# debugId=1FE99E821A4C6C7B64756E2164756E21
|
package/mcp/__init__.py
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "loki-mode",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.6.0",
|
|
4
4
|
"description": "Loki Mode by Autonomi. Multi-agent autonomous SDLC framework. Spec to deployed app: PRD, GitHub issue, OpenAPI/JSON/YAML, or one-line brief. 4 AI providers (Claude Code, OpenAI Codex, Cline, Aider). 11 quality gates.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent",
|
package/web-app/server.py
CHANGED
|
@@ -7720,36 +7720,15 @@ async def get_audit_log() -> JSONResponse:
|
|
|
7720
7720
|
|
|
7721
7721
|
# ---------------------------------------------------------------------------
|
|
7722
7722
|
# Static file serving (built React app)
|
|
7723
|
+
# IMPORTANT: serve_spa was previously defined here at line 7725, BEFORE the
|
|
7724
|
+
# /api/magic, /api/deploy, /api/sessions/.../github/actions, and
|
|
7725
|
+
# /api/sessions/.../docs route registrations. FastAPI registers routes in
|
|
7726
|
+
# definition order, so the catch-all '/{full_path:path}' silently swallowed
|
|
7727
|
+
# those 22 endpoints (they returned index.html instead of JSON). Moved to
|
|
7728
|
+
# the very end of the file just before standalone_app in v7.6.0 so all
|
|
7729
|
+
# specific API routes register first.
|
|
7723
7730
|
# ---------------------------------------------------------------------------
|
|
7724
7731
|
|
|
7725
|
-
@app.get("/{full_path:path}")
|
|
7726
|
-
async def serve_spa(full_path: str) -> FileResponse:
|
|
7727
|
-
"""Serve the React SPA and static assets from dist/.
|
|
7728
|
-
|
|
7729
|
-
The Vite build is configured with base: '/lab/' (Phase Merge-3), so the
|
|
7730
|
-
bundled HTML references assets at '/lab/assets/...'. Under the canonical
|
|
7731
|
-
standalone_app (and dashboard's Merge-4 mount), Starlette's Mount strips
|
|
7732
|
-
'/lab' from the scope path before dispatch, so this handler usually sees
|
|
7733
|
-
'assets/...' directly and the strip below is a no-op. The strip is
|
|
7734
|
-
retained as defense-in-depth for direct-`app` invocations (e.g. tests or
|
|
7735
|
-
operators running `uvicorn server:app` instead of `server:standalone_app`).
|
|
7736
|
-
"""
|
|
7737
|
-
index = DIST_DIR / "index.html"
|
|
7738
|
-
if not index.exists():
|
|
7739
|
-
return JSONResponse(
|
|
7740
|
-
status_code=503,
|
|
7741
|
-
content={"error": "Web app not built. Run: cd web-app && npm run build"},
|
|
7742
|
-
)
|
|
7743
|
-
# Resolve to the dist directory, tolerating the /lab/ base in standalone mode.
|
|
7744
|
-
relative = full_path[4:] if full_path.startswith("lab/") else full_path
|
|
7745
|
-
requested = DIST_DIR / relative
|
|
7746
|
-
if relative and requested.is_file() and str(requested.resolve()).startswith(str(DIST_DIR.resolve())):
|
|
7747
|
-
import mimetypes
|
|
7748
|
-
content_type = mimetypes.guess_type(str(requested))[0] or "application/octet-stream"
|
|
7749
|
-
return FileResponse(str(requested), media_type=content_type)
|
|
7750
|
-
# SPA fallback: return index.html for all non-file routes
|
|
7751
|
-
return FileResponse(str(index))
|
|
7752
|
-
|
|
7753
7732
|
|
|
7754
7733
|
# ---------------------------------------------------------------------------
|
|
7755
7734
|
# Magic Modules API (/api/magic/*)
|
|
@@ -8649,6 +8628,45 @@ async def docs_get_file(session_id: str, filename: str) -> Response:
|
|
|
8649
8628
|
return Response(content=content, media_type="text/markdown")
|
|
8650
8629
|
|
|
8651
8630
|
|
|
8631
|
+
# ---------------------------------------------------------------------------
|
|
8632
|
+
# SPA catch-all (MUST be last @app route -- see comment at the old serve_spa
|
|
8633
|
+
# location higher in this file). Any path that isn't matched by a specific
|
|
8634
|
+
# @app.get/post/... above falls through here and serves the React bundle.
|
|
8635
|
+
# v7.6.0 bug fix: previously this was at line 7725 and silently swallowed
|
|
8636
|
+
# 22 downstream API routes (/api/magic/*, /api/deploy/*, /api/sessions/.../
|
|
8637
|
+
# github/actions/*, /api/sessions/.../docs/*) which returned text/html
|
|
8638
|
+
# instead of JSON. Real-user test (Playwright on /lab/magic) surfaced it.
|
|
8639
|
+
# ---------------------------------------------------------------------------
|
|
8640
|
+
|
|
8641
|
+
@app.get("/{full_path:path}")
|
|
8642
|
+
async def serve_spa(full_path: str) -> FileResponse:
|
|
8643
|
+
"""Serve the React SPA and static assets from dist/.
|
|
8644
|
+
|
|
8645
|
+
The Vite build is configured with base: '/lab/' (Phase Merge-3), so the
|
|
8646
|
+
bundled HTML references assets at '/lab/assets/...'. Under the canonical
|
|
8647
|
+
standalone_app (and dashboard's Merge-4 mount), Starlette's Mount strips
|
|
8648
|
+
'/lab' from the scope path before dispatch, so this handler usually sees
|
|
8649
|
+
'assets/...' directly and the strip below is a no-op. The strip is
|
|
8650
|
+
retained as defense-in-depth for direct-`app` invocations (e.g. tests or
|
|
8651
|
+
operators running `uvicorn server:app` instead of `server:standalone_app`).
|
|
8652
|
+
"""
|
|
8653
|
+
index = DIST_DIR / "index.html"
|
|
8654
|
+
if not index.exists():
|
|
8655
|
+
return JSONResponse(
|
|
8656
|
+
status_code=503,
|
|
8657
|
+
content={"error": "Web app not built. Run: cd web-app && npm run build"},
|
|
8658
|
+
)
|
|
8659
|
+
# Resolve to the dist directory, tolerating the /lab/ base in standalone mode.
|
|
8660
|
+
relative = full_path[4:] if full_path.startswith("lab/") else full_path
|
|
8661
|
+
requested = DIST_DIR / relative
|
|
8662
|
+
if relative and requested.is_file() and str(requested.resolve()).startswith(str(DIST_DIR.resolve())):
|
|
8663
|
+
import mimetypes
|
|
8664
|
+
content_type = mimetypes.guess_type(str(requested))[0] or "application/octet-stream"
|
|
8665
|
+
return FileResponse(str(requested), media_type=content_type)
|
|
8666
|
+
# SPA fallback: return index.html for all non-file routes
|
|
8667
|
+
return FileResponse(str(index))
|
|
8668
|
+
|
|
8669
|
+
|
|
8652
8670
|
# ---------------------------------------------------------------------------
|
|
8653
8671
|
# Standalone wrapper (Phase Merge-3): mounts the FastAPI `app` under '/lab/'
|
|
8654
8672
|
# so the rebased Vite bundle (with base: '/lab/') routes correctly through
|