loki-mode 7.7.19 → 7.7.21
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 +236 -9
- package/dashboard/__init__.py +1 -1
- package/dashboard/server.py +105 -6
- package/dashboard/static/index.html +49 -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/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.7.
|
|
6
|
+
# Loki Mode v7.7.21
|
|
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.7.
|
|
384
|
+
**v7.7.21 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
7.7.
|
|
1
|
+
7.7.21
|
package/autonomy/loki
CHANGED
|
@@ -377,6 +377,30 @@ try:
|
|
|
377
377
|
}
|
|
378
378
|
output['memories'].append(memory_item)
|
|
379
379
|
|
|
380
|
+
# v7.7.20: wake the previously-dead cross-project + knowledge-graph
|
|
381
|
+
# code. When local episodic retrieval is sparse (< 5 hits), augment
|
|
382
|
+
# with patterns mined from OTHER projects' .loki/memory/ stores. This
|
|
383
|
+
# is the cross-project learning the diagnosis flagged as dead code
|
|
384
|
+
# (zero call sites). Best-effort: any failure is swallowed and the
|
|
385
|
+
# local results stand alone. Opt out with LOKI_SKIP_CROSS_PROJECT=true.
|
|
386
|
+
output['cross_project'] = []
|
|
387
|
+
if os.environ.get('LOKI_SKIP_CROSS_PROJECT', '').lower() not in ('true', '1', 'yes'):
|
|
388
|
+
try:
|
|
389
|
+
from memory.knowledge_graph import OrganizationKnowledgeGraph
|
|
390
|
+
kg = OrganizationKnowledgeGraph()
|
|
391
|
+
query_text = context.get('goal', '') or 'general'
|
|
392
|
+
patterns = kg.query_patterns(query_text, max_results=3)
|
|
393
|
+
for p in patterns:
|
|
394
|
+
output['cross_project'].append({
|
|
395
|
+
'source': 'knowledge-graph',
|
|
396
|
+
'pattern': str(p.get('description', p.get('pattern', p)))[:200]
|
|
397
|
+
if isinstance(p, dict) else str(p)[:200],
|
|
398
|
+
'score': round(float(p.get('score', 0.0)), 3) if isinstance(p, dict) else 0.0,
|
|
399
|
+
})
|
|
400
|
+
except Exception:
|
|
401
|
+
pass # cross-project augmentation is best-effort
|
|
402
|
+
output['cross_project_count'] = len(output['cross_project'])
|
|
403
|
+
|
|
380
404
|
print(json.dumps(output, indent=2))
|
|
381
405
|
except ImportError:
|
|
382
406
|
print('{"error": "Memory module not available", "memory_count": 0, "memories": []}')
|
|
@@ -15028,6 +15052,14 @@ except Exception as e:
|
|
|
15028
15052
|
echo " loki memory vectors"
|
|
15029
15053
|
echo " loki memory vectors rebuild"
|
|
15030
15054
|
echo ""
|
|
15055
|
+
echo " loki memory ingest --from-claude-transcript <path> # v7.7.18"
|
|
15056
|
+
echo " loki memory ingest --from-stdin # v7.7.18"
|
|
15057
|
+
echo " loki memory crossproject --for 'build api' # v7.7.20"
|
|
15058
|
+
echo " loki memory graph --export graph.json # v7.7.20"
|
|
15059
|
+
echo " loki memory graph rebuild # v7.7.20"
|
|
15060
|
+
echo " loki memory enable-hook # v7.7.20"
|
|
15061
|
+
echo " loki memory disable-hook # v7.7.20"
|
|
15062
|
+
echo ""
|
|
15031
15063
|
echo " loki memory namespace detect"
|
|
15032
15064
|
echo " loki memory namespace list"
|
|
15033
15065
|
echo " loki memory namespace create my-project"
|
|
@@ -15106,15 +15138,210 @@ print(json.dumps({'episode_path': path}))
|
|
|
15106
15138
|
fi
|
|
15107
15139
|
;;
|
|
15108
15140
|
|
|
15109
|
-
|
|
15110
|
-
|
|
15111
|
-
|
|
15112
|
-
|
|
15113
|
-
|
|
15114
|
-
|
|
15115
|
-
|
|
15116
|
-
|
|
15117
|
-
|
|
15141
|
+
crossproject)
|
|
15142
|
+
# v7.7.20: surface cross-project knowledge-graph patterns
|
|
15143
|
+
# (wakes the previously-dead memory/knowledge_graph.py).
|
|
15144
|
+
# Usage: loki memory crossproject [--for <goal text>]
|
|
15145
|
+
shift # drop "crossproject"
|
|
15146
|
+
local _cp_query="general"
|
|
15147
|
+
while [ $# -gt 0 ]; do
|
|
15148
|
+
case "$1" in
|
|
15149
|
+
--for) _cp_query="${2:-general}"; if [ $# -ge 2 ]; then shift 2; else shift; fi ;;
|
|
15150
|
+
-h|--help) echo "Usage: loki memory crossproject [--for <goal>]"; return 0 ;;
|
|
15151
|
+
*) shift ;;
|
|
15152
|
+
esac
|
|
15153
|
+
done
|
|
15154
|
+
PYTHONPATH="${SKILL_DIR:-$(pwd)}" python3 -c "
|
|
15155
|
+
import sys, json
|
|
15156
|
+
try:
|
|
15157
|
+
from memory.knowledge_graph import OrganizationKnowledgeGraph
|
|
15158
|
+
kg = OrganizationKnowledgeGraph()
|
|
15159
|
+
patterns = kg.query_patterns(sys.argv[1], max_results=10)
|
|
15160
|
+
if not patterns:
|
|
15161
|
+
print('No cross-project patterns found. Run sessions in multiple projects to build the graph.')
|
|
15162
|
+
else:
|
|
15163
|
+
for i, p in enumerate(patterns, 1):
|
|
15164
|
+
desc = p.get('description', p.get('pattern', p)) if isinstance(p, dict) else p
|
|
15165
|
+
print(f'{i}. {str(desc)[:160]}')
|
|
15166
|
+
except ImportError:
|
|
15167
|
+
print('knowledge_graph module not available')
|
|
15168
|
+
except Exception as e:
|
|
15169
|
+
print(f'Error: {e}')
|
|
15170
|
+
" "$_cp_query"
|
|
15171
|
+
;;
|
|
15172
|
+
|
|
15173
|
+
graph)
|
|
15174
|
+
# v7.7.20: export OR rebuild the cross-project knowledge graph.
|
|
15175
|
+
# Usage: loki memory graph [--export <path>]
|
|
15176
|
+
# loki memory graph rebuild # populate from semantic patterns
|
|
15177
|
+
shift # drop "graph"
|
|
15178
|
+
# v7.7.20 council fix (Opus 1): the read side (query_patterns)
|
|
15179
|
+
# was woken but the WRITE side had no caller, so the graph
|
|
15180
|
+
# stayed empty forever. `graph rebuild` mines .loki/memory/
|
|
15181
|
+
# semantic/*.json across discovered projects and persists them
|
|
15182
|
+
# to the org knowledge graph -- the population path that makes
|
|
15183
|
+
# crossproject + load_memory_context augmentation non-inert.
|
|
15184
|
+
if [ "${1:-}" = "rebuild" ]; then
|
|
15185
|
+
shift
|
|
15186
|
+
PYTHONPATH="${SKILL_DIR:-$(pwd)}" python3 -c "
|
|
15187
|
+
import sys, os
|
|
15188
|
+
try:
|
|
15189
|
+
from memory.knowledge_graph import OrganizationKnowledgeGraph
|
|
15190
|
+
from memory.cross_project import CrossProjectIndex
|
|
15191
|
+
cpi = CrossProjectIndex()
|
|
15192
|
+
cpi.discover_projects()
|
|
15193
|
+
project_dirs = cpi.get_project_dirs()
|
|
15194
|
+
kg = OrganizationKnowledgeGraph()
|
|
15195
|
+
# v7.7.20 council fix (Opus 1): save_patterns appends, so a naive
|
|
15196
|
+
# rebuild would accumulate duplicates across runs. Dedup the UNION of
|
|
15197
|
+
# existing + freshly-extracted, then truncate-rewrite so rebuild is
|
|
15198
|
+
# idempotent.
|
|
15199
|
+
existing = kg.load_patterns(limit=100000)
|
|
15200
|
+
fresh = kg.extract_patterns(project_dirs)
|
|
15201
|
+
merged = kg.deduplicate_patterns(list(existing) + list(fresh))
|
|
15202
|
+
# Truncate then write the deduped union (save_patterns appends).
|
|
15203
|
+
try:
|
|
15204
|
+
if os.path.exists(kg.patterns_file):
|
|
15205
|
+
open(kg.patterns_file, 'w').close()
|
|
15206
|
+
except OSError:
|
|
15207
|
+
pass
|
|
15208
|
+
if merged:
|
|
15209
|
+
kg.save_patterns(merged)
|
|
15210
|
+
print(f'Rebuilt knowledge graph: {len(merged)} unique patterns from {len(project_dirs)} project(s)')
|
|
15211
|
+
else:
|
|
15212
|
+
print(f'No semantic patterns found across {len(project_dirs)} project(s). Run sessions + consolidation first.')
|
|
15213
|
+
except ImportError as e:
|
|
15214
|
+
print(f'knowledge_graph/cross_project module not available: {e}')
|
|
15215
|
+
except Exception as e:
|
|
15216
|
+
print(f'Error: {e}')
|
|
15217
|
+
"
|
|
15218
|
+
return 0
|
|
15219
|
+
fi
|
|
15220
|
+
local _graph_export=""
|
|
15221
|
+
while [ $# -gt 0 ]; do
|
|
15222
|
+
case "$1" in
|
|
15223
|
+
--export) _graph_export="${2:-}"; if [ $# -ge 2 ]; then shift 2; else shift; fi ;;
|
|
15224
|
+
-h|--help) echo "Usage: loki memory graph [--export <path>] | loki memory graph rebuild"; return 0 ;;
|
|
15225
|
+
*) shift ;;
|
|
15226
|
+
esac
|
|
15227
|
+
done
|
|
15228
|
+
PYTHONPATH="${SKILL_DIR:-$(pwd)}" _LOKI_GRAPH_EXPORT="$_graph_export" python3 -c "
|
|
15229
|
+
import sys, os, json
|
|
15230
|
+
export_path = os.environ.get('_LOKI_GRAPH_EXPORT', '')
|
|
15231
|
+
try:
|
|
15232
|
+
from memory.knowledge_graph import OrganizationKnowledgeGraph
|
|
15233
|
+
kg = OrganizationKnowledgeGraph()
|
|
15234
|
+
patterns = kg.load_patterns(limit=100)
|
|
15235
|
+
summary = {'pattern_count': len(patterns), 'patterns': patterns[:100]}
|
|
15236
|
+
if export_path:
|
|
15237
|
+
with open(export_path, 'w') as f:
|
|
15238
|
+
json.dump(summary, f, indent=2, default=str)
|
|
15239
|
+
print(f'Exported {len(patterns)} patterns to {export_path}')
|
|
15240
|
+
else:
|
|
15241
|
+
print(json.dumps(summary, indent=2, default=str)[:4000])
|
|
15242
|
+
except ImportError:
|
|
15243
|
+
print('knowledge_graph module not available')
|
|
15244
|
+
except Exception as e:
|
|
15245
|
+
print(f'Error: {e}')
|
|
15246
|
+
"
|
|
15247
|
+
;;
|
|
15248
|
+
|
|
15249
|
+
enable-hook)
|
|
15250
|
+
# v7.7.20: idempotently install a Claude Code SessionEnd hook
|
|
15251
|
+
# using the VERIFIED schema {matcher, hooks:[{type,command}]}.
|
|
15252
|
+
# Per WebSearch (v7.7.18 council): SessionEnd fires on /clear,
|
|
15253
|
+
# payload is JSON on stdin with transcript_path. The shipped
|
|
15254
|
+
# script claude/hooks/loki-session-end.sh handles both stdin
|
|
15255
|
+
# JSON and env-var fallback, so we point the hook at it.
|
|
15256
|
+
# No-op under LOKI_MEMORY_HOOK_DISABLED=true.
|
|
15257
|
+
shift
|
|
15258
|
+
if [ "${LOKI_MEMORY_HOOK_DISABLED:-}" = "true" ]; then
|
|
15259
|
+
echo -e "${YELLOW}Hook install skipped (LOKI_MEMORY_HOOK_DISABLED=true)${NC}"
|
|
15260
|
+
return 0
|
|
15261
|
+
fi
|
|
15262
|
+
local _settings="$HOME/.claude/settings.json"
|
|
15263
|
+
local _hook_script="${SKILL_DIR:-$(pwd)}/claude/hooks/loki-session-end.sh"
|
|
15264
|
+
mkdir -p "$(dirname "$_settings")" 2>/dev/null || true
|
|
15265
|
+
[ ! -f "$_settings" ] && echo '{}' > "$_settings"
|
|
15266
|
+
_LOKI_HOOK_SCRIPT="$_hook_script" _LOKI_SETTINGS="$_settings" python3 -c "
|
|
15267
|
+
import json, os, sys, tempfile
|
|
15268
|
+
settings_path = os.environ['_LOKI_SETTINGS']
|
|
15269
|
+
hook_script = os.environ['_LOKI_HOOK_SCRIPT']
|
|
15270
|
+
hook_cmd = f'bash {hook_script}'
|
|
15271
|
+
try:
|
|
15272
|
+
with open(settings_path) as f:
|
|
15273
|
+
data = json.load(f)
|
|
15274
|
+
except Exception:
|
|
15275
|
+
data = {}
|
|
15276
|
+
hooks = data.setdefault('hooks', {})
|
|
15277
|
+
session_end = hooks.setdefault('SessionEnd', [])
|
|
15278
|
+
# Verified Claude Code schema: list of {matcher, hooks:[{type,command}]}.
|
|
15279
|
+
# Idempotency: detect an existing entry whose nested command references
|
|
15280
|
+
# our hook script.
|
|
15281
|
+
for entry in session_end:
|
|
15282
|
+
if isinstance(entry, dict):
|
|
15283
|
+
for h in entry.get('hooks', []):
|
|
15284
|
+
if isinstance(h, dict) and 'loki-session-end.sh' in str(h.get('command', '')):
|
|
15285
|
+
print('already-installed')
|
|
15286
|
+
sys.exit(0)
|
|
15287
|
+
session_end.append({
|
|
15288
|
+
'matcher': 'clear',
|
|
15289
|
+
'hooks': [{'type': 'command', 'command': hook_cmd}],
|
|
15290
|
+
})
|
|
15291
|
+
tmp_fd, tmp_path = tempfile.mkstemp(dir=os.path.dirname(settings_path), prefix='.loki-settings-')
|
|
15292
|
+
with os.fdopen(tmp_fd, 'w') as f:
|
|
15293
|
+
json.dump(data, f, indent=2)
|
|
15294
|
+
os.replace(tmp_path, settings_path)
|
|
15295
|
+
print('installed')
|
|
15296
|
+
"
|
|
15297
|
+
local _r=$?
|
|
15298
|
+
if [ $_r -eq 0 ]; then
|
|
15299
|
+
echo -e "${GREEN}SessionEnd hook ready.${NC} Fires on /clear; pipes transcript to loki memory ingest."
|
|
15300
|
+
echo -e "${CYAN}Note: SessionEnd only fires on /clear, not normal exits (Claude Code limitation).${NC}"
|
|
15301
|
+
echo -e "${CYAN}Remove with: loki memory disable-hook${NC}"
|
|
15302
|
+
fi
|
|
15303
|
+
;;
|
|
15304
|
+
|
|
15305
|
+
disable-hook)
|
|
15306
|
+
# v7.7.20: reverse counterpart. Removes any SessionEnd entry
|
|
15307
|
+
# whose nested command references loki-session-end.sh.
|
|
15308
|
+
shift
|
|
15309
|
+
local _settings="$HOME/.claude/settings.json"
|
|
15310
|
+
if [ ! -f "$_settings" ]; then
|
|
15311
|
+
echo -e "${YELLOW}No ~/.claude/settings.json to clean${NC}"
|
|
15312
|
+
return 0
|
|
15313
|
+
fi
|
|
15314
|
+
_LOKI_SETTINGS="$_settings" python3 -c "
|
|
15315
|
+
import json, os, sys, tempfile
|
|
15316
|
+
settings_path = os.environ['_LOKI_SETTINGS']
|
|
15317
|
+
try:
|
|
15318
|
+
with open(settings_path) as f:
|
|
15319
|
+
data = json.load(f)
|
|
15320
|
+
except Exception:
|
|
15321
|
+
print('settings-not-readable')
|
|
15322
|
+
sys.exit(0)
|
|
15323
|
+
hooks = data.get('hooks', {})
|
|
15324
|
+
session_end = hooks.get('SessionEnd', [])
|
|
15325
|
+
def refs_loki(entry):
|
|
15326
|
+
if not isinstance(entry, dict):
|
|
15327
|
+
return False
|
|
15328
|
+
for h in entry.get('hooks', []):
|
|
15329
|
+
if isinstance(h, dict) and 'loki-session-end.sh' in str(h.get('command', '')):
|
|
15330
|
+
return True
|
|
15331
|
+
return False
|
|
15332
|
+
filtered = [e for e in session_end if not refs_loki(e)]
|
|
15333
|
+
if len(filtered) == len(session_end):
|
|
15334
|
+
print('not-installed')
|
|
15335
|
+
sys.exit(0)
|
|
15336
|
+
hooks['SessionEnd'] = filtered
|
|
15337
|
+
data['hooks'] = hooks
|
|
15338
|
+
tmp_fd, tmp_path = tempfile.mkstemp(dir=os.path.dirname(settings_path), prefix='.loki-settings-')
|
|
15339
|
+
with os.fdopen(tmp_fd, 'w') as f:
|
|
15340
|
+
json.dump(data, f, indent=2)
|
|
15341
|
+
os.replace(tmp_path, settings_path)
|
|
15342
|
+
print('removed')
|
|
15343
|
+
"
|
|
15344
|
+
;;
|
|
15118
15345
|
|
|
15119
15346
|
*)
|
|
15120
15347
|
echo -e "${RED}Unknown memory command: $subcommand${NC}"
|
package/dashboard/__init__.py
CHANGED
package/dashboard/server.py
CHANGED
|
@@ -2638,16 +2638,115 @@ async def get_skill(skill_id: str):
|
|
|
2638
2638
|
raise HTTPException(status_code=404, detail="Skill not found")
|
|
2639
2639
|
|
|
2640
2640
|
|
|
2641
|
-
@app.get("/api/memory/economics")
|
|
2641
|
+
@app.get("/api/memory/economics", dependencies=[Depends(auth.require_scope("read"))])
|
|
2642
2642
|
async def get_token_economics():
|
|
2643
|
-
"""Get token usage economics.
|
|
2644
|
-
|
|
2643
|
+
"""Get token usage economics (v7.7.21: normalized + hit_rate + top_patterns).
|
|
2644
|
+
|
|
2645
|
+
Excellence bar 5: per-retrieval cost + hit rate + top patterns visible.
|
|
2646
|
+
Reads token_economics.json (written by memory.token_economics.save())
|
|
2647
|
+
which has shape {session_id, metrics:{discovery_tokens, read_tokens,
|
|
2648
|
+
cache_hits, cache_misses, ...}, ratio, savings_percent}. Computes a
|
|
2649
|
+
cache hit_rate + surfaces the most-accessed episodes/patterns. The
|
|
2650
|
+
pre-v7.7.21 endpoint returned camelCase keys that did not match the
|
|
2651
|
+
snake_case file; the `raw` field preserves the original document for
|
|
2652
|
+
backward compat while the top-level fields are normalized.
|
|
2653
|
+
"""
|
|
2654
|
+
loki_dir = _get_loki_dir()
|
|
2655
|
+
econ_file = loki_dir / "memory" / "token_economics.json"
|
|
2656
|
+
raw = {}
|
|
2645
2657
|
if econ_file.exists():
|
|
2646
2658
|
try:
|
|
2647
|
-
|
|
2659
|
+
raw = json.loads(econ_file.read_text())
|
|
2648
2660
|
except Exception:
|
|
2649
|
-
|
|
2650
|
-
|
|
2661
|
+
raw = {}
|
|
2662
|
+
|
|
2663
|
+
metrics = raw.get("metrics", {}) if isinstance(raw, dict) else {}
|
|
2664
|
+
cache_hits = int(metrics.get("cache_hits", 0) or 0)
|
|
2665
|
+
cache_misses = int(metrics.get("cache_misses", 0) or 0)
|
|
2666
|
+
cache_total = cache_hits + cache_misses
|
|
2667
|
+
hit_rate = round(cache_hits / cache_total, 4) if cache_total > 0 else 0.0
|
|
2668
|
+
discovery_tokens = int(metrics.get("discovery_tokens", 0) or 0)
|
|
2669
|
+
read_tokens = int(metrics.get("read_tokens", 0) or 0)
|
|
2670
|
+
|
|
2671
|
+
# Top-accessed memories: scan episodic + semantic, rank by access_count
|
|
2672
|
+
# then importance.
|
|
2673
|
+
# v7.7.21 council fix (Opus 1 + Opus 2):
|
|
2674
|
+
# - os.walk(followlinks=False) instead of recursive glob: does NOT
|
|
2675
|
+
# descend symlinked dirs (prevents traversal/exfil + DoS-amplify
|
|
2676
|
+
# via a symlink to a huge tree).
|
|
2677
|
+
# - realpath containment: every candidate file must resolve to a
|
|
2678
|
+
# path under mem_root (mirrors the sibling get_skill endpoint).
|
|
2679
|
+
# - hard cap on files SCANNED (not just surfaced): stop after
|
|
2680
|
+
# MAX_SCAN files per subdir so a large store cannot make this
|
|
2681
|
+
# request unboundedly slow even with the 30s auto-refresh.
|
|
2682
|
+
top_patterns = []
|
|
2683
|
+
try:
|
|
2684
|
+
import os as _os
|
|
2685
|
+
mem_root = (loki_dir / "memory").resolve()
|
|
2686
|
+
MAX_SCAN = 300
|
|
2687
|
+
candidates = []
|
|
2688
|
+
for sub in ("episodic", "semantic"):
|
|
2689
|
+
sub_root = mem_root / sub
|
|
2690
|
+
if not sub_root.is_dir():
|
|
2691
|
+
continue
|
|
2692
|
+
scanned = 0
|
|
2693
|
+
stop = False
|
|
2694
|
+
for dirpath, dirnames, filenames in _os.walk(str(sub_root), followlinks=False):
|
|
2695
|
+
if stop:
|
|
2696
|
+
break
|
|
2697
|
+
for fn in filenames:
|
|
2698
|
+
if not fn.endswith(".json"):
|
|
2699
|
+
continue
|
|
2700
|
+
fp = _os.path.join(dirpath, fn)
|
|
2701
|
+
# Containment: resolved path must stay under mem_root.
|
|
2702
|
+
try:
|
|
2703
|
+
rp = _os.path.realpath(fp)
|
|
2704
|
+
if _os.path.commonpath([rp, str(mem_root)]) != str(mem_root):
|
|
2705
|
+
continue
|
|
2706
|
+
except (OSError, ValueError):
|
|
2707
|
+
continue
|
|
2708
|
+
try:
|
|
2709
|
+
with open(fp) as fh:
|
|
2710
|
+
d = json.load(fh)
|
|
2711
|
+
candidates.append({
|
|
2712
|
+
"id": d.get("id", ""),
|
|
2713
|
+
"kind": sub,
|
|
2714
|
+
"access_count": int(d.get("access_count", 0) or 0),
|
|
2715
|
+
"importance": float(d.get("importance", 0.0) or 0.0),
|
|
2716
|
+
"summary": str(
|
|
2717
|
+
d.get("summary")
|
|
2718
|
+
or d.get("pattern")
|
|
2719
|
+
or d.get("context", {}).get("goal", "")
|
|
2720
|
+
)[:160],
|
|
2721
|
+
})
|
|
2722
|
+
except Exception:
|
|
2723
|
+
continue
|
|
2724
|
+
scanned += 1
|
|
2725
|
+
if scanned >= MAX_SCAN:
|
|
2726
|
+
stop = True
|
|
2727
|
+
break
|
|
2728
|
+
candidates.sort(key=lambda c: (c["access_count"], c["importance"]), reverse=True)
|
|
2729
|
+
top_patterns = candidates[:10]
|
|
2730
|
+
except Exception:
|
|
2731
|
+
top_patterns = []
|
|
2732
|
+
|
|
2733
|
+
return {
|
|
2734
|
+
"session_id": raw.get("session_id"),
|
|
2735
|
+
"discovery_tokens": discovery_tokens,
|
|
2736
|
+
"read_tokens": read_tokens,
|
|
2737
|
+
"total_tokens": discovery_tokens + read_tokens,
|
|
2738
|
+
"cache_hits": cache_hits,
|
|
2739
|
+
"cache_misses": cache_misses,
|
|
2740
|
+
"hit_rate": hit_rate,
|
|
2741
|
+
"ratio": raw.get("ratio", 0.0),
|
|
2742
|
+
"savings_percent": raw.get("savings_percent", 0.0),
|
|
2743
|
+
"top_patterns": top_patterns,
|
|
2744
|
+
# Backward-compat aliases (pre-v7.7.21 camelCase consumers)
|
|
2745
|
+
"discoveryTokens": discovery_tokens,
|
|
2746
|
+
"readTokens": read_tokens,
|
|
2747
|
+
"savingsPercent": raw.get("savings_percent", 0.0),
|
|
2748
|
+
"raw": raw,
|
|
2749
|
+
}
|
|
2651
2750
|
|
|
2652
2751
|
|
|
2653
2752
|
@app.post("/api/memory/consolidate", dependencies=[Depends(auth.require_scope("control"))])
|
|
@@ -632,6 +632,55 @@
|
|
|
632
632
|
<div>
|
|
633
633
|
<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>
|
|
634
634
|
<loki-memory-browser id="memory-browser" tab="summary"></loki-memory-browser>
|
|
635
|
+
<!-- v7.7.21 token economics tile: hit rate + tokens + top patterns -->
|
|
636
|
+
<div id="memory-economics-tile" style="margin-top: 12px; background: var(--loki-bg-card, #1a1a1a); border: 1px solid var(--loki-border, #333); border-radius: 5px; padding: 12px;">
|
|
637
|
+
<div style="font-size: 11px; color: var(--loki-text-muted, #888); margin-bottom: 8px;">Token Economics</div>
|
|
638
|
+
<div id="memory-economics-metrics" style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; font-size: 13px;">
|
|
639
|
+
<div><span style="color: var(--loki-text-muted, #888);">Hit rate</span><br><strong id="econ-hit-rate">--</strong></div>
|
|
640
|
+
<div><span style="color: var(--loki-text-muted, #888);">Total tokens</span><br><strong id="econ-total-tokens">--</strong></div>
|
|
641
|
+
<div><span style="color: var(--loki-text-muted, #888);">Savings</span><br><strong id="econ-savings">--</strong></div>
|
|
642
|
+
</div>
|
|
643
|
+
<div id="memory-economics-top" style="margin-top: 10px; font-size: 12px; color: var(--loki-text-muted, #888);"></div>
|
|
644
|
+
</div>
|
|
645
|
+
<script>
|
|
646
|
+
(function(){
|
|
647
|
+
function loadEconomics(){
|
|
648
|
+
fetch('/api/memory/economics').then(function(r){ return r.json(); }).then(function(j){
|
|
649
|
+
var hr = document.getElementById('econ-hit-rate');
|
|
650
|
+
var tt = document.getElementById('econ-total-tokens');
|
|
651
|
+
var sv = document.getElementById('econ-savings');
|
|
652
|
+
var top = document.getElementById('memory-economics-top');
|
|
653
|
+
if (hr) hr.textContent = ((j.hit_rate || 0) * 100).toFixed(1) + '%';
|
|
654
|
+
if (tt) tt.textContent = (j.total_tokens || 0).toLocaleString();
|
|
655
|
+
if (sv) sv.textContent = (j.savings_percent || 0).toFixed(1) + '%';
|
|
656
|
+
if (top) {
|
|
657
|
+
var patterns = j.top_patterns || [];
|
|
658
|
+
// v7.7.21 council fix (Opus 1): build DOM with
|
|
659
|
+
// textContent (NOT innerHTML single-char escape) so
|
|
660
|
+
// agent/PRD-derived summaries cannot inject markup.
|
|
661
|
+
while (top.firstChild) top.removeChild(top.firstChild);
|
|
662
|
+
if (patterns.length === 0) {
|
|
663
|
+
top.textContent = 'No retrieval patterns yet. Run sessions to accumulate.';
|
|
664
|
+
} else {
|
|
665
|
+
var header = document.createElement('div');
|
|
666
|
+
header.style.marginBottom = '4px';
|
|
667
|
+
header.textContent = 'Top retrieved:';
|
|
668
|
+
top.appendChild(header);
|
|
669
|
+
patterns.slice(0, 5).forEach(function(p){
|
|
670
|
+
var row = document.createElement('div');
|
|
671
|
+
// textContent escapes everything; no markup injection.
|
|
672
|
+
row.textContent = (p.access_count || 0) + 'x · ' +
|
|
673
|
+
(p.summary || p.id || '');
|
|
674
|
+
top.appendChild(row);
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
}).catch(function(){ /* endpoint not available; tile stays at -- */ });
|
|
679
|
+
}
|
|
680
|
+
loadEconomics();
|
|
681
|
+
setInterval(loadEconomics, 30000);
|
|
682
|
+
})();
|
|
683
|
+
</script>
|
|
635
684
|
</div>
|
|
636
685
|
<div>
|
|
637
686
|
<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>
|
package/docs/INSTALLATION.md
CHANGED
package/loki-ts/dist/loki.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
var _7=Object.defineProperty;var I7=(K)=>K;function P7(K,$){this[K]=I7.bind(null,$)}var v=(K,$)=>{for(var Q in $)_7(K,Q,{get:$[Q],enumerable:!0,configurable:!0,set:P7.bind($,Q)})};var R=(K,$)=>()=>(K&&($=K(K=0)),$);var t=import.meta.require;var e1={};v(e1,{lokiDir:()=>P,homeLokiDir:()=>k1,findRepoRootForVersion:()=>N1,REPO_ROOT:()=>p});import{resolve as u,dirname as S1}from"path";import{fileURLToPath as L7}from"url";import{existsSync as J1}from"fs";import{homedir as R7}from"os";function E7(){let K=i1;for(let $=0;$<6;$++){if(J1(u(K,"VERSION"))&&J1(u(K,"autonomy/run.sh")))return K;let Q=S1(K);if(Q===K)break;K=Q}return u(i1,"..","..","..")}function N1(K){let $=K;for(let Q=0;Q<6;Q++){if(J1(u($,"VERSION"))&&J1(u($,"autonomy/run.sh")))return $;let X=S1($);if(X===$)break;$=X}return u(K,"..","..","..")}function P(){return process.env.LOKI_DIR??u(process.cwd(),".loki")}function k1(){return u(R7(),".loki")}var i1,p;var g=R(()=>{i1=S1(L7(import.meta.url));p=E7()});import{readFileSync as F7}from"fs";import{resolve as w7,dirname as x7}from"path";import{fileURLToPath as S7}from"url";function G1(){if(o!==null)return o;let K="7.7.
|
|
2
|
+
var _7=Object.defineProperty;var I7=(K)=>K;function P7(K,$){this[K]=I7.bind(null,$)}var v=(K,$)=>{for(var Q in $)_7(K,Q,{get:$[Q],enumerable:!0,configurable:!0,set:P7.bind($,Q)})};var R=(K,$)=>()=>(K&&($=K(K=0)),$);var t=import.meta.require;var e1={};v(e1,{lokiDir:()=>P,homeLokiDir:()=>k1,findRepoRootForVersion:()=>N1,REPO_ROOT:()=>p});import{resolve as u,dirname as S1}from"path";import{fileURLToPath as L7}from"url";import{existsSync as J1}from"fs";import{homedir as R7}from"os";function E7(){let K=i1;for(let $=0;$<6;$++){if(J1(u(K,"VERSION"))&&J1(u(K,"autonomy/run.sh")))return K;let Q=S1(K);if(Q===K)break;K=Q}return u(i1,"..","..","..")}function N1(K){let $=K;for(let Q=0;Q<6;Q++){if(J1(u($,"VERSION"))&&J1(u($,"autonomy/run.sh")))return $;let X=S1($);if(X===$)break;$=X}return u(K,"..","..","..")}function P(){return process.env.LOKI_DIR??u(process.cwd(),".loki")}function k1(){return u(R7(),".loki")}var i1,p;var g=R(()=>{i1=S1(L7(import.meta.url));p=E7()});import{readFileSync as F7}from"fs";import{resolve as w7,dirname as x7}from"path";import{fileURLToPath as S7}from"url";function G1(){if(o!==null)return o;let K="7.7.21";if(typeof K==="string"&&K.length>0)return o=K,o;try{let $=x7(S7(import.meta.url)),Q=N1($);o=F7(w7(Q,"VERSION"),"utf-8").trim()}catch{o="unknown"}return o}var o=null;var D1=R(()=>{g()});var $0={};v($0,{runOrThrow:()=>N7,run:()=>k,commandVersion:()=>D7,commandExists:()=>h,ShellError:()=>C1});async function k(K,$={}){let Q=Bun.spawn({cmd:[...K],stdout:"pipe",stderr:"pipe",env:$.env?{...process.env,...$.env}:process.env,cwd:$.cwd}),X,Z;if($.timeoutMs&&$.timeoutMs>0)X=setTimeout(()=>{try{Q.kill("SIGTERM")}catch{}Z=setTimeout(()=>{try{Q.kill("SIGKILL")}catch{}},2000)},$.timeoutMs);try{let[W,z,q]=await Promise.all([new Response(Q.stdout).text(),new Response(Q.stderr).text(),Q.exited]);return{stdout:W,stderr:z,exitCode:q}}finally{if(X)clearTimeout(X);if(Z)clearTimeout(Z)}}async function N7(K,$={}){let Q=await k(K,$);if(Q.exitCode!==0)throw new C1(`command failed (${Q.exitCode}): ${K.join(" ")}`,Q.exitCode,Q.stdout,Q.stderr);return Q}async function h(K){let $=k7(K),Q=await k(["sh","-c",`command -v ${$}`],{timeoutMs:5000});if(Q.exitCode===0)return Q.stdout.trim()||null;return null}function k7(K){if(!/^[A-Za-z0-9._/-]+$/.test(K))throw Error(`refused to shell-escape suspect token: ${K}`);return K}async function D7(K,$="--version"){if(!await h(K))return null;let X=await k([K,$],{timeoutMs:5000});if(X.exitCode!==0)return null;return((X.stdout||X.stderr).split(/\r?\n/)[0]?.trim()??"")||null}var C1;var n=R(()=>{C1=class C1 extends Error{message;exitCode;stdout;stderr;constructor(K,$,Q,X){super(K);this.message=K;this.exitCode=$;this.stdout=Q;this.stderr=X;this.name="ShellError"}}});function c(K){return C7?"":K}var C7,E,b,F,T6,O,D,w,H;var a=R(()=>{C7=(process.env.NO_COLOR??"").length>0;E=c("\x1B[0;31m"),b=c("\x1B[0;32m"),F=c("\x1B[1;33m"),T6=c("\x1B[0;34m"),O=c("\x1B[0;36m"),D=c("\x1B[1m"),w=c("\x1B[2m"),H=c("\x1B[0m")});import{existsSync as c7}from"fs";async function i(){if(X1!==void 0)return X1;let K="/opt/homebrew/bin/python3.12";if(c7(K))return X1=K,K;let $=await h("python3.12");if($)return X1=$,$;let Q=await h("python3");return X1=Q,Q}async function s(K,$={}){let Q=await i();if(!Q)return{stdout:"",stderr:"python3 not found",exitCode:127};return k([Q,"-c",K],$)}var X1;var Z1=R(()=>{n()});var G0={};v(G0,{runStatus:()=>Q5});import{existsSync as N,readFileSync as W1,readdirSync as W0,statSync as H0}from"fs";import{resolve as x,basename as a7}from"path";async function r7(){if(await h("jq"))return!0;return process.stdout.write(`${E}Error: jq is required but not installed.${H}
|
|
3
3
|
`),process.stdout.write(`Install with:
|
|
4
4
|
`),process.stdout.write(` brew install jq (macOS)
|
|
5
5
|
`),process.stdout.write(` apt install jq (Debian/Ubuntu)
|
|
@@ -585,4 +585,4 @@ Set LOKI_LEGACY_BASH=1 to force the bash CLI for every command.
|
|
|
585
585
|
`),2}default:return process.stderr.write(`Unknown command: ${$}
|
|
586
586
|
`),process.stderr.write(j7),2}}process.on("SIGINT",()=>process.exit(130));process.on("SIGTERM",()=>process.exit(143));var X6=await Q6(Bun.argv.slice(2));process.exit(X6);
|
|
587
587
|
|
|
588
|
-
//# debugId=
|
|
588
|
+
//# debugId=0F0FAE2834FF534B64756E2164756E21
|
package/mcp/__init__.py
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "loki-mode",
|
|
3
|
-
"version": "7.7.
|
|
3
|
+
"version": "7.7.21",
|
|
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",
|