leopold-driver 0.1.1 → 0.1.3

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.
Files changed (48) hide show
  1. package/README.md +19 -5
  2. package/assets/VERSION +1 -0
  3. package/assets/extensions/README.md +52 -0
  4. package/assets/extensions/gstack/extension.json +8 -0
  5. package/assets/extensions/gstack/manage.sh +68 -0
  6. package/assets/extensions/leopold/extension.json +8 -0
  7. package/assets/extensions/leopold/manage.sh +59 -0
  8. package/assets/extensions/ovmem/README.md +101 -0
  9. package/assets/extensions/ovmem/extension.json +8 -0
  10. package/assets/extensions/ovmem/install.sh +330 -0
  11. package/assets/extensions/ovmem/manage.sh +87 -0
  12. package/assets/extensions/ovmem/models.json +24 -0
  13. package/assets/extensions/ovmem/payload/RUNTIME.md +121 -0
  14. package/assets/extensions/ovmem/payload/ovmem-cleanup.py +148 -0
  15. package/assets/extensions/ovmem/payload/ovmem.py +421 -0
  16. package/assets/extensions/serena/README.md +50 -0
  17. package/assets/extensions/serena/extension.json +8 -0
  18. package/assets/extensions/serena/manage.sh +119 -0
  19. package/assets/hooks/guard-irreversible.sh +185 -0
  20. package/assets/hooks/hooks.json +20 -0
  21. package/assets/hooks/stop-continuity.sh +132 -0
  22. package/assets/install.sh +184 -0
  23. package/assets/scripts/__pycache__/leopold-watch.cpython-312.pyc +0 -0
  24. package/assets/scripts/leopold-doctor.sh +53 -0
  25. package/assets/scripts/leopold-menu.sh +132 -0
  26. package/assets/scripts/leopold-update-check.sh +23 -0
  27. package/assets/scripts/leopold-update.sh +13 -0
  28. package/assets/scripts/leopold-watch.py +585 -0
  29. package/assets/scripts/record-demo.sh +61 -0
  30. package/assets/scripts/test-guard.sh +76 -0
  31. package/assets/scripts/test-hooks.sh +121 -0
  32. package/assets/settings.template.json +23 -0
  33. package/assets/skills/leopold-brief/SKILL.md +121 -0
  34. package/assets/skills/leopold-doctor/SKILL.md +23 -0
  35. package/assets/skills/leopold-run/SKILL.md +171 -0
  36. package/assets/skills/leopold-status/SKILL.md +34 -0
  37. package/assets/skills/leopold-stop/SKILL.md +36 -0
  38. package/assets/skills/leopold-update/SKILL.md +27 -0
  39. package/assets/skills/leopold-watch/SKILL.md +48 -0
  40. package/assets/templates/CHARTER.md +32 -0
  41. package/assets/templates/DECISIONS.md +15 -0
  42. package/assets/templates/GUARDRAILS.md +38 -0
  43. package/assets/templates/MISSION.md +22 -0
  44. package/assets/templates/PLAN.md +9 -0
  45. package/dist/guard.js +82 -23
  46. package/dist/harness.js +71 -0
  47. package/dist/index.js +53 -23
  48. package/package.json +6 -3
@@ -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`.