leopold-driver 0.1.0 → 0.1.2
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 +19 -5
- package/assets/VERSION +1 -0
- package/assets/extensions/README.md +52 -0
- package/assets/extensions/gstack/extension.json +8 -0
- package/assets/extensions/gstack/manage.sh +68 -0
- package/assets/extensions/leopold/extension.json +8 -0
- package/assets/extensions/leopold/manage.sh +59 -0
- package/assets/extensions/ovmem/README.md +101 -0
- package/assets/extensions/ovmem/extension.json +8 -0
- package/assets/extensions/ovmem/install.sh +330 -0
- package/assets/extensions/ovmem/manage.sh +87 -0
- package/assets/extensions/ovmem/models.json +24 -0
- package/assets/extensions/ovmem/payload/RUNTIME.md +121 -0
- package/assets/extensions/ovmem/payload/ovmem-cleanup.py +148 -0
- package/assets/extensions/ovmem/payload/ovmem.py +421 -0
- package/assets/extensions/serena/README.md +50 -0
- package/assets/extensions/serena/extension.json +8 -0
- package/assets/extensions/serena/manage.sh +119 -0
- package/assets/hooks/guard-irreversible.sh +185 -0
- package/assets/hooks/hooks.json +20 -0
- package/assets/hooks/stop-continuity.sh +132 -0
- package/assets/install.sh +150 -0
- package/assets/scripts/__pycache__/leopold-watch.cpython-312.pyc +0 -0
- package/assets/scripts/leopold-doctor.sh +53 -0
- package/assets/scripts/leopold-menu.sh +132 -0
- package/assets/scripts/leopold-update-check.sh +23 -0
- package/assets/scripts/leopold-update.sh +13 -0
- package/assets/scripts/leopold-watch.py +585 -0
- package/assets/scripts/record-demo.sh +61 -0
- package/assets/scripts/test-guard.sh +76 -0
- package/assets/scripts/test-hooks.sh +121 -0
- package/assets/settings.template.json +23 -0
- package/assets/skills/leopold-brief/SKILL.md +121 -0
- package/assets/skills/leopold-doctor/SKILL.md +23 -0
- package/assets/skills/leopold-run/SKILL.md +171 -0
- package/assets/skills/leopold-status/SKILL.md +34 -0
- package/assets/skills/leopold-stop/SKILL.md +36 -0
- package/assets/skills/leopold-update/SKILL.md +27 -0
- package/assets/skills/leopold-watch/SKILL.md +48 -0
- package/assets/templates/CHARTER.md +32 -0
- package/assets/templates/DECISIONS.md +15 -0
- package/assets/templates/GUARDRAILS.md +38 -0
- package/assets/templates/MISSION.md +22 -0
- package/assets/templates/PLAN.md +9 -0
- package/dist/guard.js +82 -23
- package/dist/harness.js +71 -0
- package/dist/index.js +53 -23
- package/package.json +18 -6
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ovmem installer / reconfigure — pick a provider + models, set everything up, and
|
|
3
|
+
# switch safely between providers (incl. re-embedding the memory store when the
|
|
4
|
+
# embedding model changes).
|
|
5
|
+
#
|
|
6
|
+
# Providers:
|
|
7
|
+
# openai - one API key (needs embedding + model.request scopes).
|
|
8
|
+
# bedrock - AWS Bedrock via LiteLLM; auth = a Bedrock API key (bearer) + region.
|
|
9
|
+
#
|
|
10
|
+
# Re-running detects the current install: it offers to reuse the existing credential,
|
|
11
|
+
# defaults each prompt to the current choice, and — if you change the embedding model —
|
|
12
|
+
# rebuilds the vector index for the new dimension (your memory CONTENT is preserved;
|
|
13
|
+
# only the index is re-embedded). The old index is backed up and restored if the rebuild
|
|
14
|
+
# fails. Server restarts are lock-aware (one OpenViking process per data dir).
|
|
15
|
+
#
|
|
16
|
+
# Interactive prompts read /dev/tty (works under `curl … | bash`). Headless: set
|
|
17
|
+
# OVMEM_PROVIDER, OVMEM_CHAT_MODEL, OVMEM_EMBED_MODEL + OPENAI_API_KEY (openai) or
|
|
18
|
+
# AWS_BEARER_TOKEN_BEDROCK + AWS_REGION (bedrock).
|
|
19
|
+
set -euo pipefail
|
|
20
|
+
|
|
21
|
+
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
22
|
+
PAYLOAD="$HERE/payload"; MODELS="$HERE/models.json"
|
|
23
|
+
CLAUDE="${CLAUDE_HOME:-$HOME/.claude}"; OVMEM_DIR="$CLAUDE/ovmem"; SETTINGS="$CLAUDE/settings.json"
|
|
24
|
+
OV_DIR="$HOME/.openviking"; OV_CONF="$OV_DIR/ov.conf"; WORKSPACE="$OV_DIR/data"; VDB="$WORKSPACE/vectordb"
|
|
25
|
+
BIN="$HOME/.local/bin"; HEALTH="http://127.0.0.1:1933/health"; API="http://127.0.0.1:1933/api/v1"
|
|
26
|
+
OPENVIKING_PIN="openviking==0.3.21"
|
|
27
|
+
|
|
28
|
+
say() { printf "\033[36m->\033[0m %s\n" "$*"; }
|
|
29
|
+
ok() { printf " \033[32mok\033[0m %s\n" "$*"; }
|
|
30
|
+
warn() { printf " \033[33mwarn\033[0m %s\n" "$*"; }
|
|
31
|
+
die() { printf "\033[31mx\033[0m %s\n" "$*" >&2; exit 1; }
|
|
32
|
+
|
|
33
|
+
TTY=""; if exec 3<>/dev/tty 2>/dev/null; then TTY=3; fi
|
|
34
|
+
ask() { local p="$1" d="${2:-}" a=""
|
|
35
|
+
if [ -n "$TTY" ]; then
|
|
36
|
+
if [ -n "$d" ]; then printf "%s [%s]: " "$p" "$d" >&"$TTY"; else printf "%s: " "$p" >&"$TTY"; fi
|
|
37
|
+
IFS= read -r a <&"$TTY" || a=""; [ -z "$a" ] && a="$d"
|
|
38
|
+
else a="$d"; fi
|
|
39
|
+
printf "%s" "$a"; }
|
|
40
|
+
ask_secret() { local p="$1" a=""
|
|
41
|
+
if [ -n "$TTY" ]; then printf "%s: " "$p" >&"$TTY"; IFS= read -rs a <&"$TTY" || a=""; printf "\n" >&"$TTY"; fi
|
|
42
|
+
printf "%s" "$a"; }
|
|
43
|
+
confirm() { case "$(ask "$1 [Y/n]" "Y")" in [nN]*) return 1;; *) return 0;; esac; }
|
|
44
|
+
j() { jq -r "$1" "$MODELS"; }
|
|
45
|
+
|
|
46
|
+
# Run a command with a live "<label>… Ns" spinner so a long, silent step never looks
|
|
47
|
+
# frozen — even when it's really a background download/reindex. stdout+stderr are captured
|
|
48
|
+
# to SPIN_OUT so callers can use the output (or print it on failure). Headless: one line.
|
|
49
|
+
SPIN_OUT=""
|
|
50
|
+
run_spin() {
|
|
51
|
+
local label="$1"; shift
|
|
52
|
+
local tmp; tmp="$(mktemp 2>/dev/null || echo "/tmp/ovm_spin.$$")"
|
|
53
|
+
( "$@" >"$tmp" 2>&1 ) & local pid=$!
|
|
54
|
+
if [ -n "$TTY" ]; then
|
|
55
|
+
local i=0 t0=$SECONDS f='|/-\'
|
|
56
|
+
while kill -0 "$pid" 2>/dev/null; do
|
|
57
|
+
printf "\r \033[2m%s %s… %ds\033[0m" "${f:i%4:1}" "$label" "$((SECONDS-t0))" >&"$TTY"
|
|
58
|
+
i=$((i+1)); sleep 0.2
|
|
59
|
+
done
|
|
60
|
+
printf "\r\033[K" >&"$TTY"
|
|
61
|
+
else
|
|
62
|
+
printf " ... %s (running)\n" "$label"
|
|
63
|
+
fi
|
|
64
|
+
local rc=0; wait "$pid" || rc=$?
|
|
65
|
+
SPIN_OUT="$(cat "$tmp" 2>/dev/null || true)"; rm -f "$tmp"
|
|
66
|
+
return "$rc"
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
# ---- platform + server lifecycle (lock-aware) ------------------------------
|
|
70
|
+
case "$(uname -s 2>/dev/null || echo unknown)" in
|
|
71
|
+
Linux|Darwin) : ;;
|
|
72
|
+
MINGW*|MSYS*|CYGWIN*|Windows*) die "Native Windows is not supported. Run inside WSL." ;;
|
|
73
|
+
*) warn "unrecognized OS '$(uname -s 2>/dev/null)'; only Linux/macOS are tested." ;;
|
|
74
|
+
esac
|
|
75
|
+
port_pid() { if command -v lsof >/dev/null 2>&1; then lsof -ti tcp:1933 2>/dev/null | head -1
|
|
76
|
+
elif command -v ss >/dev/null 2>&1; then ss -tlnp 2>/dev/null | grep ':1933' | sed -n 's/.*pid=\([0-9]*\).*/\1/p' | head -1; fi; }
|
|
77
|
+
server_pids() { pgrep -f "bin/openviking-server" 2>/dev/null || true; }
|
|
78
|
+
# Stop EVERY OpenViking process and wait until the data-dir lock is free. Running two
|
|
79
|
+
# instances on one data dir is forbidden (DataDirectoryLocked); a kill + instant restart
|
|
80
|
+
# races the lock, so we wait for the process to actually die.
|
|
81
|
+
stop_server() {
|
|
82
|
+
local p _; p="$(server_pids)"; [ -n "$p" ] && kill $p 2>/dev/null || true
|
|
83
|
+
for _ in $(seq 1 40); do [ -z "$(server_pids)" ] && [ -z "$(port_pid || true)" ] && break; sleep 0.4; done
|
|
84
|
+
p="$(server_pids)"; [ -n "$p" ] && kill -9 $p 2>/dev/null || true
|
|
85
|
+
for _ in $(seq 1 10); do [ -z "$(server_pids)" ] && break; sleep 0.3; done
|
|
86
|
+
rm -f /tmp/openviking.pid "$WORKSPACE/.openviking.pid" 2>/dev/null || true
|
|
87
|
+
}
|
|
88
|
+
start_server() {
|
|
89
|
+
"$BIN/openviking-start"
|
|
90
|
+
run_spin "waiting for OpenViking to be healthy" \
|
|
91
|
+
curl -s --retry 40 --retry-delay 1 --retry-connrefused -m 80 "$HEALTH" \
|
|
92
|
+
|| die "OpenViking did not become healthy; see /tmp/openviking.log"
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
# ---- detect the current install --------------------------------------------
|
|
96
|
+
CUR_PROVIDER=""; CUR_CHAT_MODEL=""; CUR_EMBED_MODEL=""; CUR_DIM=""; CUR_ROOTKEY=""
|
|
97
|
+
CUR_OPENAI_KEY=""; CUR_AWS_TOKEN=""; CUR_AWS_REGION=""; CUR_CHAT_ID=""; CUR_EMBED_ID=""
|
|
98
|
+
read_current() {
|
|
99
|
+
[ -f "$OV_CONF" ] && command -v jq >/dev/null 2>&1 || return 0
|
|
100
|
+
CUR_CHAT_MODEL="$(jq -r '.vlm.model // empty' "$OV_CONF" 2>/dev/null)"
|
|
101
|
+
CUR_EMBED_MODEL="$(jq -r '.embedding.dense.model // empty' "$OV_CONF" 2>/dev/null)"
|
|
102
|
+
CUR_DIM="$(jq -r '.embedding.dense.dimension // empty' "$OV_CONF" 2>/dev/null)"
|
|
103
|
+
CUR_ROOTKEY="$(jq -r '.server.root_api_key // empty' "$OV_CONF" 2>/dev/null)"
|
|
104
|
+
case "$CUR_CHAT_MODEL" in bedrock/*) CUR_PROVIDER=bedrock ;; ?*) CUR_PROVIDER=openai ;; esac
|
|
105
|
+
[ "$CUR_PROVIDER" = openai ] && CUR_OPENAI_KEY="$(jq -r '.vlm.api_key // empty' "$OV_CONF" 2>/dev/null)"
|
|
106
|
+
CUR_CHAT_ID="$(j ".chat[]|select(.model==\"$CUR_CHAT_MODEL\").id" 2>/dev/null | head -1)"
|
|
107
|
+
CUR_EMBED_ID="$(j ".embed[]|select(.model==\"$CUR_EMBED_MODEL\").id" 2>/dev/null | head -1)"
|
|
108
|
+
if [ -f "$BIN/openviking-start" ]; then
|
|
109
|
+
CUR_AWS_TOKEN="$( (eval "$(grep '^export AWS_BEARER_TOKEN_BEDROCK=' "$BIN/openviking-start" 2>/dev/null)"; printf '%s' "${AWS_BEARER_TOKEN_BEDROCK:-}") )"
|
|
110
|
+
CUR_AWS_REGION="$( (eval "$(grep '^export AWS_REGION=' "$BIN/openviking-start" 2>/dev/null)"; printf '%s' "${AWS_REGION:-}") )"
|
|
111
|
+
fi
|
|
112
|
+
}
|
|
113
|
+
read_current
|
|
114
|
+
|
|
115
|
+
# ---- prereqs ---------------------------------------------------------------
|
|
116
|
+
say "checking prerequisites"
|
|
117
|
+
command -v curl >/dev/null 2>&1 || die "curl is required"
|
|
118
|
+
command -v jq >/dev/null 2>&1 || die "jq is required (https://jqlang.github.io/jq/)"
|
|
119
|
+
command -v python3 >/dev/null 2>&1 || die "python3 is required"
|
|
120
|
+
[ -f "$MODELS" ] || die "models.json not found next to the installer"
|
|
121
|
+
if ! command -v uv >/dev/null 2>&1; then
|
|
122
|
+
warn "uv not found - installing it"; curl -LsSf https://astral.sh/uv/install.sh | sh || die "uv install failed"; export PATH="$HOME/.local/bin:$PATH"
|
|
123
|
+
fi
|
|
124
|
+
ok "curl, jq, python3, uv present"
|
|
125
|
+
[ -n "$CUR_PROVIDER" ] && say "current ovmem: provider=$CUR_PROVIDER chat=${CUR_CHAT_ID:-$CUR_CHAT_MODEL} embed=${CUR_EMBED_ID:-$CUR_EMBED_MODEL} (${CUR_DIM}d)"
|
|
126
|
+
|
|
127
|
+
# ---- pick provider + models (default to current) ---------------------------
|
|
128
|
+
PROVIDER="${OVMEM_PROVIDER:-}"
|
|
129
|
+
if [ -z "$PROVIDER" ]; then
|
|
130
|
+
if [ -n "$TTY" ]; then
|
|
131
|
+
pdef=1; [ "$CUR_PROVIDER" = bedrock ] && pdef=2
|
|
132
|
+
om=""; [ "$CUR_PROVIDER" = openai ] && om=" (current)"; bm=""; [ "$CUR_PROVIDER" = bedrock ] && bm=" (current)"
|
|
133
|
+
printf "\nProvider:\n 1) openai%s\n 2) bedrock%s\n" "$om" "$bm" >&"$TTY"
|
|
134
|
+
case "$(ask "select" "$pdef")" in 2|bedrock) PROVIDER=bedrock ;; *) PROVIDER=openai ;; esac
|
|
135
|
+
else die "headless: set OVMEM_PROVIDER=openai|bedrock"; fi
|
|
136
|
+
fi
|
|
137
|
+
[ "$PROVIDER" = openai ] || [ "$PROVIDER" = bedrock ] || die "unknown provider: $PROVIDER"
|
|
138
|
+
|
|
139
|
+
pick_model() { # $1 kind $2 default_id -> echoes chosen id
|
|
140
|
+
local kind="$1" defid="${2:-}" preset="" i n choice line defnum=1 id mark
|
|
141
|
+
local ids=()
|
|
142
|
+
case "$kind" in chat) preset="${OVMEM_CHAT_MODEL:-}";; embed) preset="${OVMEM_EMBED_MODEL:-}";; esac
|
|
143
|
+
while IFS= read -r line; do [ -n "$line" ] && ids+=("$line"); done < <(j ".${kind}[] | select(.provider==\"$PROVIDER\") | .id")
|
|
144
|
+
[ "${#ids[@]}" -gt 0 ] || die "no $kind models for provider $PROVIDER in models.json"
|
|
145
|
+
if [ -n "$preset" ]; then printf "%s" "$preset"; return; fi
|
|
146
|
+
if [ -z "$TTY" ]; then die "headless: set OVMEM_$(printf '%s' "$kind" | tr '[:lower:]' '[:upper:]')_MODEL (one of: ${ids[*]})"; fi
|
|
147
|
+
i=1; for id in "${ids[@]}"; do [ "$id" = "$defid" ] && defnum=$i; i=$((i+1)); done
|
|
148
|
+
printf "\n%s model:\n" "$kind" >&"$TTY"; i=1
|
|
149
|
+
for id in "${ids[@]}"; do
|
|
150
|
+
mark=""; [ "$id" = "$defid" ] && mark=" (current)"
|
|
151
|
+
if [ "$kind" = chat ]; then
|
|
152
|
+
printf " %d) %-22s \$%s in / \$%s out per 1M%s\n" "$i" "$id" "$(j ".chat[]|select(.id==\"$id\").in")" "$(j ".chat[]|select(.id==\"$id\").out")" "$mark" >&"$TTY"
|
|
153
|
+
else
|
|
154
|
+
printf " %d) %-26s \$%s per 1M · %sd%s\n" "$i" "$id" "$(j ".embed[]|select(.id==\"$id\").price")" "$(j ".embed[]|select(.id==\"$id\").dim")" "$mark" >&"$TTY"
|
|
155
|
+
fi
|
|
156
|
+
i=$((i+1))
|
|
157
|
+
done
|
|
158
|
+
n="${#ids[@]}"; choice="$(ask "select" "$defnum")"
|
|
159
|
+
case "$choice" in (*[!0-9]*|"") choice=$defnum ;; esac
|
|
160
|
+
[ "$choice" -ge 1 ] && [ "$choice" -le "$n" ] || choice=$defnum
|
|
161
|
+
printf "%s" "${ids[$((choice-1))]}"
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
[ "$PROVIDER" = "$CUR_PROVIDER" ] && DEF_CHAT="$CUR_CHAT_ID" && DEF_EMBED="$CUR_EMBED_ID" || { DEF_CHAT=""; DEF_EMBED=""; }
|
|
165
|
+
CHAT_ID="$(pick_model chat "$DEF_CHAT")"
|
|
166
|
+
EMBED_ID="$(pick_model embed "$DEF_EMBED")"
|
|
167
|
+
CHAT_MODEL="$(j ".chat[]|select(.id==\"$CHAT_ID\").model")"
|
|
168
|
+
EMBED_MODEL="$(j ".embed[]|select(.id==\"$EMBED_ID\").model")"
|
|
169
|
+
EMBED_DIM="$(j ".embed[]|select(.id==\"$EMBED_ID\").dim")"
|
|
170
|
+
ok "provider=$PROVIDER chat=$CHAT_ID embed=$EMBED_ID (${EMBED_DIM}d)"
|
|
171
|
+
|
|
172
|
+
# ---- decide if the memory index must be rebuilt ----------------------------
|
|
173
|
+
REINDEX=0
|
|
174
|
+
if [ -d "$VDB" ] && [ -n "$CUR_EMBED_MODEL" ] && { [ "$EMBED_MODEL" != "$CUR_EMBED_MODEL" ] || [ "$EMBED_DIM" != "$CUR_DIM" ]; }; then
|
|
175
|
+
REINDEX=1
|
|
176
|
+
warn "embedding is changing: $CUR_EMBED_MODEL (${CUR_DIM}d) -> $EMBED_MODEL (${EMBED_DIM}d)."
|
|
177
|
+
warn "your memories stay; the vector index is rebuilt (re-embedded in the new model's space)."
|
|
178
|
+
[ -n "$TTY" ] && { confirm "proceed with the switch + rebuild?" || die "aborted; nothing changed."; }
|
|
179
|
+
fi
|
|
180
|
+
|
|
181
|
+
# ---- OpenViking (+ boto3 for bedrock) --------------------------------------
|
|
182
|
+
# First install downloads ~140 packages (OpenViking + LiteLLM + deps); warn so the long,
|
|
183
|
+
# quiet uv resolve/download step doesn't look frozen. Re-runs are near-instant.
|
|
184
|
+
ov_hint() { printf " \033[2m(downloading OpenViking + ~140 packages — up to a few minutes on first install)\033[0m\n"; }
|
|
185
|
+
uv_fail() { [ -n "$SPIN_OUT" ] && printf '%s\n' "$SPIN_OUT" | tail -5 | sed 's/^/ /'; die "$1"; }
|
|
186
|
+
if [ "$PROVIDER" = bedrock ]; then
|
|
187
|
+
say "ensuring OpenViking + boto3 (Bedrock)"
|
|
188
|
+
command -v openviking-server >/dev/null 2>&1 || ov_hint
|
|
189
|
+
run_spin "downloading OpenViking + boto3" uv tool install --with boto3 "$OPENVIKING_PIN" \
|
|
190
|
+
|| uv_fail "uv tool install (with boto3) failed"
|
|
191
|
+
elif ! command -v openviking-server >/dev/null 2>&1; then
|
|
192
|
+
say "installing OpenViking"; ov_hint
|
|
193
|
+
run_spin "downloading OpenViking + deps" uv tool install "$OPENVIKING_PIN" \
|
|
194
|
+
|| uv_fail "uv tool install failed"
|
|
195
|
+
fi
|
|
196
|
+
export PATH="$BIN:$PATH"
|
|
197
|
+
command -v openviking-server >/dev/null 2>&1 || die "openviking-server not on PATH (add $BIN)"
|
|
198
|
+
ok "OpenViking ready"
|
|
199
|
+
|
|
200
|
+
# ---- auth (reuse existing credential when possible) ------------------------
|
|
201
|
+
OPENAI_KEY=""; AWS_TOKEN=""; AWS_REGION_V=""
|
|
202
|
+
validate_openai() { local cc ec
|
|
203
|
+
cc="$(curl -s -o /tmp/ovm_c.json -w '%{http_code}' -m 30 https://api.openai.com/v1/chat/completions \
|
|
204
|
+
-H "Authorization: Bearer $1" -H "Content-Type: application/json" \
|
|
205
|
+
-d "{\"model\":\"$CHAT_MODEL\",\"max_tokens\":5,\"messages\":[{\"role\":\"user\",\"content\":\"ok\"}]}" 2>/dev/null || echo 000)"
|
|
206
|
+
if [ "$cc" != 200 ]; then grep -q "model.request" /tmp/ovm_c.json 2>/dev/null && warn "key missing the model.request (chat) scope" || warn "chat check failed (HTTP $cc)"; return 1; fi
|
|
207
|
+
ec="$(curl -s -o /tmp/ovm_e.json -w '%{http_code}' -m 30 https://api.openai.com/v1/embeddings \
|
|
208
|
+
-H "Authorization: Bearer $1" -H "Content-Type: application/json" -d "{\"model\":\"$EMBED_MODEL\",\"input\":\"ok\"}" 2>/dev/null || echo 000)"
|
|
209
|
+
[ "$ec" = 200 ] || { warn "embeddings check failed (HTTP $ec)"; return 1; }; return 0; }
|
|
210
|
+
|
|
211
|
+
if [ "$PROVIDER" = openai ]; then
|
|
212
|
+
if [ -n "$CUR_OPENAI_KEY" ] && [ "$CUR_OPENAI_KEY" != bedrock ] && validate_openai "$CUR_OPENAI_KEY" && { [ -z "$TTY" ] || confirm "reuse the existing OpenAI key?"; }; then
|
|
213
|
+
OPENAI_KEY="$CUR_OPENAI_KEY"; ok "reusing the existing OpenAI key (validated)"
|
|
214
|
+
elif [ -z "$TTY" ]; then
|
|
215
|
+
OPENAI_KEY="${OPENAI_API_KEY:-}"; [ -n "$OPENAI_KEY" ] || die "headless: set OPENAI_API_KEY"
|
|
216
|
+
validate_openai "$OPENAI_KEY" || die "OPENAI_API_KEY failed validation"; ok "key validated"
|
|
217
|
+
else
|
|
218
|
+
say "OpenAI key (needs embedding + model.request scopes; hidden)"
|
|
219
|
+
for _t in 1 2 3; do OPENAI_KEY="$(ask_secret " paste key")"
|
|
220
|
+
[ -n "$OPENAI_KEY" ] && validate_openai "$OPENAI_KEY" && { ok "key validated"; break; } || OPENAI_KEY=""
|
|
221
|
+
[ "$_t" = 3 ] && die "key validation failed 3x."; done
|
|
222
|
+
fi
|
|
223
|
+
rm -f /tmp/ovm_c.json /tmp/ovm_e.json
|
|
224
|
+
else
|
|
225
|
+
AWS_REGION_V="${AWS_REGION:-${CUR_AWS_REGION:-}}"; [ -n "$AWS_REGION_V" ] || AWS_REGION_V="$(ask "AWS region" "us-east-1")"
|
|
226
|
+
if [ -n "$CUR_AWS_TOKEN" ] && { [ -z "$TTY" ] || confirm "reuse the existing Bedrock API key?"; }; then
|
|
227
|
+
AWS_TOKEN="$CUR_AWS_TOKEN"; ok "reusing the existing Bedrock API key"
|
|
228
|
+
elif [ -z "$TTY" ]; then
|
|
229
|
+
AWS_TOKEN="${AWS_BEARER_TOKEN_BEDROCK:-}"; [ -n "$AWS_TOKEN" ] || die "headless: set AWS_BEARER_TOKEN_BEDROCK"
|
|
230
|
+
else
|
|
231
|
+
AWS_TOKEN="$(ask_secret " Bedrock API key (bearer token)")"; [ -n "$AWS_TOKEN" ] || die "no Bedrock API key given"
|
|
232
|
+
fi
|
|
233
|
+
ok "Bedrock region=$AWS_REGION_V (verified by the round-trip below)"
|
|
234
|
+
fi
|
|
235
|
+
|
|
236
|
+
# ---- ov.conf (preserve the root key on reconfigure) ------------------------
|
|
237
|
+
say "writing $OV_CONF"
|
|
238
|
+
mkdir -p "$OV_DIR" "$WORKSPACE"
|
|
239
|
+
[ -f "$OV_CONF" ] && cp "$OV_CONF" "$OV_CONF.ovmem.bak"
|
|
240
|
+
ROOTKEY="${CUR_ROOTKEY:-$(openssl rand -hex 16 2>/dev/null || echo ov-local-dev-key)}"
|
|
241
|
+
if [ "$PROVIDER" = openai ]; then
|
|
242
|
+
jq -n --arg key "$OPENAI_KEY" --arg ws "$WORKSPACE" --arg rk "$ROOTKEY" --arg cm "$CHAT_MODEL" --arg em "$EMBED_MODEL" --argjson dim "$EMBED_DIM" '{
|
|
243
|
+
storage:{workspace:$ws}, server:{host:"127.0.0.1",port:1933,root_api_key:$rk},
|
|
244
|
+
embedding:{dense:{provider:"openai",model:$em,api_key:$key,api_base:"https://api.openai.com/v1",dimension:$dim}},
|
|
245
|
+
vlm:{provider:"openai",model:$cm,api_key:$key,api_base:"https://api.openai.com/v1",temperature:0.0,max_tokens:16384,max_retries:2},
|
|
246
|
+
output_language_override:"en"}' > "$OV_CONF"
|
|
247
|
+
else
|
|
248
|
+
jq -n --arg ws "$WORKSPACE" --arg rk "$ROOTKEY" --arg cm "$CHAT_MODEL" --arg em "$EMBED_MODEL" --argjson dim "$EMBED_DIM" '{
|
|
249
|
+
storage:{workspace:$ws}, server:{host:"127.0.0.1",port:1933,root_api_key:$rk},
|
|
250
|
+
embedding:{dense:{provider:"litellm",model:$em,api_key:"bedrock",dimension:$dim}},
|
|
251
|
+
vlm:{provider:"litellm",model:$cm,api_key:"bedrock",temperature:0.0,max_tokens:8192,max_retries:2},
|
|
252
|
+
output_language_override:"en"}' > "$OV_CONF"
|
|
253
|
+
fi
|
|
254
|
+
chmod 600 "$OV_CONF"; ok "ov.conf written (chmod 600)"
|
|
255
|
+
|
|
256
|
+
# ---- server bootstrap wrapper (+ bedrock env) ------------------------------
|
|
257
|
+
mkdir -p "$BIN"
|
|
258
|
+
{ echo '#!/usr/bin/env bash'; echo '# Start OpenViking in the background if not already healthy.'
|
|
259
|
+
if [ "$PROVIDER" = bedrock ]; then printf 'export AWS_BEARER_TOKEN_BEDROCK=%q\n' "$AWS_TOKEN"; printf 'export AWS_REGION=%q\n' "$AWS_REGION_V"; printf 'export AWS_DEFAULT_REGION=%q\n' "$AWS_REGION_V"; fi
|
|
260
|
+
echo 'if curl -sf http://127.0.0.1:1933/health > /dev/null 2>&1; then exit 0; fi'
|
|
261
|
+
printf 'nohup "%s/openviking-server" --host 127.0.0.1 --port 1933 > /tmp/openviking.log 2>&1 &\n' "$BIN"
|
|
262
|
+
echo 'echo $! > /tmp/openviking.pid'; } > "$BIN/openviking-start"
|
|
263
|
+
chmod 700 "$BIN/openviking-start"
|
|
264
|
+
|
|
265
|
+
# ---- apply: stop (lock-safe) -> (rebuild index if needed) -> start ----------
|
|
266
|
+
H=(-H "x-api-key: $ROOTKEY" -H "X-OpenViking-Account: default" -H "X-OpenViking-User: ${USER:-default}" -H "X-OpenViking-Agent: claude-code" -H "Content-Type: application/json")
|
|
267
|
+
say "restarting OpenViking with the new config"
|
|
268
|
+
stop_server
|
|
269
|
+
if [ "$REINDEX" = 1 ] && [ -d "$VDB" ]; then
|
|
270
|
+
rm -rf "$VDB.ovmem.bak"; cp -r "$VDB" "$VDB.ovmem.bak"; rm -rf "$VDB"
|
|
271
|
+
warn "index backed up to $VDB.ovmem.bak and dropped (memory content untouched)"
|
|
272
|
+
fi
|
|
273
|
+
start_server
|
|
274
|
+
ok "server healthy on 127.0.0.1:1933"
|
|
275
|
+
|
|
276
|
+
if [ "$REINDEX" = 1 ]; then
|
|
277
|
+
say "re-embedding your memories with $EMBED_ID (content preserved)"
|
|
278
|
+
run_spin "re-embedding memories" \
|
|
279
|
+
curl -s -m 600 -X POST "${H[@]}" -d '{"uri":"viking://","wait":true}' "$API/content/reindex" || true
|
|
280
|
+
R="$SPIN_OUT"; [ -n "$R" ] || R='{}'
|
|
281
|
+
if echo "$R" | jq -e '(.result.status=="completed") and ((.result.failed_records // 0)==0)' >/dev/null 2>&1; then
|
|
282
|
+
ok "reindex complete ($(echo "$R" | jq -r '.result.rebuilt_records // 0') records re-embedded)"
|
|
283
|
+
rm -rf "$VDB.ovmem.bak"
|
|
284
|
+
else
|
|
285
|
+
warn "reindex did not complete cleanly — restoring the previous index"
|
|
286
|
+
stop_server; rm -rf "$VDB"; [ -d "$VDB.ovmem.bak" ] && mv "$VDB.ovmem.bak" "$VDB"
|
|
287
|
+
cp "$OV_CONF.ovmem.bak" "$OV_CONF" 2>/dev/null || true; start_server
|
|
288
|
+
die "the embedding switch failed; your previous setup ($CUR_PROVIDER/$CUR_EMBED_ID) was restored. See /tmp/openviking.log"
|
|
289
|
+
fi
|
|
290
|
+
fi
|
|
291
|
+
|
|
292
|
+
# ---- vendor scripts + wire hooks (idempotent) ------------------------------
|
|
293
|
+
say "installing ovmem engine + hooks"
|
|
294
|
+
mkdir -p "$OVMEM_DIR/state"
|
|
295
|
+
cp "$PAYLOAD/ovmem.py" "$OVMEM_DIR/ovmem.py"; cp "$PAYLOAD/ovmem-cleanup.py" "$OVMEM_DIR/ovmem-cleanup.py"
|
|
296
|
+
cp "$PAYLOAD/RUNTIME.md" "$OVMEM_DIR/README.md" 2>/dev/null || true
|
|
297
|
+
[ -f "$SETTINGS" ] || echo '{}' > "$SETTINGS"; cp "$SETTINGS" "$SETTINGS.ovmem.bak"
|
|
298
|
+
SS="python3 $OVMEM_DIR/ovmem.py --event session-start"; UP="python3 $OVMEM_DIR/ovmem.py --event user-prompt"
|
|
299
|
+
PC="python3 $OVMEM_DIR/ovmem.py --event pre-compact"; SE="python3 $OVMEM_DIR/ovmem.py --event session-end"
|
|
300
|
+
tmp="$(mktemp)"
|
|
301
|
+
jq --arg ss "$SS" --arg up "$UP" --arg pc "$PC" --arg se "$SE" '
|
|
302
|
+
.hooks //= {} | .hooks.SessionStart //= [] | .hooks.UserPromptSubmit //= [] | .hooks.PreCompact //= [] | .hooks.SessionEnd //= []
|
|
303
|
+
| (if any(.hooks.SessionStart[]?.hooks[]?; .command==$ss) then . else .hooks.SessionStart += [{hooks:[{type:"command",command:$ss,timeout:12}]}] end)
|
|
304
|
+
| (if any(.hooks.UserPromptSubmit[]?.hooks[]?;.command==$up) then . else .hooks.UserPromptSubmit += [{hooks:[{type:"command",command:$up,timeout:12}]}] end)
|
|
305
|
+
| (if any(.hooks.PreCompact[]?.hooks[]?; .command==$pc) then . else .hooks.PreCompact += [{hooks:[{type:"command",command:$pc,timeout:25}]}] end)
|
|
306
|
+
| (if any(.hooks.SessionEnd[]?.hooks[]?; .command==$se) then . else .hooks.SessionEnd += [{hooks:[{type:"command",command:$se,timeout:25}]}] end)
|
|
307
|
+
' "$SETTINGS" > "$tmp" && mv "$tmp" "$SETTINGS"
|
|
308
|
+
ok "engine + 4 hooks installed"
|
|
309
|
+
|
|
310
|
+
# ---- round-trip verify ------------------------------------------------------
|
|
311
|
+
say "verifying end-to-end (commit -> extract)"
|
|
312
|
+
SID="ovmem-install-check"
|
|
313
|
+
curl -s -m 10 -X POST "${H[@]}" -d "{\"session_id\":\"$SID\"}" "$API/sessions" >/dev/null 2>&1 || true
|
|
314
|
+
curl -s -m 12 -X POST "${H[@]}" -d '{"messages":[{"role":"user","content":"Install check: I prefer concise answers."},{"role":"assistant","content":"Noted."}]}' "$API/sessions/$SID/messages/batch" >/dev/null 2>&1 || true
|
|
315
|
+
run_spin "extracting memories (this can take up to ~2 min)" \
|
|
316
|
+
curl -s -m 150 -X POST "${H[@]}" "$API/sessions/$SID/extract" || true
|
|
317
|
+
EXTRACT="$SPIN_OUT"; [ -n "$EXTRACT" ] || EXTRACT='{}'
|
|
318
|
+
curl -s -m 8 -X DELETE "${H[@]}" "$API/sessions/$SID" >/dev/null 2>&1 || true
|
|
319
|
+
if echo "$EXTRACT" | jq -e '.status=="ok"' >/dev/null 2>&1; then
|
|
320
|
+
ok "extraction works (memories: $(echo "$EXTRACT" | jq '.result|if type=="array" then length else . end' 2>/dev/null))"
|
|
321
|
+
else
|
|
322
|
+
warn "extraction did not confirm. Last server errors:"
|
|
323
|
+
grep -iE "error|denied|expired|invalid|bedrock|token|scope|mismatch" /tmp/openviking.log 2>/dev/null | tail -3 | sed 's/^/ /' || true
|
|
324
|
+
[ "$PROVIDER" = bedrock ] && warn "Bedrock: check the API key, the region, and that the model is enabled in AWS Bedrock model access."
|
|
325
|
+
fi
|
|
326
|
+
|
|
327
|
+
[ -n "$TTY" ] && exec 3>&- || true
|
|
328
|
+
echo
|
|
329
|
+
ok "ovmem ready ($PROVIDER · $CHAT_ID · $EMBED_ID). Active on your NEXT Claude Code session."
|
|
330
|
+
echo " off: OVMEM_DISABLE=1 · debug: OVMEM_DEBUG=1 (~/.claude/ovmem/ovmem.log) · prune: python3 $OVMEM_DIR/ovmem-cleanup.py"
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ovmem extension - autonomous RAG long-term memory (OpenViking + 4 Claude Code hooks).
|
|
3
|
+
#
|
|
4
|
+
# All subcommands are real: detect / status / doctor probe a live install;
|
|
5
|
+
# install / update run the full installer (also the reconfigure / provider-switch path,
|
|
6
|
+
# with credential reuse + a safe index rebuild); remove unwires the hooks and deletes the
|
|
7
|
+
# engine, leaving your OpenViking server + memory data intact. See README.md in this folder.
|
|
8
|
+
set -euo pipefail
|
|
9
|
+
|
|
10
|
+
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
11
|
+
CLAUDE="${CLAUDE_HOME:-$HOME/.claude}"
|
|
12
|
+
OVMEM_DIR="$CLAUDE/ovmem"
|
|
13
|
+
SETTINGS="$CLAUDE/settings.json"
|
|
14
|
+
OV_HEALTH="http://127.0.0.1:1933/health"
|
|
15
|
+
|
|
16
|
+
server_up() { curl -s -m 2 "$OV_HEALTH" 2>/dev/null | grep -q '"healthy":true'; }
|
|
17
|
+
|
|
18
|
+
case "${1:-}" in
|
|
19
|
+
detect)
|
|
20
|
+
[ -f "$OVMEM_DIR/ovmem.py" ]
|
|
21
|
+
;;
|
|
22
|
+
|
|
23
|
+
status)
|
|
24
|
+
if [ -f "$OVMEM_DIR/ovmem.py" ]; then
|
|
25
|
+
if server_up; then echo "server up"; else echo "server down"; fi
|
|
26
|
+
else
|
|
27
|
+
echo "not installed"
|
|
28
|
+
fi
|
|
29
|
+
;;
|
|
30
|
+
|
|
31
|
+
install|update)
|
|
32
|
+
# install.sh is also the reconfigure/switch path: it detects the current setup,
|
|
33
|
+
# offers to reuse the credential, and rebuilds the index when the embedding changes.
|
|
34
|
+
bash "$HERE/install.sh"
|
|
35
|
+
;;
|
|
36
|
+
|
|
37
|
+
remove)
|
|
38
|
+
if [ -f "$SETTINGS" ] && command -v jq >/dev/null 2>&1; then
|
|
39
|
+
cp "$SETTINGS" "$SETTINGS.ovmem.bak"
|
|
40
|
+
tmp="$(mktemp)"
|
|
41
|
+
jq '
|
|
42
|
+
if .hooks then
|
|
43
|
+
.hooks |= ( to_entries
|
|
44
|
+
| map(.value |= ( map( .hooks |= map(select((.command // "") | test("ovmem.py --event") | not)) )
|
|
45
|
+
| map(select((.hooks | length) > 0)) ))
|
|
46
|
+
| from_entries )
|
|
47
|
+
else . end
|
|
48
|
+
' "$SETTINGS" > "$tmp" && mv "$tmp" "$SETTINGS"
|
|
49
|
+
echo "unwired ovmem hooks (backup at $SETTINGS.ovmem.bak)"
|
|
50
|
+
fi
|
|
51
|
+
rm -rf "$OVMEM_DIR"
|
|
52
|
+
echo "removed $OVMEM_DIR"
|
|
53
|
+
echo "left untouched: the OpenViking server + ~/.openviking (your memory data)."
|
|
54
|
+
echo "to purge those too: uv tool uninstall openviking ; rm -rf ~/.openviking"
|
|
55
|
+
;;
|
|
56
|
+
|
|
57
|
+
doctor)
|
|
58
|
+
echo "engine: $([ -f "$OVMEM_DIR/ovmem.py" ] && echo "$OVMEM_DIR/ovmem.py" || echo missing)"
|
|
59
|
+
echo "cleanup: $([ -f "$OVMEM_DIR/ovmem-cleanup.py" ] && echo present || echo missing)"
|
|
60
|
+
echo "server: $(server_up && echo "up (127.0.0.1:1933)" || echo "down")"
|
|
61
|
+
if [ -f "$SETTINGS" ]; then
|
|
62
|
+
local_hooks="$(grep -c 'ovmem.py --event' "$SETTINGS" 2>/dev/null || echo 0)"
|
|
63
|
+
echo "hooks: $local_hooks/4 wired in settings.json"
|
|
64
|
+
else
|
|
65
|
+
echo "hooks: settings.json not found"
|
|
66
|
+
fi
|
|
67
|
+
if [ -f "$HOME/.openviking/ov.conf" ] && command -v jq >/dev/null 2>&1; then
|
|
68
|
+
prov="$(jq -r '.vlm.provider // "?"' "$HOME/.openviking/ov.conf" 2>/dev/null)"
|
|
69
|
+
chat="$(jq -r '.vlm.model // "?"' "$HOME/.openviking/ov.conf" 2>/dev/null)"
|
|
70
|
+
emb="$(jq -r '.embedding.dense.model // "?"' "$HOME/.openviking/ov.conf" 2>/dev/null)"
|
|
71
|
+
lang="$(jq -r '.output_language_override // "auto"' "$HOME/.openviking/ov.conf" 2>/dev/null)"
|
|
72
|
+
echo "provider: $prov"
|
|
73
|
+
echo "chat: $chat"
|
|
74
|
+
echo "embed: $emb"
|
|
75
|
+
echo "ov.conf: present (lang=$lang)"
|
|
76
|
+
elif [ -f "$HOME/.openviking/ov.conf" ]; then
|
|
77
|
+
echo "ov.conf: present"
|
|
78
|
+
else
|
|
79
|
+
echo "ov.conf: missing"
|
|
80
|
+
fi
|
|
81
|
+
;;
|
|
82
|
+
|
|
83
|
+
*)
|
|
84
|
+
echo "usage: manage.sh {detect|status|install|update|remove|doctor}" >&2
|
|
85
|
+
exit 2
|
|
86
|
+
;;
|
|
87
|
+
esac
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_meta": {
|
|
3
|
+
"prices": "USD per 1M tokens (input/output for chat; flat for embedding). Approximate, for guidance only.",
|
|
4
|
+
"source": "LiteLLM model_prices_and_context_window map",
|
|
5
|
+
"as_of": "2026-06",
|
|
6
|
+
"note": "ovmem only runs extraction at PreCompact/SessionEnd, so real cost is cents. Verify prices on the provider's pricing page."
|
|
7
|
+
},
|
|
8
|
+
"chat": [
|
|
9
|
+
{ "id": "gpt-4o-mini", "provider": "openai", "model": "gpt-4o-mini", "in": 0.15, "out": 0.60, "ctx": "128k", "tag": "cheap default" },
|
|
10
|
+
{ "id": "gpt-4.1-mini", "provider": "openai", "model": "gpt-4.1-mini", "in": 0.40, "out": 1.60, "ctx": "1M" },
|
|
11
|
+
{ "id": "gpt-4o", "provider": "openai", "model": "gpt-4o", "in": 2.50, "out": 10.00, "ctx": "128k" },
|
|
12
|
+
{ "id": "nova-lite", "provider": "bedrock", "model": "bedrock/us.amazon.nova-lite-v1:0", "in": 0.06, "out": 0.24, "ctx": "300k", "tag": "cheapest" },
|
|
13
|
+
{ "id": "claude-3-5-haiku", "provider": "bedrock", "model": "bedrock/us.anthropic.claude-3-5-haiku-20241022-v1:0", "in": 0.80, "out": 4.00, "ctx": "200k" },
|
|
14
|
+
{ "id": "claude-3-5-sonnet", "provider": "bedrock", "model": "bedrock/us.anthropic.claude-3-5-sonnet-20241022-v2:0","in": 3.00, "out": 15.00, "ctx": "200k" },
|
|
15
|
+
{ "id": "claude-sonnet-4-5", "provider": "bedrock", "model": "bedrock/us.anthropic.claude-sonnet-4-5-20250929-v1:0","in": 3.30, "out": 16.50, "ctx": "200k" }
|
|
16
|
+
],
|
|
17
|
+
"embed": [
|
|
18
|
+
{ "id": "text-embedding-3-small", "provider": "openai", "model": "text-embedding-3-small", "price": 0.02, "dim": 1536, "tag": "default" },
|
|
19
|
+
{ "id": "text-embedding-3-large", "provider": "openai", "model": "text-embedding-3-large", "price": 0.13, "dim": 3072 },
|
|
20
|
+
{ "id": "titan-embed-v2", "provider": "bedrock", "model": "bedrock/amazon.titan-embed-text-v2:0","price": 0.02, "dim": 1024 },
|
|
21
|
+
{ "id": "cohere-embed-v3", "provider": "bedrock", "model": "bedrock/cohere.embed-multilingual-v3","price": 0.10, "dim": 1024 },
|
|
22
|
+
{ "id": "titan-embed-v1", "provider": "bedrock", "model": "bedrock/amazon.titan-embed-text-v1", "price": 0.10, "dim": 1536 }
|
|
23
|
+
]
|
|
24
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# ovmem - autonomous RAG memory for Claude Code
|
|
2
|
+
|
|
3
|
+
Wires **OpenViking** (hierarchical context DB at `127.0.0.1:1933`) to Claude Code via
|
|
4
|
+
**4 native hooks**, so any session stays optimized without destructive `/compact` or
|
|
5
|
+
`/clear`. All reflection/distillation happens **server-side** (OpenViking calls the LLM),
|
|
6
|
+
so the hooks never spend an LLM call of their own.
|
|
7
|
+
|
|
8
|
+
## Flow
|
|
9
|
+
|
|
10
|
+
| Hook | What it does | OpenViking endpoint |
|
|
11
|
+
|---|---|---|
|
|
12
|
+
| **SessionStart** | start the server if down + rehydrate (session summary + long-term memory) | `GET /sessions/{id}/context`, `POST /search/find` |
|
|
13
|
+
| **UserPromptSubmit** | recall: inject memory relevant to the prompt (token-budgeted) | `POST /search/find` |
|
|
14
|
+
| **PreCompact** | flush: send the transcript delta to the OV session and commit **before** compaction destroys it | `messages/batch` + `commit` |
|
|
15
|
+
| **SessionEnd** | flush + commit on session end, then maybe run the weekly cleanup | `messages/batch` + `commit` |
|
|
16
|
+
|
|
17
|
+
`commit` archives the session and kicks off **long-term memory extraction**
|
|
18
|
+
(preferences / entities / events / agent) asynchronously, using the VLM configured
|
|
19
|
+
in OpenViking (`gpt-4o-mini`).
|
|
20
|
+
|
|
21
|
+
Read-hook output is **plain text** (not JSON) so it concatenates cleanly with other hooks
|
|
22
|
+
on the same event (e.g. `skill-activator.sh`). The hooks **never** exit non-zero - they
|
|
23
|
+
fail silently (fail-open) and never block the session.
|
|
24
|
+
|
|
25
|
+
## Files
|
|
26
|
+
|
|
27
|
+
- `ovmem.py` - single engine, dispatched by `--event {session-start|user-prompt|pre-compact|session-end}`. Pure stdlib, no deps.
|
|
28
|
+
- `ovmem-cleanup.py` - hotness-based lifecycle prune (see below).
|
|
29
|
+
- `state/<session_id>.json` - transcript offset already committed (avoids re-sending).
|
|
30
|
+
- `state/access.json` - local access signal (frequency + recency) feeding the cleanup.
|
|
31
|
+
- `ovmem.log` - debug log (only with `OVMEM_DEBUG=1`).
|
|
32
|
+
|
|
33
|
+
Registered in `~/.claude/settings.json` under `hooks`.
|
|
34
|
+
|
|
35
|
+
## Controls (env vars)
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
OVMEM_DISABLE=1 turn everything off (immediate no-op)
|
|
39
|
+
OVMEM_DEBUG=1 log to ~/.claude/ovmem/ovmem.log
|
|
40
|
+
OVMEM_RECALL_LIMIT=5 max memories injected (default 5)
|
|
41
|
+
OVMEM_RECALL_SCORE=0.28 minimum score to inject (default 0.28)
|
|
42
|
+
OVMEM_CHAR_BUDGET=2200 char cap on the injected block (default 2200)
|
|
43
|
+
OVMEM_TIMEOUT=4 timeout (s) for critical-path calls
|
|
44
|
+
OVMEM_ACCOUNT=default OVMEM_USER=$USER OVMEM_AGENT=claude-code
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Accumulation / lifecycle (dedup, reconsolidation, pruning)
|
|
48
|
+
|
|
49
|
+
The biggest risk for long-term memory is accumulation: duplicates, stale notes, and cold
|
|
50
|
+
episodes polluting retrieval. Division of responsibility:
|
|
51
|
+
|
|
52
|
+
| Problem | Mechanism | Where |
|
|
53
|
+
|---|---|---|
|
|
54
|
+
| **Duplication** | `MemoryDeduplicator` (LLM) - updates the existing note instead of creating another | **native OpenViking**, on commit |
|
|
55
|
+
| **Obsolescence / contradiction** | `contradicts` / `evolved_from` / `supersedes` relations during extraction | **native OpenViking**, on commit |
|
|
56
|
+
| **Cold-memory accumulation** | `ovmem-cleanup.py` - hotness pruning | **ovmem** (the engine has `MemoryArchiver` but never triggers it) |
|
|
57
|
+
|
|
58
|
+
Verified empirically: committing the same preference across N sessions yields **1** leaf
|
|
59
|
+
(updated), not N duplicates. Dedup and reconsolidation already work - do not rebuild them.
|
|
60
|
+
|
|
61
|
+
**Hotness pruning** (`ovmem-cleanup.py`), faithful to the native `MemoryArchiver`:
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
hotness = sigmoid(log1p(freq)) * exp(-ln2/half_life * age_days)
|
|
65
|
+
freq = max(OpenViking active_count, local recall count)
|
|
66
|
+
age = from the more recent of updated_at and last local recall
|
|
67
|
+
archive L2 leaves with hotness < threshold AND age >= min_age -> {parent}/_archive/
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
- The frequency signal comes from **recall**: every memory the hook injects is recorded in
|
|
71
|
+
`state/access.json` (`record_access`). We do not rely on OpenViking's `active_count` -
|
|
72
|
+
it does not increment reliably via the REST `used` endpoint in this version.
|
|
73
|
+
- Runs **dry-run by default**; `--apply` moves to `_archive/` (reversible, nothing deleted).
|
|
74
|
+
- Auto-trigger: the **SessionEnd** hook calls `ovmem-cleanup.py --apply` at most **once a
|
|
75
|
+
week** (gated by `state/last_cleanup`), detached. No cron.
|
|
76
|
+
- Protected (never archived): `identity.md`, `soul.md` (agent core identity).
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
python3 ~/.claude/ovmem/ovmem-cleanup.py # dry-run: list cold candidates
|
|
80
|
+
python3 ~/.claude/ovmem/ovmem-cleanup.py --apply # archive them
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Tunables: `OVMEM_HOTNESS_THRESHOLD=0.1` `OVMEM_HALF_LIFE_DAYS=7` `OVMEM_MIN_AGE_DAYS=7`
|
|
84
|
+
`OVMEM_CLEANUP_PROTECT="identity.md,soul.md"`.
|
|
85
|
+
|
|
86
|
+
## Verify
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
# server alive?
|
|
90
|
+
curl -s http://127.0.0.1:1933/health
|
|
91
|
+
|
|
92
|
+
# manual recall
|
|
93
|
+
echo '{"prompt":"what is my preferred stack?","cwd":"'"$PWD"'"}' \
|
|
94
|
+
| python3 ~/.claude/ovmem/ovmem.py --event user-prompt
|
|
95
|
+
|
|
96
|
+
# inspect long-term memory
|
|
97
|
+
curl -s -H "x-api-key: ov-local-dev-key" -H "X-OpenViking-User: $USER" \
|
|
98
|
+
"http://127.0.0.1:1933/api/v1/fs/tree?uri=viking://user/$USER/memories&level_limit=4"
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## OpenViking config dependencies
|
|
102
|
+
|
|
103
|
+
`~/.openviking/ov.conf` (backup in `ov.conf.bak`):
|
|
104
|
+
- `embedding.dense` -> OpenAI `text-embedding-3-small` (recall / semantic search).
|
|
105
|
+
- `vlm` -> OpenAI `gpt-4o-mini`, **`max_tokens: 16384`** (the model's cap; without it
|
|
106
|
+
OpenViking requests 32768 and OpenAI rejects with 400).
|
|
107
|
+
- `output_language_override: "en"` -> forces memory extraction and summaries/overviews to
|
|
108
|
+
English regardless of conversation language.
|
|
109
|
+
- The API key needs the **`model.request`** scope (chat), not just embedding - otherwise
|
|
110
|
+
extraction fails with 401 and long-term memory never populates.
|
|
111
|
+
|
|
112
|
+
> Local alternative (no OpenAI): point `vlm.api_base` at an Ollama instance running a model
|
|
113
|
+
> that does real **tool-calling** (qwen2.5:7b+). `nemotron-3-nano:4b` does not follow
|
|
114
|
+
> OpenViking's "operations" protocol.
|
|
115
|
+
|
|
116
|
+
## Server persistence
|
|
117
|
+
|
|
118
|
+
- The **SessionStart** hook calls `~/.local/bin/openviking-start` (idempotent) if the
|
|
119
|
+
server is down - auto-bootstrap.
|
|
120
|
+
- For full uptime independent of Claude Code, add `~/.local/bin/openviking-start` to
|
|
121
|
+
`~/.bashrc`.
|