Initial commit: RA-H Open Source Edition

Local-first knowledge management system with BYO API keys.

Features:
- 3-panel UI (Nodes | Focus | Helpers)
- SQLite + sqlite-vec for vector search
- Agent system (Easy/Hard mode orchestrators)
- Content extraction (YouTube, PDF, web)
- Integrate workflow for connection discovery
- Dimension system with auto-assignment

Tech stack:
- Next.js 15 + TypeScript + Tailwind CSS
- Anthropic (Claude) + OpenAI (GPT) via Vercel AI SDK

Setup:
  npm install && npm rebuild better-sqlite3
  scripts/dev/bootstrap-local.sh
  npm run dev

MIT License
This commit is contained in:
“BeeRad”
2025-12-15 16:14:28 +11:00
commit 733d1c3407
226 changed files with 46231 additions and 0 deletions
+20
View File
@@ -0,0 +1,20 @@
#!/usr/bin/env bash
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
OUTPUT_PATH="${1:-$REPO_ROOT/dist/resources/rah_seed.sqlite}"
SQLITE_SCRIPT="$REPO_ROOT/scripts/database/sqlite-ensure-app-schema.sh"
if [ ! -x "$SQLITE_SCRIPT" ]; then
echo "Error: $SQLITE_SCRIPT not found or not executable" >&2
exit 1
fi
mkdir -p "$(dirname "$OUTPUT_PATH")"
rm -f "$OUTPUT_PATH" "$OUTPUT_PATH-wal" "$OUTPUT_PATH-shm"
sqlite3 "$OUTPUT_PATH" "VACUUM;" >/dev/null 2>&1 || true
bash "$SQLITE_SCRIPT" "$OUTPUT_PATH"
echo "✅ Seed database ready at $OUTPUT_PATH"
+58
View File
@@ -0,0 +1,58 @@
#!/usr/bin/env bash
set -euo pipefail
# RA-H SQLite backup using VACUUM INTO
# Safer behavior: auto-read .env.local for SQLITE_DB_PATH, verify integrity, print table counts.
# Resolve DB path in this order:
# 1) $SQLITE_DB_PATH (env)
# 2) ./.env.local → SQLITE_DB_PATH
# 3) Default dev path under ~/Library
DB_PATH=${SQLITE_DB_PATH:-}
if [ -z "${DB_PATH}" ] && [ -f ".env.local" ]; then
# Extract only the SQLITE_DB_PATH line (supports values with spaces)
DB_PATH=$(grep -E '^SQLITE_DB_PATH=' .env.local | sed 's/^SQLITE_DB_PATH=//' | sed 's/^"\(.*\)"$/\1/') || true
fi
if [ -z "${DB_PATH}" ]; then
DB_PATH="$HOME/Library/Application Support/RA-H/db/rah.sqlite"
fi
# Normalize and validate
if [ ! -f "$DB_PATH" ]; then
echo "❌ Error: Resolved DB not found: $DB_PATH" >&2
echo "Hint: Set SQLITE_DB_PATH in .env.local or export it inline: SQLITE_DB_PATH=\"/full/path/rah.sqlite\" npm run sqlite:backup" >&2
exit 1
fi
echo "Resolved DB path: $DB_PATH"
BACKUP_DIR="$(dirname "$0")/../backups"
mkdir -p "$BACKUP_DIR"
TS=$(date +"%Y%m%d_%H%M%S")
BASENAME="rah_backup_${TS}.sqlite"
DEST="$BACKUP_DIR/$BASENAME"
echo "Backing up → $DEST"
sqlite3 "$DB_PATH" <<SQL
PRAGMA optimize;
VACUUM INTO '$DEST';
SQL
echo "Verifying snapshot integrity..."
ICHECK=$(sqlite3 "$DEST" "PRAGMA integrity_check;")
echo " $ICHECK"
if [ "$ICHECK" != "ok" ]; then
echo "❌ Snapshot integrity check failed" >&2
exit 2
fi
echo "Snapshot counts:"
sqlite3 "$DEST" "SELECT 'nodes',COUNT(*) FROM nodes UNION ALL SELECT 'edges',COUNT(*) FROM edges UNION ALL SELECT 'chunks',COUNT(*) FROM chunks;" | sed 's/^/ /'
SIZE=$(du -h "$DEST" | awk '{print $1}')
echo "✅ Backup complete: $BASENAME ($SIZE)"
echo "Recent backups:"
ls -lht "$BACKUP_DIR"/*.sqlite 2>/dev/null | head -5 || echo " (none)"
+579
View File
@@ -0,0 +1,579 @@
#!/usr/bin/env bash
set -euo pipefail
DB_PATH=${1:-rah_trial.db}
if [ ! -f "$DB_PATH" ]; then
echo "Error: Database file not found: $DB_PATH" >&2
exit 1
fi
if command -v brew >/dev/null 2>&1; then
SQLITE_BIN="$(brew --prefix sqlite 2>/dev/null)/bin/sqlite3"
[ -x "$SQLITE_BIN" ] || SQLITE_BIN="sqlite3"
else
SQLITE_BIN="sqlite3"
fi
echo "Using sqlite: $($SQLITE_BIN --version)"
has_col() {
local table=$1 col=$2
"$SQLITE_BIN" "$DB_PATH" -json \
"PRAGMA table_info($table);" | \
grep -q "\"name\":\s*\"$col\""
}
has_table() {
local table=$1
"$SQLITE_BIN" "$DB_PATH" -json \
"SELECT name FROM sqlite_master WHERE type='table' AND name='$table';" | \
grep -q "$table"
}
has_view() {
local view=$1
"$SQLITE_BIN" "$DB_PATH" -json \
"SELECT name FROM sqlite_master WHERE type='view' AND name='$view';" | \
grep -q "$view"
}
has_trigger() {
local trg=$1
"$SQLITE_BIN" "$DB_PATH" -json \
"SELECT name FROM sqlite_master WHERE type='trigger' AND name='$trg';" | \
grep -q "$trg"
}
echo "Ensuring agents table exists and orchestrator is seeded..."
# Rename legacy helpers table if present
if has_table helpers && ! has_table agents; then
"$SQLITE_BIN" "$DB_PATH" "ALTER TABLE helpers RENAME TO agents;"
fi
if ! has_table agents; then
"$SQLITE_BIN" "$DB_PATH" <<'SQL'
CREATE TABLE agents (
id INTEGER PRIMARY KEY AUTOINCREMENT,
key TEXT UNIQUE NOT NULL,
display_name TEXT NOT NULL,
role TEXT NOT NULL DEFAULT 'executor',
system_prompt TEXT NOT NULL,
available_tools TEXT NOT NULL,
model TEXT NOT NULL,
description TEXT,
enabled INTEGER DEFAULT 1,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
memory TEXT,
prompts TEXT
);
SQL
fi
if has_table agents && ! has_col agents role; then
"$SQLITE_BIN" "$DB_PATH" "ALTER TABLE agents ADD COLUMN role TEXT NOT NULL DEFAULT 'executor';"
fi
if has_table agents && ! has_col agents prompts; then
"$SQLITE_BIN" "$DB_PATH" "ALTER TABLE agents ADD COLUMN prompts TEXT DEFAULT '[]';"
fi
if has_table agents && ! has_col agents memory; then
"$SQLITE_BIN" "$DB_PATH" "ALTER TABLE agents ADD COLUMN memory TEXT;"
fi
COUNT_AGENTS=$("$SQLITE_BIN" -readonly "$DB_PATH" "SELECT COUNT(*) FROM agents;" 2>/dev/null || echo 0)
if [ "${COUNT_AGENTS:-0}" = "0" ]; then
echo " Seeding default orchestrator agent (ra-h)..."
NOW=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
TOOLS_JSON='["queryNodes","createNode","updateNode","createEdge","queryEdge","updateEdge","searchContentEmbeddings","webSearch","think","delegateToMiniRAH"]'
PROMPTS_JSON='[{"id":"p_seed_0","name":"Summary of Focus","content":"Summarize the primary focused node clearly. Include 35 key points and cite [NODE:id:\"title\"]."},{"id":"p_seed_1","name":"Next Steps","content":"Propose 3 concrete next actions based on the focused nodes with references to [NODE:id:\"title\"]."}]'
SYSTEM_PROMPT="You are ra-h, the main orchestrator for RA-H. Coordinate work, delegate to mini ra-hs when tasks can be isolated, and keep the conversation focused on the user's goals."
ESCAPED_SYSTEM_PROMPT=${SYSTEM_PROMPT//\'/''}
"$SQLITE_BIN" "$DB_PATH" <<SQL
INSERT INTO agents(key, display_name, role, system_prompt, available_tools, model, description, enabled, created_at, updated_at, prompts)
VALUES (
'ra-h',
'ra-h',
'orchestrator',
'$ESCAPED_SYSTEM_PROMPT',
'$TOOLS_JSON',
'anthropic/claude-sonnet-4.5',
'Opinionated orchestrator agent',
1,
'$NOW',
'$NOW',
'$PROMPTS_JSON'
);
SQL
fi
echo "Ensuring core tables exist (nodes, chunks, edges, chats, node_dimensions)..."
if ! has_table nodes; then
"$SQLITE_BIN" "$DB_PATH" <<'SQL'
CREATE TABLE nodes (
id INTEGER PRIMARY KEY,
title TEXT,
description TEXT,
content TEXT,
link TEXT,
type TEXT,
created_at TEXT,
updated_at TEXT,
metadata TEXT,
chunk TEXT,
embedding BLOB,
embedding_updated_at TEXT,
embedding_text TEXT,
chunk_status TEXT DEFAULT 'not_chunked'
);
SQL
fi
if ! has_table chunks; then
"$SQLITE_BIN" "$DB_PATH" <<'SQL'
CREATE TABLE chunks (
id INTEGER PRIMARY KEY,
node_id INTEGER NOT NULL,
chunk_idx INTEGER,
text TEXT,
created_at TEXT,
embedding_type TEXT DEFAULT 'text-embedding-3-small',
metadata TEXT,
FOREIGN KEY (node_id) REFERENCES nodes(id) ON DELETE CASCADE
);
CREATE INDEX idx_chunks_by_node ON chunks(node_id);
CREATE INDEX idx_chunks_by_node_idx ON chunks(node_id, chunk_idx);
SQL
fi
if ! has_table edges; then
"$SQLITE_BIN" "$DB_PATH" <<'SQL'
CREATE TABLE edges (
id INTEGER PRIMARY KEY,
from_node_id INTEGER NOT NULL,
to_node_id INTEGER NOT NULL,
source TEXT,
created_at TEXT,
context TEXT,
user_feedback INTEGER,
FOREIGN KEY (from_node_id) REFERENCES nodes(id) ON DELETE CASCADE,
FOREIGN KEY (to_node_id) REFERENCES nodes(id) ON DELETE CASCADE
);
CREATE INDEX idx_edges_from ON edges(from_node_id);
CREATE INDEX idx_edges_to ON edges(to_node_id);
SQL
fi
echo "Dropping legacy episodic/semantic memory tables if they exist..."
if has_table chats; then
if "$SQLITE_BIN" "$DB_PATH" -json "PRAGMA table_info(chats);" | grep -q 'focused_memory_id'; then
echo " Removing legacy chats.focused_memory_id column"
"$SQLITE_BIN" "$DB_PATH" <<'SQL'
PRAGMA foreign_keys=OFF;
BEGIN TRANSACTION;
ALTER TABLE chats RENAME TO chats_legacy_cleanup;
CREATE TABLE chats (
id INTEGER PRIMARY KEY,
chat_type TEXT,
helper_name TEXT,
agent_type TEXT DEFAULT 'orchestrator',
delegation_id INTEGER,
user_message TEXT,
assistant_message TEXT,
thread_id TEXT,
focused_node_id INTEGER,
created_at TEXT DEFAULT (CURRENT_TIMESTAMP),
metadata TEXT,
FOREIGN KEY (focused_node_id) REFERENCES nodes(id) ON DELETE SET NULL
);
INSERT INTO chats (
id, chat_type, helper_name, agent_type, delegation_id,
user_message, assistant_message, thread_id, focused_node_id,
created_at, metadata
)
SELECT id, chat_type, helper_name, agent_type, delegation_id,
user_message, assistant_message, thread_id, focused_node_id,
created_at, metadata
FROM chats_legacy_cleanup;
DROP TABLE chats_legacy_cleanup;
CREATE INDEX IF NOT EXISTS idx_chats_thread ON chats(thread_id);
COMMIT;
PRAGMA foreign_keys=ON;
SQL
fi
fi
"$SQLITE_BIN" "$DB_PATH" <<'SQL'
DROP TRIGGER IF EXISTS trg_episodic_prune;
DROP TABLE IF EXISTS episodic_memory;
DROP TABLE IF EXISTS episodic_pipeline_state;
DROP TABLE IF EXISTS semantic_memory;
DROP TABLE IF EXISTS semantic_pipeline_state;
DROP TABLE IF EXISTS memory_pipeline_state;
DROP TABLE IF EXISTS memory;
SQL
echo "Removing deprecated context_versions table (if present)..."
"$SQLITE_BIN" "$DB_PATH" <<'SQL'
DROP TABLE IF EXISTS context_versions;
DROP INDEX IF EXISTS idx_context_created;
SQL
if ! has_table chats; then
"$SQLITE_BIN" "$DB_PATH" <<'SQL'
CREATE TABLE chats (
id INTEGER PRIMARY KEY,
chat_type TEXT,
helper_name TEXT,
agent_type TEXT DEFAULT 'orchestrator',
delegation_id INTEGER,
user_message TEXT,
assistant_message TEXT,
thread_id TEXT,
focused_node_id INTEGER,
created_at TEXT DEFAULT (CURRENT_TIMESTAMP),
metadata TEXT,
FOREIGN KEY (focused_node_id) REFERENCES nodes(id) ON DELETE SET NULL
);
CREATE INDEX idx_chats_thread ON chats(thread_id);
SQL
fi
if has_table chats && ! has_col chats agent_type; then
"$SQLITE_BIN" "$DB_PATH" "ALTER TABLE chats ADD COLUMN agent_type TEXT DEFAULT 'orchestrator';"
fi
if has_table chats && ! has_col chats delegation_id; then
"$SQLITE_BIN" "$DB_PATH" "ALTER TABLE chats ADD COLUMN delegation_id INTEGER;"
fi
if ! has_table chat_memory_state; then
"$SQLITE_BIN" "$DB_PATH" <<'SQL'
CREATE TABLE chat_memory_state (
thread_id TEXT PRIMARY KEY,
helper_name TEXT,
last_processed_chat_id INTEGER DEFAULT 0,
last_processed_at TEXT
);
CREATE INDEX IF NOT EXISTS idx_chat_memory_thread ON chat_memory_state(thread_id);
SQL
fi
echo "Ensuring agent_delegations table exists..."
"$SQLITE_BIN" "$DB_PATH" <<'SQL'
CREATE TABLE IF NOT EXISTS agent_delegations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT UNIQUE NOT NULL,
task TEXT NOT NULL,
context TEXT,
expected_outcome TEXT,
status TEXT NOT NULL DEFAULT 'queued',
summary TEXT,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
);
SQL
if ! has_table node_dimensions; then
"$SQLITE_BIN" "$DB_PATH" <<'SQL'
CREATE TABLE node_dimensions (
node_id INTEGER NOT NULL,
dimension TEXT NOT NULL,
PRIMARY KEY (node_id, dimension),
FOREIGN KEY (node_id) REFERENCES nodes(id) ON DELETE CASCADE
) WITHOUT ROWID;
CREATE INDEX idx_dim_by_dimension ON node_dimensions(dimension, node_id);
CREATE INDEX idx_dim_by_node ON node_dimensions(node_id, dimension);
SQL
fi
echo "Checking/adding missing columns..."
if has_table nodes; then
if ! has_col nodes type; then
echo "Adding nodes.type"
"$SQLITE_BIN" "$DB_PATH" "ALTER TABLE nodes ADD COLUMN type TEXT;"
fi
if ! has_col nodes description; then
echo "Adding nodes.description"
"$SQLITE_BIN" "$DB_PATH" "ALTER TABLE nodes ADD COLUMN description TEXT;"
fi
if ! has_col nodes metadata; then
echo "Adding nodes.metadata"
"$SQLITE_BIN" "$DB_PATH" "ALTER TABLE nodes ADD COLUMN metadata TEXT;"
fi
if ! has_col nodes chunk; then
echo "Adding nodes.chunk"
"$SQLITE_BIN" "$DB_PATH" "ALTER TABLE nodes ADD COLUMN chunk TEXT;"
fi
if ! has_col nodes is_pinned; then
echo "Adding nodes.is_pinned"
"$SQLITE_BIN" "$DB_PATH" "ALTER TABLE nodes ADD COLUMN is_pinned INTEGER DEFAULT 0;"
fi
fi
if has_table chunks; then
if ! has_col chunks embedding_type; then
echo "Adding chunks.embedding_type"
"$SQLITE_BIN" "$DB_PATH" "ALTER TABLE chunks ADD COLUMN embedding_type TEXT DEFAULT 'text-embedding-3-small';"
fi
if ! has_col chunks metadata; then
echo "Adding chunks.metadata"
"$SQLITE_BIN" "$DB_PATH" "ALTER TABLE chunks ADD COLUMN metadata TEXT;"
fi
fi
echo "Ensuring dimensions table exists..."
if ! has_table dimensions; then
"$SQLITE_BIN" "$DB_PATH" <<'SQL'
CREATE TABLE dimensions (
name TEXT PRIMARY KEY,
is_priority INTEGER DEFAULT 0,
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
);
SQL
fi
if has_table dimensions; then
echo "Seeding default locked dimensions..."
for dimension in research ideas projects memory preferences; do
"$SQLITE_BIN" "$DB_PATH" <<SQL
INSERT INTO dimensions (name, is_priority, updated_at)
VALUES ('$dimension', 1, datetime('now'))
ON CONFLICT(name) DO UPDATE SET is_priority = 1, updated_at = datetime('now');
SQL
done
fi
echo "Refreshing helper view nodes_v..."
"$SQLITE_BIN" "$DB_PATH" "DROP VIEW IF EXISTS nodes_v;"
"$SQLITE_BIN" "$DB_PATH" <<'SQL'
CREATE VIEW nodes_v AS
SELECT n.id,
n.title,
n.description,
n.content,
n.link,
n.type,
n.metadata,
n.created_at,
n.updated_at,
COALESCE((SELECT JSON_GROUP_ARRAY(d.dimension)
FROM node_dimensions d
WHERE d.node_id = n.id), '[]') AS dimensions_json
FROM nodes n;
SQL
echo "Ensuring logs table and triggers exist (migrating from memory if needed)..."
# migrate memory -> logs if needed
if ! has_table logs && has_table memory; then
echo "Dropping view memory_v (if exists) to unlock table rename..."
"$SQLITE_BIN" "$DB_PATH" "DROP VIEW IF EXISTS memory_v;"
echo "Renaming memory -> logs..."
"$SQLITE_BIN" "$DB_PATH" "ALTER TABLE memory RENAME TO logs;"
fi
# logs table
if ! has_table logs; then
"$SQLITE_BIN" "$DB_PATH" <<'SQL'
CREATE TABLE logs (
id INTEGER PRIMARY KEY,
ts TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP),
table_name TEXT NOT NULL,
action TEXT NOT NULL,
row_id INTEGER NOT NULL,
summary TEXT,
enriched_summary TEXT,
snapshot_json TEXT
);
SQL
fi
# Add enriched_summary column if missing
if has_table logs && ! has_col logs enriched_summary; then
"$SQLITE_BIN" "$DB_PATH" "ALTER TABLE logs ADD COLUMN enriched_summary TEXT;"
fi
# indexes on logs (cleanup legacy names first)
"$SQLITE_BIN" "$DB_PATH" <<'SQL'
DROP INDEX IF EXISTS idx_memory_ts;
DROP INDEX IF EXISTS idx_memory_table_ts;
DROP INDEX IF EXISTS idx_memory_table_row;
CREATE INDEX IF NOT EXISTS idx_logs_ts ON logs(ts);
CREATE INDEX IF NOT EXISTS idx_logs_table_ts ON logs(table_name, ts);
CREATE INDEX IF NOT EXISTS idx_logs_table_row ON logs(table_name, row_id);
SQL
# triggers for nodes (drop/recreate to ensure enriched payloads)
if has_table nodes; then
"$SQLITE_BIN" "$DB_PATH" <<'SQL'
DROP TRIGGER IF EXISTS trg_nodes_ai;
DROP TRIGGER IF EXISTS trg_nodes_au;
CREATE TRIGGER trg_nodes_ai AFTER INSERT ON nodes BEGIN
INSERT INTO logs(table_name, action, row_id, summary, snapshot_json)
VALUES('nodes', 'insert', NEW.id,
printf('node created: %s', COALESCE(NEW.title,'')),
json_object(
'id', NEW.id,
'title', NEW.title,
'link', NEW.link
)
);
END;
CREATE TRIGGER trg_nodes_au AFTER UPDATE ON nodes BEGIN
INSERT INTO logs(table_name, action, row_id, summary, snapshot_json)
VALUES('nodes', 'update', NEW.id,
printf('node updated: %s', COALESCE(NEW.title,'')),
json_object(
'id', NEW.id,
'title', NEW.title,
'link', NEW.link
)
);
END;
SQL
fi
# triggers for edges (enriched with node titles, truncated)
if has_table edges; then
"$SQLITE_BIN" "$DB_PATH" <<'SQL'
DROP TRIGGER IF EXISTS trg_edges_ai;
DROP TRIGGER IF EXISTS trg_edges_au;
CREATE TRIGGER trg_edges_ai AFTER INSERT ON edges BEGIN
INSERT INTO logs(table_name, action, row_id, summary, snapshot_json)
VALUES('edges', 'insert', NEW.id,
printf('edge %d→%d (%s)', NEW.from_node_id, NEW.to_node_id, COALESCE(NEW.source,'')),
json_object(
'id', NEW.id,
'from', NEW.from_node_id,
'to', NEW.to_node_id,
'source', NEW.source,
'from_title', substr((SELECT title FROM nodes WHERE id = NEW.from_node_id), 1, 120),
'to_title', substr((SELECT title FROM nodes WHERE id = NEW.to_node_id), 1, 120)
)
);
END;
CREATE TRIGGER trg_edges_au AFTER UPDATE ON edges BEGIN
INSERT INTO logs(table_name, action, row_id, summary, snapshot_json)
VALUES('edges', 'update', NEW.id,
printf('edge updated %d→%d', NEW.from_node_id, NEW.to_node_id),
json_object(
'id', NEW.id,
'from', NEW.from_node_id,
'to', NEW.to_node_id,
'source', NEW.source,
'from_title', substr((SELECT title FROM nodes WHERE id = NEW.from_node_id), 1, 120),
'to_title', substr((SELECT title FROM nodes WHERE id = NEW.to_node_id), 1, 120)
)
);
END;
-- Add trigger to auto-update node updated_at timestamps when edges are created
DROP TRIGGER IF EXISTS trg_edges_update_nodes_on_insert;
CREATE TRIGGER trg_edges_update_nodes_on_insert
AFTER INSERT ON edges
BEGIN
UPDATE nodes SET updated_at = strftime('%Y-%m-%dT%H:%M:%f', 'now') || 'Z' WHERE id = NEW.from_node_id;
UPDATE nodes SET updated_at = strftime('%Y-%m-%dT%H:%M:%f', 'now') || 'Z' WHERE id = NEW.to_node_id;
END;
SQL
fi
# trigger for chats (enriched with content previews, truncated)
if has_table chats; then
"$SQLITE_BIN" "$DB_PATH" <<'SQL'
DROP TRIGGER IF EXISTS trg_chats_ai;
CREATE TRIGGER trg_chats_ai AFTER INSERT ON chats BEGIN
INSERT INTO logs(table_name, action, row_id, summary, snapshot_json)
VALUES('chats', 'insert', NEW.id,
printf('chat: %s (%s)', COALESCE(NEW.helper_name,''), COALESCE(NEW.thread_id,'')),
json_object(
'id', NEW.id,
'helper', NEW.helper_name,
'thread', NEW.thread_id,
'user_preview', substr(NEW.user_message, 1, 120),
'assistant_preview', substr(NEW.assistant_message, 1, 120)
)
);
END;
SQL
fi
# Add trigger to auto-prune logs to keep only most recent 10k entries
if has_table logs; then
"$SQLITE_BIN" "$DB_PATH" <<'SQL'
DROP TRIGGER IF EXISTS trg_logs_prune;
CREATE TRIGGER trg_logs_prune AFTER INSERT ON logs BEGIN
DELETE FROM logs WHERE id < NEW.id - 10000;
END;
SQL
fi
echo "Ensuring logs_v view exists (removing legacy memory_v)..."
"$SQLITE_BIN" "$DB_PATH" <<'SQL'
DROP VIEW IF EXISTS logs_v;
DROP VIEW IF EXISTS memory_v;
CREATE VIEW logs_v AS
SELECT
m.id,
m.ts,
m.table_name,
m.action,
m.row_id,
m.summary,
m.enriched_summary,
m.snapshot_json,
CASE WHEN m.table_name='nodes' THEN n.title END AS node_title,
CASE WHEN m.table_name='edges' THEN nf.title END AS edge_from_title,
CASE WHEN m.table_name='edges' THEN nt.title END AS edge_to_title,
CASE WHEN m.table_name='chats' THEN c.helper_name END AS chat_helper,
CASE WHEN m.table_name='chats' THEN substr(c.user_message,1,120) END AS chat_user_preview,
CASE WHEN m.table_name='chats' THEN substr(c.assistant_message,1,120) END AS chat_assistant_preview,
CASE WHEN m.table_name='chats' THEN c.user_message END AS chat_user_full,
CASE WHEN m.table_name='chats' THEN c.assistant_message END AS chat_assistant_full
FROM logs m
LEFT JOIN nodes n ON (m.table_name='nodes' AND m.row_id = n.id)
LEFT JOIN edges e ON (m.table_name='edges' AND m.row_id = e.id)
LEFT JOIN nodes nf ON e.from_node_id = nf.id
LEFT JOIN nodes nt ON e.to_node_id = nt.id
LEFT JOIN chats c ON (m.table_name='chats' AND m.row_id = c.id);
SQL
echo "Ensuring helpers.memory exists and is populated from fluid_context (if present)..."
if has_table helpers; then
if ! has_col helpers memory; then
"$SQLITE_BIN" "$DB_PATH" "ALTER TABLE helpers ADD COLUMN memory TEXT;"
if has_col helpers fluid_context; then
"$SQLITE_BIN" "$DB_PATH" "UPDATE helpers SET memory = fluid_context WHERE fluid_context IS NOT NULL;"
fi
fi
else
echo " helpers table not present; skipping helpers.memory migration"
fi
echo "Updating helper available_tools to use updateHelperMemory (renaming from updateHelperFluidContext)..."
if has_table helpers; then
"$SQLITE_BIN" "$DB_PATH" <<'SQL'
UPDATE helpers
SET available_tools = REPLACE(available_tools, 'updateHelperFluidContext', 'updateHelperMemory')
WHERE available_tools LIKE '%updateHelperFluidContext%';
SQL
else
echo " helpers table not present; skipping available_tools rename"
fi
echo "Dropping helpers.fluid_context column if present (post-migration cleanup)..."
if has_table helpers && has_col helpers fluid_context; then
"$SQLITE_BIN" "$DB_PATH" "ALTER TABLE helpers DROP COLUMN fluid_context;" || true
fi
echo "Running VACUUM and ANALYZE..."
"$SQLITE_BIN" "$DB_PATH" "VACUUM; ANALYZE;"
echo "Done. Schema is compatible."
+62
View File
@@ -0,0 +1,62 @@
#!/usr/bin/env bash
set -euo pipefail
# Restore RA-H SQLite database from a backup snapshot
if [ $# -lt 1 ]; then
echo "Usage: $0 <backup.sqlite>" >&2
exit 1
fi
SRC="$1"
if [ ! -f "$SRC" ]; then
echo "Backup file not found: $SRC" >&2
exit 1
fi
# Resolve target DB path in this order:
# 1) $SQLITE_DB_PATH (env)
# 2) ./.env.local → SQLITE_DB_PATH
# 3) Default dev path under ~/Library
DB_PATH=${SQLITE_DB_PATH:-}
if [ -z "${DB_PATH}" ] && [ -f ".env.local" ]; then
DB_PATH=$(grep -E '^SQLITE_DB_PATH=' .env.local | sed 's/^SQLITE_DB_PATH=//' | sed 's/^"\(.*\)"$/\1/') || true
fi
if [ -z "${DB_PATH}" ]; then
DB_PATH="$HOME/Library/Application Support/RA-H/db/rah.sqlite"
fi
DB_DIR="$(dirname "$DB_PATH")"
mkdir -p "$DB_DIR"
echo "Source backup: $SRC"
echo "Target DB: $DB_PATH"
echo "Verifying backup integrity before restore..."
ICHECK=$(sqlite3 "$SRC" "PRAGMA integrity_check;")
echo " $ICHECK"
if [ "$ICHECK" != "ok" ]; then
echo "❌ Backup integrity check failed — aborting restore" >&2
exit 2
fi
TS=$(date +"%Y%m%d_%H%M%S")
SAFE_COPY="$DB_DIR/rah_pre_restore_${TS}.sqlite"
echo "Creating safety copy: $SAFE_COPY"
cp -f "$DB_PATH" "$SAFE_COPY" 2>/dev/null || true
echo "Restoring backup..."
cp -f "$SRC" "$DB_PATH"
echo "Integrity check on restored DB..."
RCHECK=$(sqlite3 "$DB_PATH" "PRAGMA integrity_check;")
echo " $RCHECK"
if [ "$RCHECK" != "ok" ]; then
echo "❌ Restored DB integrity check failed" >&2
exit 3
fi
echo "Restored counts:"
sqlite3 "$DB_PATH" "SELECT 'nodes',COUNT(*) FROM nodes UNION ALL SELECT 'edges',COUNT(*) FROM edges UNION ALL SELECT 'chunks',COUNT(*) FROM chunks;" | sed 's/^/ /'
echo "✅ Restore complete."
+24
View File
@@ -0,0 +1,24 @@
#!/usr/bin/env bash
set -euo pipefail
DB_PATH=${1:-rah_trial.db}
if [ ! -f "$DB_PATH" ]; then
echo "Error: Database file not found: $DB_PATH" >&2
exit 1
fi
if command -v brew >/dev/null 2>&1; then
SQLITE_BIN="$(brew --prefix sqlite 2>/dev/null)/bin/sqlite3"
if [ ! -x "$SQLITE_BIN" ]; then
SQLITE_BIN="sqlite3"
fi
else
SQLITE_BIN="sqlite3"
fi
echo "Using sqlite3 at: $SQLITE_BIN"
"$SQLITE_BIN" --version
"$SQLITE_BIN" "$DB_PATH" < "$(dirname "$0")/sqlite-verify.sql"