Add Ollama provider, portable memory, cultivation digest, refugee importer, GLM-OCR spike
- P0: unified soul binary with engram_node_full fix, read-back-verify, search fix - P0: move API keys from plaintext plists to macOS Keychain - P0: fix MCP backend URL (port 8742 → 7770) - P1.6: memory-export/import scripts (AES-256-CBC, versioned .neuronmem format) - P1.7: nightly cultivation digest with sharpness metric (launchd at 23:55) - P2.10: Ollama provider in agentic loop (SOUL_LLM_PROVIDER=ollama) - P3.12: refugee importer for ChatGPT/Screenpipe/generic formats - P3.13: GLM-OCR spike — SHIP IT (mlx-vlm, 1.59GB, photo-to-memory.sh)
This commit is contained in:
Executable
+289
@@ -0,0 +1,289 @@
|
||||
#!/usr/bin/env bash
|
||||
# memory-import.sh — Import a Neuron .neuronmem bundle onto this device
|
||||
#
|
||||
# Usage:
|
||||
# ./tools/memory-import.sh input.neuronmem [--passphrase "your passphrase"]
|
||||
# ./tools/memory-import.sh input.neuronmem [--dry-run] # verify only, no changes
|
||||
#
|
||||
# The script will:
|
||||
# 1. Decrypt and unpack the .neuronmem file
|
||||
# 2. Validate the checksum and version
|
||||
# 3. Back up the current snapshot.json
|
||||
# 4. Stop the soul service
|
||||
# 5. Replace snapshot.json
|
||||
# 6. Restart the soul service
|
||||
# 7. Verify the soul came back up
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ── Config ─────────────────────────────────────────────────────────────────────
|
||||
ENGRAM_SNAPSHOT="${HOME}/.neuron/engram/snapshot.json"
|
||||
SOUL_SERVICE="ai.neurontechnologies.soul"
|
||||
SOUL_PORT="7770"
|
||||
SOUL_STARTUP_TIMEOUT=30 # seconds to wait for soul to come back
|
||||
|
||||
# ── Parse args ─────────────────────────────────────────────────────────────────
|
||||
INPUT_PATH=""
|
||||
PASSPHRASE=""
|
||||
PASSPHRASE_SET=0
|
||||
DRY_RUN=0
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--passphrase)
|
||||
PASSPHRASE="$2"
|
||||
PASSPHRASE_SET=1
|
||||
shift 2
|
||||
;;
|
||||
--passphrase=*)
|
||||
PASSPHRASE="${1#*=}"
|
||||
PASSPHRASE_SET=1
|
||||
shift
|
||||
;;
|
||||
--dry-run)
|
||||
DRY_RUN=1
|
||||
shift
|
||||
;;
|
||||
-*)
|
||||
echo "Unknown option: $1" >&2
|
||||
echo "Usage: $0 input.neuronmem [--passphrase \"...\"] [--dry-run]" >&2
|
||||
exit 1
|
||||
;;
|
||||
*)
|
||||
if [[ -z "$INPUT_PATH" ]]; then
|
||||
INPUT_PATH="$1"
|
||||
else
|
||||
echo "Unexpected argument: $1" >&2
|
||||
exit 1
|
||||
fi
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$INPUT_PATH" ]]; then
|
||||
echo "ERROR: No input file specified." >&2
|
||||
echo "Usage: $0 input.neuronmem [--passphrase \"...\"] [--dry-run]" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "$INPUT_PATH" ]]; then
|
||||
echo "ERROR: Input file not found: $INPUT_PATH" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Neuron Memory Import"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo "Source: $INPUT_PATH"
|
||||
echo "Target: $ENGRAM_SNAPSHOT"
|
||||
if [[ $DRY_RUN -eq 1 ]]; then
|
||||
echo "Mode: DRY RUN (no changes will be made)"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# ── Prompt for passphrase if needed ───────────────────────────────────────────
|
||||
if [[ $PASSPHRASE_SET -eq 0 ]]; then
|
||||
read -r -s -p "Enter passphrase: " PASSPHRASE
|
||||
echo ""
|
||||
if [[ -z "$PASSPHRASE" ]]; then
|
||||
echo "ERROR: Passphrase cannot be empty." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── Decrypt to temp dir ────────────────────────────────────────────────────────
|
||||
WORK_DIR="$(mktemp -d)"
|
||||
CLEANUP() {
|
||||
rm -rf "$WORK_DIR"
|
||||
}
|
||||
trap CLEANUP EXIT
|
||||
|
||||
TAR_PATH="${WORK_DIR}/bundle.tar.gz"
|
||||
|
||||
echo "Decrypting..."
|
||||
if ! openssl enc -d -aes-256-cbc \
|
||||
-pbkdf2 \
|
||||
-iter 600000 \
|
||||
-in "$INPUT_PATH" \
|
||||
-out "$TAR_PATH" \
|
||||
-pass "pass:${PASSPHRASE}" 2>/dev/null; then
|
||||
echo "ERROR: Decryption failed. Wrong passphrase or corrupted file." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo " Decrypted successfully."
|
||||
|
||||
# ── Unpack ─────────────────────────────────────────────────────────────────────
|
||||
echo "Unpacking..."
|
||||
(cd "$WORK_DIR" && tar xzf "$TAR_PATH") || {
|
||||
echo "ERROR: Failed to unpack bundle. File may be corrupted." >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Locate the bundle directory (neuronmem-v1/)
|
||||
BUNDLE_DIR=""
|
||||
for d in "${WORK_DIR}"/neuronmem-v*/; do
|
||||
if [[ -d "$d" ]]; then
|
||||
BUNDLE_DIR="$d"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ -z "$BUNDLE_DIR" ]]; then
|
||||
echo "ERROR: Bundle directory not found. Invalid .neuronmem file." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
METADATA_FILE="${BUNDLE_DIR}metadata.json"
|
||||
NODES_FILE="${BUNDLE_DIR}nodes.json"
|
||||
|
||||
if [[ ! -f "$METADATA_FILE" ]]; then
|
||||
echo "ERROR: metadata.json missing from bundle." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "$NODES_FILE" ]]; then
|
||||
echo "ERROR: nodes.json missing from bundle." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ── Validate metadata ──────────────────────────────────────────────────────────
|
||||
echo "Validating metadata..."
|
||||
FORMAT_VERSION="$(python3 -c "import json; d=json.load(open('${METADATA_FILE}')); print(d.get('version','?'))")"
|
||||
EXPORTED_AT="$(python3 -c "import json; d=json.load(open('${METADATA_FILE}')); print(d.get('exported_at','?'))")"
|
||||
EXPECTED_COUNT="$(python3 -c "import json; d=json.load(open('${METADATA_FILE}')); print(d.get('node_count','?'))")"
|
||||
STORED_CHECKSUM="$(python3 -c "import json; d=json.load(open('${METADATA_FILE}')); print(d.get('sha256','?'))")"
|
||||
SOURCE_HOST="$(python3 -c "import json; d=json.load(open('${METADATA_FILE}')); print(d.get('source_host','?'))")"
|
||||
|
||||
echo " Format version: ${FORMAT_VERSION}"
|
||||
echo " Exported at: ${EXPORTED_AT}"
|
||||
echo " Source host: ${SOURCE_HOST}"
|
||||
echo " Expected nodes: ${EXPECTED_COUNT}"
|
||||
|
||||
if [[ "$FORMAT_VERSION" != "1" ]]; then
|
||||
echo "ERROR: Unsupported bundle format version: ${FORMAT_VERSION}" >&2
|
||||
echo " This tool supports version 1 only." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ── Validate checksum ──────────────────────────────────────────────────────────
|
||||
echo "Verifying checksum..."
|
||||
ACTUAL_CHECKSUM="$(openssl dgst -sha256 "$NODES_FILE" | awk '{print $NF}')"
|
||||
|
||||
if [[ "$ACTUAL_CHECKSUM" != "$STORED_CHECKSUM" ]]; then
|
||||
echo "ERROR: Checksum mismatch!" >&2
|
||||
echo " Expected: ${STORED_CHECKSUM}" >&2
|
||||
echo " Got: ${ACTUAL_CHECKSUM}" >&2
|
||||
echo " The bundle may be corrupted." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo " Checksum OK: ${ACTUAL_CHECKSUM:0:16}..."
|
||||
|
||||
# ── Verify node count ──────────────────────────────────────────────────────────
|
||||
echo "Verifying node count..."
|
||||
ACTUAL_COUNT="$(python3 -c "
|
||||
import json
|
||||
with open('${NODES_FILE}') as f:
|
||||
d = json.load(f)
|
||||
nodes = d.get('nodes', d if isinstance(d, list) else [])
|
||||
print(len(nodes) if isinstance(nodes, list) else len(nodes))
|
||||
" 2>/dev/null || echo "unknown")"
|
||||
|
||||
echo " Found ${ACTUAL_COUNT} nodes (expected ${EXPECTED_COUNT})"
|
||||
|
||||
if [[ "$ACTUAL_COUNT" != "$EXPECTED_COUNT" && "$EXPECTED_COUNT" != "unknown" ]]; then
|
||||
echo "WARNING: Node count mismatch (expected ${EXPECTED_COUNT}, found ${ACTUAL_COUNT})." >&2
|
||||
echo " Proceeding anyway — count may differ if nodes were deduplicated." >&2
|
||||
fi
|
||||
|
||||
# ── Dry run exit ───────────────────────────────────────────────────────────────
|
||||
if [[ $DRY_RUN -eq 1 ]]; then
|
||||
echo ""
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo "DRY RUN complete. Bundle is valid."
|
||||
echo " Nodes: ${ACTUAL_COUNT}"
|
||||
echo " Checksum: verified"
|
||||
echo " Run without --dry-run to import."
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ── Safety confirmation ────────────────────────────────────────────────────────
|
||||
echo ""
|
||||
echo "WARNING: This will replace your current Neuron memory store."
|
||||
echo " Current snapshot: $ENGRAM_SNAPSHOT"
|
||||
echo " A backup will be created before replacing."
|
||||
echo ""
|
||||
read -r -p "Type 'yes' to continue: " CONFIRM
|
||||
if [[ "$CONFIRM" != "yes" ]]; then
|
||||
echo "Aborted."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ── Backup existing snapshot ───────────────────────────────────────────────────
|
||||
BACKUP_TIMESTAMP="$(date -u +"%Y%m%dT%H%M%SZ")"
|
||||
ENGRAM_DIR="$(dirname "$ENGRAM_SNAPSHOT")"
|
||||
BACKUP_PATH="${HOME}/.neuron/engram-backup-${BACKUP_TIMESTAMP}.tar.gz"
|
||||
|
||||
echo ""
|
||||
echo "Backing up current snapshot..."
|
||||
if [[ -f "$ENGRAM_SNAPSHOT" ]]; then
|
||||
(cd "$HOME/.neuron" && tar czf "$BACKUP_PATH" "$(basename "$ENGRAM_DIR")/snapshot.json" 2>/dev/null) || \
|
||||
cp "$ENGRAM_SNAPSHOT" "${ENGRAM_SNAPSHOT}.backup-${BACKUP_TIMESTAMP}"
|
||||
echo " Backup: $BACKUP_PATH"
|
||||
else
|
||||
echo " No existing snapshot to back up."
|
||||
fi
|
||||
|
||||
# ── Stop soul service ──────────────────────────────────────────────────────────
|
||||
echo "Stopping soul service (${SOUL_SERVICE})..."
|
||||
launchctl stop "$SOUL_SERVICE" 2>/dev/null || true
|
||||
# Also stop engram service if running
|
||||
launchctl stop "ai.neuron.engram" 2>/dev/null || true
|
||||
sleep 2
|
||||
echo " Soul stopped."
|
||||
|
||||
# ── Replace snapshot.json ──────────────────────────────────────────────────────
|
||||
echo "Installing new snapshot..."
|
||||
cp "$NODES_FILE" "$ENGRAM_SNAPSHOT"
|
||||
echo " snapshot.json replaced ($(du -sh "$ENGRAM_SNAPSHOT" | cut -f1))"
|
||||
|
||||
# ── Restart soul service ───────────────────────────────────────────────────────
|
||||
echo "Restarting soul service..."
|
||||
launchctl start "$SOUL_SERVICE" 2>/dev/null || true
|
||||
launchctl start "ai.neuron.engram" 2>/dev/null || true
|
||||
|
||||
# ── Wait for soul to come up ───────────────────────────────────────────────────
|
||||
echo "Waiting for soul to come up on port ${SOUL_PORT}..."
|
||||
ELAPSED=0
|
||||
SOUL_UP=0
|
||||
while [[ $ELAPSED -lt $SOUL_STARTUP_TIMEOUT ]]; do
|
||||
if curl -sf "http://localhost:${SOUL_PORT}/" > /dev/null 2>&1; then
|
||||
SOUL_UP=1
|
||||
break
|
||||
fi
|
||||
# Try a known endpoint that returns any response (even 404 means it's up)
|
||||
HTTP_CODE="$(curl -s -o /dev/null -w "%{http_code}" "http://localhost:${SOUL_PORT}/api/neuron/memory" 2>/dev/null || echo "000")"
|
||||
if [[ "$HTTP_CODE" != "000" ]]; then
|
||||
SOUL_UP=1
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
ELAPSED=$((ELAPSED + 1))
|
||||
done
|
||||
|
||||
if [[ $SOUL_UP -eq 1 ]]; then
|
||||
echo " Soul is up (responded in ${ELAPSED}s)."
|
||||
else
|
||||
echo " WARNING: Soul did not respond within ${SOUL_STARTUP_TIMEOUT}s."
|
||||
echo " The service may still be starting. Check: launchctl list | grep soul"
|
||||
fi
|
||||
|
||||
# ── Final report ───────────────────────────────────────────────────────────────
|
||||
echo ""
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo "Import complete."
|
||||
echo " Nodes imported: ${ACTUAL_COUNT}"
|
||||
echo " Exported at: ${EXPORTED_AT}"
|
||||
echo " Source host: ${SOURCE_HOST}"
|
||||
echo " Backup: ${BACKUP_PATH}"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
Reference in New Issue
Block a user