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
+68
View File
@@ -0,0 +1,68 @@
# RA-H Scripts
Utility scripts for managing the RA-H knowledge management system.
## Directory Structure
```
scripts/
├── database/ # Database management
│ ├── backup.sh # Create database backups
│ └── restore.sh # Restore from backups
├── helpers/ # Helper management (CRITICAL - used by API)
│ ├── duplicate-helper.js # Create new helpers
│ └── delete-helper.js # Remove helpers
└── migrations/ # Completed one-time migrations
└── cleanup-dimensions.sql
```
## Database Scripts
### Create a Backup
```bash
npm run backup
```
- Creates timestamped backup in `/backups/` folder
- Example: `rah_backup_20250902_102846.sql`
- Includes all nodes, chunks, edges, dimensions
### Restore from Backup
```bash
npm run restore backups/rah_backup_20250902_102846.sql
```
- ⚠️ **WARNING**: Replaces entire database
- Requires confirmation before proceeding
- Shows verification after restore
### List Backups
```bash
ls -lt backups/
```
## Helper Scripts
⚠️ **CRITICAL**: These scripts are used by the API at runtime. DO NOT modify or delete.
### Create New Helper
Called automatically by the API when creating helpers through the UI.
```bash
node scripts/helpers/duplicate-helper.js "HelperName"
```
### Delete Helper
Called automatically by the API when deleting helpers through the UI.
```bash
node scripts/helpers/delete-helper.js "helper-id"
```
## What Gets Backed Up
- All nodes (42,000+ knowledge items)
- Content chunks and embeddings
- Node connections/edges
- Dimensions and metadata
- Database schema and indexes
## Notes
- Backups typically ~250MB for 40k+ nodes
- Store backups safely - they contain your entire knowledge base
- Helper scripts are integrated with the application - modify with caution
+286
View File
@@ -0,0 +1,286 @@
#!/usr/bin/env bash
set -euo pipefail
# Build universal native dependencies (better-sqlite3 + sqlite-vec) for the packaged bundle.
# Relies on the universal Node runtime prepared in dist/runtime/node-universal.
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
export PATH="$REPO_ROOT/scripts:$PATH"
DIST_ROOT="${DIST_ROOT:-$REPO_ROOT/dist/local-app}"
APP_DIR="${APP_DIR:-$DIST_ROOT/app}"
NODE_RUNTIME_DIR="${NODE_RUNTIME_DIR:-$REPO_ROOT/dist/runtime/node-universal}"
NODE_BIN="$NODE_RUNTIME_DIR/bin/node"
NPM_CLI="$NODE_RUNTIME_DIR/lib/node_modules/npm/bin/npm-cli.js"
SQLITE_VEC_REPO="${SQLITE_VEC_REPO:-https://github.com/asg017/sqlite-vec.git}"
SQLITE_VEC_REF="${SQLITE_VEC_REF:-main}"
SQLITE_VEC_SOURCE_DIR="${SQLITE_VEC_SOURCE_DIR:-$REPO_ROOT/dist/.deps/sqlite-vec-src}"
SQLITE_VEC_OUTPUT="${SQLITE_VEC_OUTPUT:-$DIST_ROOT/vendor/sqlite-extensions/vec0.dylib}"
SQLITE_VEC_VENDOR_COPY="${SQLITE_VEC_VENDOR_COPY:-$REPO_ROOT/vendor/sqlite-extensions/vec0.dylib}"
TMP_DIR="$(mktemp -d -t rah-native-XXXX)"
cleanup() {
rm -rf "$TMP_DIR"
}
trap cleanup EXIT
ensure_prerequisites() {
if [ ! -x "$NODE_BIN" ]; then
echo "❌ Universal Node runtime missing at $NODE_BIN" >&2
echo " Run scripts/build-universal-node.sh first." >&2
exit 1
fi
if [ ! -d "$APP_DIR" ]; then
echo "❌ Next.js standalone output missing at $APP_DIR" >&2
echo " Ensure scripts/build-production.sh has copied the app bundle." >&2
exit 1
fi
if [ ! -d "$APP_DIR/node_modules/better-sqlite3" ]; then
echo "❌ better-sqlite3 node module missing from $APP_DIR/node_modules." >&2
echo " Did you run npm run build before invoking this script?" >&2
exit 1
fi
if ! command -v arch >/dev/null 2>&1; then
echo "❌ arch command not found. Install Xcode command line tools." >&2
exit 1
fi
if arch -x86_64 /usr/bin/true >/dev/null 2>&1; then
:
else
echo "❌ Rosetta is required to build x86_64 binaries. Install with 'softwareupdate --install-rosetta'." >&2
exit 1
fi
if ! command -v lipo >/dev/null 2>&1; then
echo "❌ lipo not found. Install Xcode command line tools (xcode-select --install)." >&2
exit 1
fi
if ! command -v git >/dev/null 2>&1; then
echo "❌ git not found. Install git to fetch sqlite-vec source." >&2
exit 1
fi
}
ensure_sqlite_vec_source() {
if [ -d "$SQLITE_VEC_SOURCE_DIR/.git" ]; then
return
fi
echo "⬇️ Cloning sqlite-vec sources into $SQLITE_VEC_SOURCE_DIR"
mkdir -p "$(dirname "$SQLITE_VEC_SOURCE_DIR")"
git clone --depth 1 --branch "$SQLITE_VEC_REF" "$SQLITE_VEC_REPO" "$SQLITE_VEC_SOURCE_DIR"
}
refresh_better_sqlite3_sources() {
local root_module="$REPO_ROOT/node_modules/better-sqlite3"
local target_module="$APP_DIR/node_modules/better-sqlite3"
if [ ! -d "$root_module" ]; then
echo "❌ Root better-sqlite3 module missing at $root_module" >&2
echo " Run npm install in the repo root before packaging." >&2
exit 1
fi
rm -rf "$target_module"
mkdir -p "$target_module"
rsync -a "$root_module/" "$target_module/"
}
arch_prefix() {
local arch="$1"
if [ "$arch" = "x86_64" ]; then
echo "arch -x86_64"
else
echo "arch -arm64"
fi
}
codesign_if_available() {
local target="$1"
if ! command -v codesign >/dev/null 2>&1; then
return
fi
local identity="${CODESIGN_IDENTITY:--}"
local extra_opts=()
if [ "${CODESIGN_HARDENED_RUNTIME:-1}" = "1" ]; then
extra_opts=(--options runtime)
fi
local codesign_cmd=(codesign --force --timestamp --sign "$identity" "${extra_opts[@]}" "$target")
"${codesign_cmd[@]}" >/dev/null 2>&1 || \
codesign --force --timestamp --sign - "${extra_opts[@]}" "$target" >/dev/null 2>&1 || true
}
rebuild_better_sqlite3_for_arch() {
local arch="$1"
local output="$TMP_DIR/better_sqlite3.${arch}.node"
echo "⚙️ Rebuilding better-sqlite3 for ${arch}"
rm -rf "$APP_DIR/node_modules/better-sqlite3/build"
local arch_cmd
arch_cmd=($(arch_prefix "$arch"))
(
cd "$APP_DIR"
PATH="$NODE_RUNTIME_DIR/bin:$PATH" \
npm_config_build_from_source=true \
npm_config_fallback_to_build=false \
npm_config_arch="$arch" \
npm_config_target_arch="$arch" \
CFLAGS="-arch $arch" \
CXXFLAGS="-arch $arch" \
LDFLAGS="-arch $arch" \
"${arch_cmd[@]}" "$NODE_BIN" "$NPM_CLI" rebuild better-sqlite3 --build-from-source
)
if [ ! -f "$APP_DIR/node_modules/better-sqlite3/build/Release/better_sqlite3.node" ]; then
echo "❌ Failed to build better-sqlite3 for ${arch}" >&2
exit 1
fi
cp "$APP_DIR/node_modules/better-sqlite3/build/Release/better_sqlite3.node" "$output"
}
combine_better_sqlite3() {
local arm_file="$TMP_DIR/better_sqlite3.arm64.node"
local x64_file="$TMP_DIR/better_sqlite3.x86_64.node"
local target="$APP_DIR/node_modules/better-sqlite3/build/Release/better_sqlite3.node"
lipo -create "$arm_file" "$x64_file" -output "$target"
codesign_if_available "$target"
rm -f "$APP_DIR/node_modules/better-sqlite3/build/Release/test_extension.node"
echo "✓ better-sqlite3 universal binary ready at $target"
}
build_sqlite_vec_for_arch() {
local arch="$1"
local output="$TMP_DIR/vec0.${arch}.dylib"
echo "⚙️ Building sqlite-vec (vec0) for ${arch}"
local include_dir="${SQLITE3_INCLUDE_DIR:-}"
local lib_dir="${SQLITE3_LIB_DIR:-}"
if { [ -z "$include_dir" ] || [ -z "$lib_dir" ]; } && command -v brew >/dev/null 2>&1; then
local brew_prefix
brew_prefix="$(brew --prefix sqlite 2>/dev/null || true)"
if [ -n "$brew_prefix" ]; then
if [ -z "$include_dir" ] && [ -d "$brew_prefix/include" ]; then
include_dir="$brew_prefix/include"
fi
if [ -z "$lib_dir" ] && [ -d "$brew_prefix/lib" ]; then
lib_dir="$brew_prefix/lib"
fi
fi
fi
local cflags="-O3 -fPIC -arch $arch -undefined dynamic_lookup"
if [ -n "$include_dir" ]; then
cflags="$cflags -I${include_dir}"
fi
if [ -n "$lib_dir" ]; then
cflags="$cflags -L${lib_dir}"
fi
local arch_cmd
arch_cmd=($(arch_prefix "$arch"))
local -a env_vars=(
"CC=clang"
"CFLAGS=$cflags"
)
if [ -n "$include_dir" ] || [ -n "$lib_dir" ]; then
env_vars+=("USE_BREW_SQLITE=1")
if [ -n "$include_dir" ]; then
env_vars+=("SQLITE_INCLUDE_PATH=-I${include_dir}")
fi
if [ -n "$lib_dir" ]; then
env_vars+=("SQLITE_LIB_PATH=-L${lib_dir}")
fi
fi
(
cd "$SQLITE_VEC_SOURCE_DIR"
make clean >/dev/null 2>&1 || true
"${arch_cmd[@]}" env "${env_vars[@]}" make loadable
)
local built_path="$SQLITE_VEC_SOURCE_DIR/dist/vec0.dylib"
if [ ! -f "$built_path" ]; then
echo "❌ sqlite-vec build failed for ${arch}. Inspect make output above." >&2
exit 1
fi
mv "$built_path" "$output"
}
combine_sqlite_vec() {
mkdir -p "$(dirname "$SQLITE_VEC_OUTPUT")"
mkdir -p "$(dirname "$SQLITE_VEC_VENDOR_COPY")"
lipo -create \
"$TMP_DIR/vec0.arm64.dylib" \
"$TMP_DIR/vec0.x86_64.dylib" \
-output "$SQLITE_VEC_OUTPUT"
cp "$SQLITE_VEC_OUTPUT" "$SQLITE_VEC_VENDOR_COPY"
codesign_if_available "$SQLITE_VEC_OUTPUT"
codesign_if_available "$SQLITE_VEC_VENDOR_COPY"
echo "✓ sqlite-vec universal dylib ready at $SQLITE_VEC_OUTPUT"
}
codesign_additional_binaries() {
local -a extras=(
"$DIST_ROOT/bin/yt-dlp"
"$APP_DIR/node_modules/@img/sharp-darwin-arm64/lib/sharp-darwin-arm64.node"
"$APP_DIR/node_modules/@img/sharp-darwin-x64/lib/sharp-darwin-x64.node"
)
for target in "${extras[@]}"; do
if [ -e "$target" ]; then
codesign_if_available "$target"
fi
done
local glob
for glob in \
"$APP_DIR/node_modules/@img/sharp-libvips-darwin-arm64/lib/"* \
"$APP_DIR/node_modules/@img/sharp-libvips-darwin-x64/lib/"*; do
if [ -e "$glob" ]; then
codesign_if_available "$glob"
fi
done
}
main() {
ensure_prerequisites
ensure_sqlite_vec_source
refresh_better_sqlite3_sources
rebuild_better_sqlite3_for_arch arm64
rebuild_better_sqlite3_for_arch x86_64
combine_better_sqlite3
build_sqlite_vec_for_arch arm64
build_sqlite_vec_for_arch x86_64
combine_sqlite_vec
codesign_additional_binaries
}
main "$@"
+580
View File
@@ -0,0 +1,580 @@
#!/usr/bin/env bash
set -euo pipefail
# Orchestrate the production build assets required by the Tauri bundle.
# 1. Builds the Next.js standalone output.
# 2. Prepares the universal Node runtime.
# 3. Copies resources into dist/local-app for the sidecar.
# 4. Rebuilds native modules as universal binaries.
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
DIST_ROOT="$REPO_ROOT/dist/local-app"
RUNTIME_ROOT="$REPO_ROOT/dist/runtime/node-universal"
STAGING_ROOT="$REPO_ROOT/dist/.staging/production"
SEED_SOURCE="$REPO_ROOT/dist/resources/rah_seed.sqlite"
NEXT_BUILD_DIR="$REPO_ROOT/.next"
resolve_public_env_value() {
local var_name="$1"
local fallback="$2"
local value="${!var_name:-}"
if [ -z "$value" ] && [ -f "$REPO_ROOT/.env" ]; then
value="$(grep -E "^${var_name}=" "$REPO_ROOT/.env" | tail -n 1 | cut -d '=' -f2- || true)"
fi
if [ -z "$value" ]; then
value="$fallback"
fi
printf '%s' "$value"
}
ensure_prerequisites() {
if ! command -v npm >/dev/null 2>&1; then
echo "❌ npm not found. Install Node.js before running this script." >&2
exit 1
fi
if ! command -v rsync >/dev/null 2>&1; then
echo "❌ rsync is required (brew install rsync)." >&2
exit 1
fi
if ! command -v jq >/dev/null 2>&1; then
echo "️ jq not found; version.json will omit git metadata." >&2
fi
}
prepare_next_standalone() {
echo "▶️ Building Next.js standalone output"
if [ -d "$DIST_ROOT" ]; then
echo " • Clearing previous dist at $DIST_ROOT"
rm -rf "$DIST_ROOT"
fi
if [ -f "$REPO_ROOT/tsconfig.tsbuildinfo" ]; then
echo " • Removing stale tsconfig build info"
rm -f "$REPO_ROOT/tsconfig.tsbuildinfo"
fi
echo " • Using node: $(command -v node)"
echo " • Node version: $(node -v)"
echo " • Node module ABI: $(node -p 'process.versions.modules')"
local enable_subscriptions backend_url supabase_url supabase_anon app_url
enable_subscriptions="$(resolve_public_env_value "NEXT_PUBLIC_ENABLE_SUBSCRIPTION_BACKEND" "true")"
backend_url="$(resolve_public_env_value "NEXT_PUBLIC_BACKEND_URL" "https://api.ra-h.app")"
supabase_url="$(resolve_public_env_value "NEXT_PUBLIC_SUPABASE_URL" "https://wabhzavwgsizrkjpnryd.supabase.co")"
supabase_anon="$(resolve_public_env_value "NEXT_PUBLIC_SUPABASE_ANON_KEY" "")"
app_url="$(resolve_public_env_value "NEXT_PUBLIC_APP_URL" "https://ra-h.app")"
NEXT_PUBLIC_DEPLOYMENT_MODE=cloud \
NEXT_PUBLIC_ENABLE_SUBSCRIPTION_BACKEND="${enable_subscriptions}" \
NEXT_PUBLIC_BACKEND_URL="${backend_url}" \
NEXT_PUBLIC_SUPABASE_URL="${supabase_url}" \
NEXT_PUBLIC_SUPABASE_ANON_KEY="${supabase_anon}" \
NEXT_PUBLIC_APP_URL="${app_url}" \
NODE_ENV=production npm run build
if [ ! -d "$NEXT_BUILD_DIR/standalone" ]; then
echo "❌ Next.js standalone output missing at .next/standalone" >&2
exit 1
fi
}
prepare_directories() {
rm -rf "$DIST_ROOT"
rm -rf "$STAGING_ROOT"
mkdir -p "$DIST_ROOT/app"
mkdir -p "$DIST_ROOT/bin"
mkdir -p "$DIST_ROOT/lib"
mkdir -p "$DIST_ROOT/vendor/sqlite-extensions"
mkdir -p "$DIST_ROOT/resources"
mkdir -p "$DIST_ROOT/logs"
mkdir -p "$STAGING_ROOT"
}
write_splash_bootstrap() {
echo "🛰 Writing packaged splash bootstrap with instrumentation"
local build_id
build_id="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
cat <<'HTML' >"$DIST_ROOT/index.html"
<!DOCTYPE html>
<html lang="en" data-build="__BUILD_ID__">
<head>
<meta charset="utf-8" />
<title>RA-H</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style>
:root { color-scheme: dark; }
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: #09090b;
color: #f8fafc;
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
}
.status { text-align: center; letter-spacing: 0.02em; max-width: 480px; }
.status h1 { font-size: 1.5rem; margin: 0 0 0.5rem; }
.status p { margin: 0 0 0.75rem; color: #94a3b8; line-height: 1.4; }
.status pre {
text-align: left;
background: rgba(148, 163, 184, 0.08);
border-radius: 0.75rem;
padding: 0.75rem;
font-family: "SFMono-Regular", Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
font-size: 0.75rem;
max-height: 18rem;
overflow-y: auto;
margin: 0;
}
</style>
</head>
<body data-build="__BUILD_ID__">
<div class="status">
<h1 id="status-title">Starting RA-H…</h1>
<p id="status-message">Bootstrap script pending (<span id="status-build">__BUILD_ID__</span>).</p>
<pre id="status-log">[0 ms] Splash HTML loaded (build __BUILD_ID__)
</pre>
</div>
<script>
(function () {
const BUILD_ID = "__BUILD_ID__";
const start = performance.now();
const titleEl = document.getElementById('status-title');
const messageEl = document.getElementById('status-message');
const logEl = document.getElementById('status-log');
const state = {
polls: 0,
maxLogs: 300
};
const sinceStart = () => Math.round(performance.now() - start);
const appendLog = (label, detail = '') => {
const stamp = `[${sinceStart()} ms] ${label}`;
const suffix = detail ? ` :: ${detail}` : '';
if (logEl.textContent.split('\n').length > state.maxLogs) {
logEl.textContent = `${logEl.textContent.split('\n').slice(-state.maxLogs / 2).join('\n')}\n`;
}
logEl.textContent += `${stamp}${suffix}\n`;
logEl.scrollTop = logEl.scrollHeight;
try { console.log(`[splash:${BUILD_ID}] ${label}`, detail); } catch (_) {}
};
const updateStatus = (heading, text) => {
if (heading) titleEl.textContent = heading;
if (text) messageEl.textContent = text;
};
const navigateToPort = (port, source) => {
if (!port) {
appendLog('navigateToPort invoked with empty port', source);
return false;
}
const url = `http://127.0.0.1:${port}`;
updateStatus('Connecting…', `Requesting navigation to ${url}`);
appendLog('Attempting window.location.replace', `${url} via ${source}`);
try {
window.location.replace(url);
return true;
} catch (error) {
appendLog('window.location.replace threw', String(error ?? 'unknown error'));
}
try {
window.location.href = url;
appendLog('Fallback window.location.href dispatched', url);
return true;
} catch (error) {
appendLog('window.location.href threw', String(error ?? 'unknown error'));
}
return false;
};
const checkGlobalPort = (source) => {
const port = window.__RAH_SIDECAR_PORT;
if (port) {
appendLog('Detected global port', `${port} via ${source}`);
navigateToPort(port, source);
return true;
}
return false;
};
window.addEventListener('rah:sidecar-port', (event) => {
appendLog('Event: rah:sidecar-port', JSON.stringify(event?.detail));
navigateToPort(event?.detail, 'custom-event');
});
const tryAttachTauri = () => {
const api = window.__TAURI__;
if (!api || !api.tauri || !api.event) {
appendLog('Tauri APIs unavailable', 'Monitoring native dispatcher');
return;
}
appendLog('__TAURI__ detected', Object.keys(api).join(', '));
updateStatus('Tauri runtime detected', 'Listening for sidecar events.');
try {
api.event.listen('sidecar://port', (event) => {
appendLog('Event: sidecar://port', JSON.stringify(event?.payload));
navigateToPort(event?.payload, 'tauri-event');
}).catch((error) => appendLog('Failed to attach Tauri event listener', String(error ?? 'unknown error')));
} catch (error) {
appendLog('Error wiring Tauri listener', String(error ?? 'unknown error'));
}
try {
api.tauri.invoke('get_sidecar_port')
.then((existingPort) => {
appendLog('invoke(get_sidecar_port) resolved', JSON.stringify(existingPort));
if (existingPort) {
navigateToPort(existingPort, 'tauri-invoke');
} else {
updateStatus('Awaiting sidecar…', 'Will redirect once port event arrives.');
}
})
.catch((error) => {
appendLog('invoke(get_sidecar_port) rejected', String(error ?? 'unknown error'));
});
} catch (error) {
appendLog('Error invoking get_sidecar_port', String(error ?? 'unknown error'));
}
};
appendLog('Bootstrap script executing');
updateStatus('Initialising…', `Build ${BUILD_ID} starting up.`);
tryAttachTauri();
const pollForPort = () => {
if (checkGlobalPort(`poll-${state.polls}`)) {
return;
}
if (state.polls % 10 === 0) {
appendLog('Polling for native port', `attempt ${state.polls}`);
}
state.polls += 1;
setTimeout(pollForPort, 500);
};
if (!checkGlobalPort('initial')) {
pollForPort();
}
})();
</script>
</body>
</html>
HTML
python3 - <<PY
from pathlib import Path
path = Path(r"$DIST_ROOT/index.html")
content = path.read_text()
content = content.replace("__BUILD_ID__", "$build_id")
path.write_text(content)
PY
# Mirror splash bootstrap at dist/index.html so Tauri can locate web assets
mkdir -p "$REPO_ROOT/dist"
cp "$DIST_ROOT/index.html" "$REPO_ROOT/dist/index.html"
}
sync_next_output() {
echo "📦 Copying Next.js standalone output into dist/local-app"
rsync -a "$NEXT_BUILD_DIR/standalone/" "$DIST_ROOT/app/"
if [ -d "$NEXT_BUILD_DIR/static" ]; then
rsync -a "$NEXT_BUILD_DIR/static/" "$DIST_ROOT/app/.next/static/"
fi
# Next.js 15 places the shared vendor chunks outside of the standalone bundle.
if [ -d "$NEXT_BUILD_DIR/server/vendor-chunks" ]; then
echo " • Copying Next.js vendor chunks"
mkdir -p "$DIST_ROOT/app/.next/server/vendor-chunks"
rsync -a "$NEXT_BUILD_DIR/server/vendor-chunks/" "$DIST_ROOT/app/.next/server/vendor-chunks/"
fi
if [ -d "$REPO_ROOT/public" ]; then
rsync -a "$REPO_ROOT/public/" "$DIST_ROOT/app/public/"
fi
rm -f "$DIST_ROOT/app/src/helpers/"*.json 2>/dev/null || true
}
copy_seed_database() {
if [ ! -f "$SEED_SOURCE" ]; then
echo "❌ Seed database missing at $SEED_SOURCE" >&2
echo " Run scripts/database/generate-seed.sh to create it." >&2
exit 1
fi
cp "$SEED_SOURCE" "$DIST_ROOT/resources/rah_seed.sqlite"
}
prepare_node_runtime() {
echo "⚙️ Preparing universal Node runtime"
"$REPO_ROOT/scripts/build-universal-node.sh"
if [ ! -x "$RUNTIME_ROOT/bin/node" ]; then
echo "❌ Universal Node runtime missing at $RUNTIME_ROOT/bin/node" >&2
exit 1
fi
rsync -a "$RUNTIME_ROOT/bin/node" "$DIST_ROOT/bin/node"
chmod +x "$DIST_ROOT/bin/node"
# Ensure supporting libraries accompany the binary.
if [ -d "$RUNTIME_ROOT/lib" ]; then
rsync -a "$RUNTIME_ROOT/lib/" "$DIST_ROOT/lib/"
fi
}
sign_node_runtime() {
local node_bin="$DIST_ROOT/bin/node"
if [ ! -x "$node_bin" ]; then
echo "⚠️ Node runtime not found at $node_bin; skipping signing"
return
fi
if ! command -v codesign >/dev/null 2>&1; then
echo "⚠️ codesign not available; skipping Node runtime signing"
return
fi
local identity="Developer ID Application: Bradley Morris (HTMNQ7JM3H)"
local entitlements="$REPO_ROOT/apps/mac/src-tauri/entitlements-node.plist"
if ! security find-identity -v -p codesigning 2>/dev/null | grep -q "$identity"; then
echo "❌ Signing identity '$identity' not found; cannot produce a release build" >&2
exit 1
fi
echo "🔏 Signing Node runtime with $identity"
codesign --force --options runtime --timestamp --entitlements "$entitlements" --sign "$identity" "$node_bin"
}
stage_sidecar_launcher_script() {
local launcher_js="$REPO_ROOT/apps/mac/scripts/sidecar-launcher.js"
local destination="$DIST_ROOT/bin/sidecar-launcher.js"
if [ ! -f "$launcher_js" ]; then
echo "❌ sidecar-launcher.js missing at $launcher_js" >&2
exit 1
fi
mkdir -p "$(dirname "$destination")"
cp "$launcher_js" "$destination"
}
bundle_mcp_bridge() {
local server_entry="$REPO_ROOT/apps/mcp-server/server.js"
local stdio_entry="$REPO_ROOT/apps/mcp-server/stdio-server.js"
local out_dir="$DIST_ROOT/mcp-server"
if [ ! -f "$server_entry" ]; then
echo "❌ MCP server entry not found at $server_entry" >&2
exit 1
fi
mkdir -p "$out_dir"
echo "🧩 Bundling MCP HTTP bridge"
npx esbuild "$server_entry" \
--bundle \
--platform=node \
--target=node20 \
--format=cjs \
--outfile="$out_dir/server.js"
if [ -f "$stdio_entry" ]; then
echo "🧩 Bundling MCP STDIO bridge"
npx esbuild "$stdio_entry" \
--bundle \
--platform=node \
--target=node20 \
--format=cjs \
--banner:js="#!/usr/bin/env node" \
--outfile="$out_dir/stdio-server.js"
chmod +x "$out_dir/stdio-server.js"
fi
}
sign_native_modules() {
if ! command -v codesign >/dev/null 2>&1; then
echo "⚠️ codesign not available; skipping native module signing"
return
fi
local identity="Developer ID Application: Bradley Morris (HTMNQ7JM3H)"
local entitlements="$REPO_ROOT/apps/mac/src-tauri/entitlements-node.plist"
if ! security find-identity -v -p codesigning 2>/dev/null | grep -q "$identity"; then
echo "❌ Signing identity '$identity' not found; cannot produce a release build" >&2
exit 1
fi
local modules=(
"$DIST_ROOT/app/node_modules/better-sqlite3/build/Release/better_sqlite3.node"
"$DIST_ROOT/vendor/sqlite-extensions/vec0.dylib"
)
local vec_app_path="$DIST_ROOT/app/vendor/sqlite-extensions/vec0.dylib"
if [ -f "$vec_app_path" ]; then
modules+=("$vec_app_path")
fi
if [ -d "$DIST_ROOT/app/node_modules/@img" ]; then
while IFS= read -r module_path; do
modules+=("$module_path")
done < <(find "$DIST_ROOT/app/node_modules/@img" -type f \( -name "*.node" -o -name "*.dylib" \))
fi
for module_path in "${modules[@]}"; do
if [ -f "$module_path" ]; then
echo "🔏 Signing native module $(basename "$module_path")"
chmod +x "$module_path" 2>/dev/null || true
codesign --force --options runtime --timestamp --entitlements "$entitlements" --sign "$identity" "$module_path"
else
echo "⚠️ Native module not found at $module_path; skipping"
fi
done
}
prepare_vendor_assets() {
if [ -f "$REPO_ROOT/vendor/sqlite-extensions/vec0.dylib" ]; then
cp "$REPO_ROOT/vendor/sqlite-extensions/vec0.dylib" "$DIST_ROOT/vendor/sqlite-extensions/vec0.dylib"
fi
if [ -x "$REPO_ROOT/vendor/bin/yt-dlp" ]; then
mkdir -p "$DIST_ROOT/bin"
cp "$REPO_ROOT/vendor/bin/yt-dlp" "$DIST_ROOT/bin/yt-dlp"
chmod +x "$DIST_ROOT/bin/yt-dlp"
fi
}
sign_sidecar_launcher() {
local launcher="$REPO_ROOT/apps/mac/scripts/sidecar-launcher"
if [ ! -x "$launcher" ]; then
echo "⚠️ Sidecar launcher missing or not executable at $launcher"
return
fi
if ! command -v codesign >/dev/null 2>&1; then
echo "⚠️ codesign not available; skipping launcher signing"
return
fi
local identity="Developer ID Application: Bradley Morris (HTMNQ7JM3H)"
local entitlements="$REPO_ROOT/apps/mac/src-tauri/entitlements.plist"
if ! security find-identity -v -p codesigning 2>/dev/null | grep -q "$identity"; then
echo "❌ Signing identity '$identity' not found; cannot produce a release build" >&2
exit 1
fi
echo "🔏 Signing sidecar launcher with $identity"
codesign --force --options runtime --timestamp --entitlements "$entitlements" --sign "$identity" "$launcher"
}
sign_vendor_tools() {
local identity="Developer ID Application: Bradley Morris (HTMNQ7JM3H)"
if ! security find-identity -v -p codesigning 2>/dev/null | grep -q "$identity"; then
echo "❌ Signing identity '$identity' not found; cannot produce a release build" >&2
exit 1
fi
local yt_dlp="$DIST_ROOT/bin/yt-dlp"
if [ -x "$yt_dlp" ]; then
echo "🔏 Signing vendor tool $(basename "$yt_dlp")"
codesign --force --options runtime --timestamp --sign "$identity" "$yt_dlp"
fi
}
write_version_metadata() {
local version
if [ -n "${VERSION:-}" ]; then
version="$VERSION"
elif command -v jq >/dev/null 2>&1; then
version="$(jq -r '.version' < "$REPO_ROOT/package.json" 2>/dev/null || echo "0.0.0")"
else
version="$(node -p "require('./package.json').version" 2>/dev/null || echo "0.0.0")"
fi
local build_date
build_date="$(date +%Y%m%d)"
local git_sha
git_sha="$(git -C "$REPO_ROOT" rev-parse --short HEAD 2>/dev/null || echo unknown)"
local node_version
if [ -x "$RUNTIME_ROOT/bin/node" ]; then
node_version="$("$RUNTIME_ROOT/bin/node" -v 2>/dev/null || echo "vUnknown")"
else
node_version="v20.11.0"
fi
cat >"$DIST_ROOT/version.json" <<EOF
{
"version": "${version}",
"build_date": "${build_date}",
"node_version": "${node_version}",
"git_commit": "${git_sha}"
}
EOF
}
prepare_env_stub() {
local enable_subscriptions
enable_subscriptions="$(resolve_public_env_value "NEXT_PUBLIC_ENABLE_SUBSCRIPTION_BACKEND" "true")"
local deployment_mode
deployment_mode="cloud"
if [ "${NEXT_PUBLIC_DEPLOYMENT_MODE:-}" = "local" ]; then
echo "⚠️ Forcing NEXT_PUBLIC_DEPLOYMENT_MODE=cloud for production build" >&2
fi
local backend_url
backend_url="$(resolve_public_env_value "NEXT_PUBLIC_BACKEND_URL" "https://api.ra-h.app")"
local supabase_url
supabase_url="$(resolve_public_env_value "NEXT_PUBLIC_SUPABASE_URL" "https://wabhzavwgsizrkjpnryd.supabase.co")"
local supabase_anon
supabase_anon="$(resolve_public_env_value "NEXT_PUBLIC_SUPABASE_ANON_KEY" "")"
if [ -z "$supabase_anon" ]; then
echo "⚠️ NEXT_PUBLIC_SUPABASE_ANON_KEY not set in environment or .env; packaged app may not authenticate." >&2
fi
cat >"$DIST_ROOT/.env.production" <<EOF
# Production runtime configuration (public environment values only)
NODE_ENV=production
NEXT_PUBLIC_ENABLE_SUBSCRIPTION_BACKEND=${enable_subscriptions}
NEXT_PUBLIC_BACKEND_URL=${backend_url}
NEXT_PUBLIC_SUPABASE_URL=${supabase_url}
NEXT_PUBLIC_SUPABASE_ANON_KEY=${supabase_anon}
NEXT_PUBLIC_DEPLOYMENT_MODE=${deployment_mode}
EOF
}
build_native_modules() {
echo "⚙️ Building universal native modules"
"$REPO_ROOT/scripts/build-native-modules.sh"
}
main() {
ensure_prerequisites
prepare_next_standalone
prepare_directories
write_splash_bootstrap
sync_next_output
copy_seed_database
prepare_node_runtime
stage_sidecar_launcher_script
bundle_mcp_bridge
sign_node_runtime
prepare_vendor_assets
sign_sidecar_launcher
sign_vendor_tools
write_version_metadata
prepare_env_stub
build_native_modules
sign_native_modules
echo ""
echo "✅ Production payload staged in $DIST_ROOT"
}
main "$@"
+137
View File
@@ -0,0 +1,137 @@
#!/usr/bin/env bash
set -euo pipefail
# Build a universal (arm64 + x86_64) Node.js runtime staged inside dist/runtime/node-universal.
# The script downloads the official macOS tarballs for each architecture, combines the Mach-O
# binaries with lipo, and leaves the result ready to be copied into the packaged app.
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
NODE_VERSION="${NODE_VERSION:-20.11.0}"
CACHE_ROOT="${NODE_CACHE_DIR:-$HOME/.cache/ra-h/node}"
OUTPUT_ROOT="${NODE_OUTPUT_DIR:-$REPO_ROOT/dist/runtime/node-universal}"
STAGING_ROOT="${NODE_STAGING_DIR:-$REPO_ROOT/dist/.staging/node-universal}"
ARM_ARCH="arm64"
X64_ARCH="x64"
ARM_TARBALL="node-v${NODE_VERSION}-darwin-${ARM_ARCH}"
X64_TARBALL="node-v${NODE_VERSION}-darwin-${X64_ARCH}"
ensure_prerequisites() {
if ! command -v curl >/dev/null 2>&1; then
echo "❌ curl not found. Install curl before running this script." >&2
exit 1
fi
if ! command -v tar >/dev/null 2>&1; then
echo "❌ tar not found. Install GNU tar / BSD tar before running this script." >&2
exit 1
fi
if ! command -v rsync >/dev/null 2>&1; then
echo "❌ rsync not found. Install rsync before running this script." >&2
exit 1
fi
if ! command -v lipo >/dev/null 2>&1; then
echo "❌ lipo not found. Install Xcode command line tools (xcode-select --install)." >&2
exit 1
fi
}
download_node() {
local tarball_name="$1"
local archive_dir="$CACHE_ROOT/${tarball_name}"
if [ -x "${archive_dir}/bin/node" ]; then
echo "✓ Node ${tarball_name} cached at ${archive_dir}"
return
fi
mkdir -p "$CACHE_ROOT"
local url="https://nodejs.org/dist/v${NODE_VERSION}/${tarball_name}.tar.gz"
echo "⬇️ Downloading ${url}"
tmp_archive="$(mktemp -d -t node-archive-XXXX)"
trap 'rm -rf "$tmp_archive"' EXIT
curl -fsSL "$url" | tar xz -C "$tmp_archive"
mv "${tmp_archive}/${tarball_name}" "$archive_dir"
rm -rf "$tmp_archive"
trap - EXIT
echo "✓ Node ${tarball_name} cached"
}
prepare_staging() {
rm -rf "$OUTPUT_ROOT"
rm -rf "$STAGING_ROOT"
mkdir -p "$STAGING_ROOT"
mkdir -p "$OUTPUT_ROOT"
}
copy_arch_tree() {
local src="$1"
local dest="$2"
mkdir -p "$(dirname "$dest")"
rsync -a --delete "$src/" "$dest/"
}
create_universal_binary() {
local relative_path="$1"
local arm_file="${STAGING_ROOT}/${ARM_TARBALL}/${relative_path}"
local x64_file="${STAGING_ROOT}/${X64_TARBALL}/${relative_path}"
local output_file="${OUTPUT_ROOT}/${relative_path}"
if [ ! -f "$arm_file" ] || [ ! -f "$x64_file" ]; then
return
fi
mkdir -p "$(dirname "$output_file")"
lipo -create "$arm_file" "$x64_file" -output "$output_file"
}
walk_and_merge_binaries() {
local pattern='*'
(
cd "${STAGING_ROOT}/${ARM_TARBALL}"
find . -type f | while read -r relative; do
if file "${relative}" 2>/dev/null | grep -q "Mach-O"; then
create_universal_binary "${relative#./}"
fi
done
)
}
finalize_assets() {
# Copy everything from arm64 tree first (text files, scripts, npm, etc.)
copy_arch_tree "${STAGING_ROOT}/${ARM_TARBALL}" "$OUTPUT_ROOT"
# Merge Mach-O binaries/dylibs with lipo (overwrites the copies above).
walk_and_merge_binaries
chmod +x "$OUTPUT_ROOT/bin/node"
echo "node" >"$OUTPUT_ROOT/.arch"
}
print_summary() {
echo ""
echo "✅ Universal Node runtime prepared:"
echo " - Version: v${NODE_VERSION}"
echo " - Output : ${OUTPUT_ROOT}"
echo " - Includes npm CLI at ${OUTPUT_ROOT}/lib/node_modules/npm/"
echo ""
lipo -info "$OUTPUT_ROOT/bin/node"
}
main() {
ensure_prerequisites
download_node "$ARM_TARBALL"
download_node "$X64_TARBALL"
prepare_staging
copy_arch_tree "$CACHE_ROOT/$ARM_TARBALL" "$STAGING_ROOT/$ARM_TARBALL"
copy_arch_tree "$CACHE_ROOT/$X64_TARBALL" "$STAGING_ROOT/$X64_TARBALL"
finalize_assets
print_summary
}
main "$@"
+109
View File
@@ -0,0 +1,109 @@
#!/usr/bin/env node
// Script to create Supabase database schema using the REST API
// This works around the issue with direct SQL execution via psql
const https = require('https');
const SUPABASE_URL = process.env.SUPABASE_URL;
const SERVICE_KEY = process.env.SUPABASE_SERVICE_ROLE_KEY;
if (!SUPABASE_URL || !SERVICE_KEY) {
console.error('Missing SUPABASE_URL or SUPABASE_SERVICE_ROLE_KEY environment variables.');
process.exit(1);
}
// SQL statements to execute
const sqlStatements = [
// Create subscriptions table
`CREATE TABLE IF NOT EXISTS public.subscriptions (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
user_id UUID NOT NULL,
stripe_subscription_id TEXT UNIQUE,
stripe_customer_id TEXT,
tier TEXT NOT NULL CHECK (tier IN ('free', 'lite', 'pro', 'max')),
status TEXT NOT NULL CHECK (status IN ('active', 'canceled', 'past_due', 'trialing', 'incomplete')),
current_period_start TIMESTAMPTZ,
current_period_end TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL,
updated_at TIMESTAMPTZ DEFAULT NOW() NOT NULL
);`,
// Create usage_events table
`CREATE TABLE IF NOT EXISTS public.usage_events (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
user_id UUID NOT NULL,
timestamp TIMESTAMPTZ DEFAULT NOW() NOT NULL,
model TEXT NOT NULL,
tokens_prompt INTEGER NOT NULL DEFAULT 0,
tokens_completion INTEGER NOT NULL DEFAULT 0,
cost_usd DECIMAL(10,6) NOT NULL DEFAULT 0,
endpoint TEXT NOT NULL,
provider TEXT NOT NULL CHECK (provider IN ('openai', 'anthropic', 'tavily')),
created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL
);`,
// Create indexes
`CREATE INDEX IF NOT EXISTS idx_subscriptions_user_id ON public.subscriptions(user_id);`,
`CREATE INDEX IF NOT EXISTS idx_usage_events_user_id ON public.usage_events(user_id);`
];
function executeSQL(sql) {
return new Promise((resolve, reject) => {
const postData = JSON.stringify({ query: sql });
const options = {
hostname: new URL(SUPABASE_URL).hostname,
port: 443,
path: '/rest/v1/rpc/query',
method: 'POST',
headers: {
'Content-Type': 'application/json',
'apikey': SERVICE_KEY,
'Authorization': `Bearer ${SERVICE_KEY}`,
'Content-Length': Buffer.byteLength(postData)
}
};
const req = https.request(options, (res) => {
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
if (res.statusCode >= 200 && res.statusCode < 300) {
resolve(data);
} else {
reject(new Error(`HTTP ${res.statusCode}: ${data}`));
}
});
});
req.on('error', (e) => {
reject(e);
});
req.write(postData);
req.end();
});
}
async function createSchema() {
console.log('Creating Supabase schema...');
for (let i = 0; i < sqlStatements.length; i++) {
const sql = sqlStatements[i];
try {
console.log(`Executing statement ${i + 1}/${sqlStatements.length}...`);
const result = await executeSQL(sql);
console.log('✓ Success');
} catch (error) {
console.error(`✗ Failed: ${error.message}`);
// Don't exit on error, continue with next statement
}
}
console.log('Schema creation complete!');
}
createSchema().catch(console.error);
+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"
+39
View File
@@ -0,0 +1,39 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT_DIR"
RED='\033[0;31m'
GREEN='\033[0;32m'
NC='\033[0m'
TMP_FILE="$(mktemp)"
printf "[audit] scanning for accidental API keys (sk-)...\n"
if rg --hidden --no-messages --files-with-matches "sk-" \
--glob '!.git/**' --glob '!node_modules/**' --glob '!dist/**' --glob '!apps/mac/src-tauri/target/**' \
> "$TMP_FILE"; then
printf "${RED}Found potential secrets:${NC}\n"
cat "$TMP_FILE"
rm -f "$TMP_FILE"
exit 1
else
status=$?
if [ "$status" -ne 1 ]; then
rm -f "$TMP_FILE"
exit "$status"
fi
printf "${GREEN}No sk- tokens found.${NC}\n"
fi
rm -f "$TMP_FILE"
printf "[audit] checking tracked files > 50MB...\n"
LARGE_FILES=$(git ls-tree -r HEAD --long | awk '$4 > 52428800 {printf "%s\t%s\n", $4, $5}')
if [ -n "$LARGE_FILES" ]; then
printf "${RED}Large tracked files detected (>50MB):${NC}\n%s\n" "$LARGE_FILES"
exit 1
else
printf "${GREEN}No tracked blobs exceed 50MB.${NC}\n"
fi
printf "[audit] done.\n"
+56
View File
@@ -0,0 +1,56 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_DIR="$(cd "${SCRIPT_DIR}/../.." && pwd)"
ENV_TEMPLATE="${REPO_DIR}/.env.example.local"
TARGET_ENV="${REPO_DIR}/.env.local"
SQLITE_SEED_SCRIPT="${REPO_DIR}/scripts/database/sqlite-ensure-app-schema.sh"
log() {
echo "[bootstrap-local] $1"
}
if [ ! -f "$ENV_TEMPLATE" ]; then
echo "Error: ${ENV_TEMPLATE} not found. Make sure .env.example.local exists." >&2
exit 1
fi
if [ -f "$TARGET_ENV" ]; then
log ".env.local already exists; leaving it untouched."
else
log "Creating .env.local from template..."
cp "$ENV_TEMPLATE" "$TARGET_ENV"
fi
SQLITE_DB_PATH_LINE=$(grep -E '^SQLITE_DB_PATH=' "$TARGET_ENV" | tail -n 1 || true)
DEFAULT_DB_PATH="$HOME/Library/Application Support/RA-H/db/rah.sqlite"
if [ -z "$SQLITE_DB_PATH_LINE" ]; then
SQLITE_DB_PATH="$DEFAULT_DB_PATH"
else
SQLITE_DB_PATH="${SQLITE_DB_PATH_LINE#SQLITE_DB_PATH=}"
fi
# Expand variables like $HOME or ~
EXPANDED_DB_PATH=$(eval "echo \"$SQLITE_DB_PATH\"")
DB_DIR=$(dirname "$EXPANDED_DB_PATH")
log "Ensuring database directory exists: $DB_DIR"
mkdir -p "$DB_DIR"
if [ ! -f "$EXPANDED_DB_PATH" ]; then
log "Creating empty SQLite database at $EXPANDED_DB_PATH"
: > "$EXPANDED_DB_PATH"
else
log "SQLite database already exists at $EXPANDED_DB_PATH"
fi
if [ ! -x "$SQLITE_SEED_SCRIPT" ]; then
echo "Error: $SQLITE_SEED_SCRIPT not found or not executable" >&2
exit 1
fi
log "Seeding database schema via sqlite-ensure-app-schema.sh"
"$SQLITE_SEED_SCRIPT" "$EXPANDED_DB_PATH"
log "Bootstrap complete. Run 'npm run dev:local' to start the local-first app."
+38
View File
@@ -0,0 +1,38 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
INFO() { printf "[clean-local] %s\n" "$1"; }
maybe_remove_dir() {
local target="$1"
if [ -d "$target" ]; then
rm -rf "$target"
INFO "Removed directory $target"
fi
}
maybe_remove_file() {
local target="$1"
if [ -f "$target" ]; then
rm -f "$target"
INFO "Removed file $target"
fi
}
INFO "Cleaning generated artifacts (safe-only)"
maybe_remove_dir "$REPO_ROOT/dist/local-app"
maybe_remove_dir "$REPO_ROOT/dist/runtime"
maybe_remove_dir "$REPO_ROOT/dist/.staging"
maybe_remove_dir "$REPO_ROOT/.next"
maybe_remove_dir "$REPO_ROOT/apps/mac/dist"
maybe_remove_file "$REPO_ROOT/dist/index.html"
maybe_remove_file "$REPO_ROOT/logs/helper-interactions.log"
maybe_remove_file "$REPO_ROOT/logs/next-dev.log"
maybe_remove_file "$REPO_ROOT/app/api/logs/requests.log"
INFO "Cleanup complete"
+96
View File
@@ -0,0 +1,96 @@
#!/usr/bin/env node
/*
Dev utility: Inspect memory claims ledger and write a Markdown report.
Usage: node scripts/dev/inspect-memory-ledger.js [--out docs/development/reports/memory-ledger.md] [--versions 5]
Optionally set RAH_DB_PATH to override the default DB path.
*/
const fs = require('fs');
const path = require('path');
const os = require('os');
const { execFileSync } = require('child_process');
const args = process.argv.slice(2);
const outIdx = args.indexOf('--out');
const outPath = outIdx >= 0 ? args[outIdx + 1] : 'docs/development/reports/memory-ledger.md';
const verIdx = args.indexOf('--versions');
const maxVersions = verIdx >= 0 ? parseInt(args[verIdx + 1], 10) : 5;
const defaultDbPath = path.join(os.homedir(), 'Library', 'Application Support', 'RA-H', 'db', 'rah.sqlite');
const dbPath = process.env.RAH_DB_PATH || defaultDbPath;
function q(dbPath, sql, params = []) {
const paramList = params.map(p => typeof p === 'number' ? String(p) : `'${String(p).replace(/'/g, "''")}'`).join(',');
const wrapped = params.length ? sql.replace(/\?/g, () => params.shift()) : sql;
try {
const out = execFileSync('sqlite3', ['-json', dbPath, wrapped], { encoding: 'utf8' });
return JSON.parse(out || '[]');
} catch (e) {
console.error('sqlite3 query failed:', e.message);
process.exit(1);
}
}
function loadCategoryRows(dbPath, category, limit) {
const sql = `SELECT id, version, is_current, content, metadata, created_at
FROM memory
WHERE type='big_memory' AND entity_id='${category}'
ORDER BY version DESC
LIMIT ${limit}`;
return q(dbPath, sql);
}
function parseMeta(m) {
try { return m ? JSON.parse(m) : {}; } catch { return {}; }
}
function renderTable(rowsLatest, rowsPrev) {
// rowsLatest: {claim, count, first_seen, last_seen}
// Map previous counts for delta
const prevMap = new Map(rowsPrev.map(r => [r.claim, r.count]));
const header = `| claim | count | Δ | first_seen | last_seen |\n|---|---:|---:|---|---|`;
const lines = rowsLatest.map(r => {
const prev = prevMap.get(r.claim) || 0;
const delta = r.count - prev;
return `| ${r.claim} | ${r.count} | ${delta >= 0 ? '+'+delta : delta} | ${r.first_seen} | ${r.last_seen} |`;
});
return [header, ...lines].join('\n');
}
function main() {
const categories = ['world_model','interests','goals','styles','connections'];
let md = `# Memory Ledger Report\n\n- DB: \`${dbPath}\`\n- Generated: ${new Date().toISOString()}\n- Versions shown per category: ${maxVersions}\n\n`;
for (const cat of categories) {
const rows = loadCategoryRows(dbPath, cat, maxVersions);
if (!rows || rows.length === 0) {
md += `## ${cat}\n(No rows)\n\n`;
continue;
}
const current = rows[0];
const prev = rows[1] || null;
const curMeta = parseMeta(current.metadata);
const prevMeta = parseMeta(prev && prev.metadata);
const curClaims = curMeta.claims || {};
const prevClaims = prevMeta.claims || {};
const toArr = (obj) => Object.entries(obj).map(([k, v]) => ({ claim: k, count: Number(v.count || 0), first_seen: v.first_seen || '', last_seen: v.last_seen || '' }));
const curArr = toArr(curClaims).sort((a,b) => b.count - a.count || (a.last_seen > b.last_seen ? -1 : 1)).slice(0, 50);
const prevArr = toArr(prevClaims).sort((a,b) => b.count - a.count).slice(0, 50);
md += `## ${cat}\n\n`;
md += `- current version: ${current.version} (created ${current.created_at})\n`;
if (prev) md += `- previous version: ${prev.version} (created ${prev.created_at})\n`;
md += `- current paragraph (first 200 chars):\n\n> ${String(current.content || '').slice(0,200)}\n\n`;
md += `### Top Claims (current vs previous Δ)\n\n`;
md += renderTable(curArr, prevArr) + '\n\n';
}
// Ensure folder exists
const outAbs = path.resolve(outPath);
fs.mkdirSync(path.dirname(outAbs), { recursive: true });
fs.writeFileSync(outAbs, md, 'utf8');
console.log(`Wrote report: ${outAbs}`);
}
main();
+47
View File
@@ -0,0 +1,47 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
REPO_DIR="$(cd "${ROOT_DIR}/.." && pwd)"
PORT="${PORT:-3000}"
HOST="127.0.0.1"
NEXT_LOG="${REPO_DIR}/logs/next-dev.log"
mkdir -p "${REPO_DIR}/logs"
cleanup() {
if [[ -n "${NEXT_PID:-}" ]]; then
echo "Shutting down Next.js dev server (pid ${NEXT_PID})"
kill "${NEXT_PID}" 2>/dev/null || true
wait "${NEXT_PID}" 2>/dev/null || true
fi
}
trap cleanup EXIT INT TERM
echo "Starting Next.js dev server on http://${HOST}:${PORT}"
HOST="${HOST}" PORT="${PORT}" npm run dev > "${NEXT_LOG}" 2>&1 &
NEXT_PID=$!
sleep 1
echo -n "Waiting for Next.js to become ready"
for _ in $(seq 1 90); do
if nc -z "${HOST}" "${PORT}" 2>/dev/null; then
echo " ✓"
break
fi
echo -n "."
sleep 1
done
if ! nc -z "${HOST}" "${PORT}" 2>/dev/null; then
echo " ✗ Next.js did not start within 90s. Last log lines:"
tail -n 40 "${NEXT_LOG}" || true
exit 1
fi
echo "Next.js ready. Launching Tauri shell."
RAH_ATTACH_DEV_SERVER=true \
RAH_DEV_SERVER_PORT="${PORT}" \
PORT="${PORT}" \
HOST="${HOST}" \
npm run tauri:dev --workspace @ra-h/mac-shell
+132
View File
@@ -0,0 +1,132 @@
#!/usr/bin/env bash
set -euo pipefail
# Generate a Tauri updater manifest for the universal DMG build.
# Usage:
# scripts/generate-update-manifest.sh \
# --file dist/bundle/RA-H.dmg \
# --url https://updates.ra-h.app/dmg/RA-H.dmg \
# --version 0.1.0 \
# --out dist/update/latest.json \
# [--notes release-notes.md] \
# [--signature <base64 signature>]
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
FILE_PATH=""
DOWNLOAD_URL=""
VERSION=""
OUTPUT_PATH=""
NOTES_CONTENT=""
SIGNATURE="${TAURI_UPDATER_SIGNATURE:-}"
usage() {
grep '^#' "$0" | cut -c 3-
}
while [[ $# -gt 0 ]]; do
case "$1" in
--file)
FILE_PATH="$2"
shift 2
;;
--url)
DOWNLOAD_URL="$2"
shift 2
;;
--version)
VERSION="$2"
shift 2
;;
--out)
OUTPUT_PATH="$2"
shift 2
;;
--notes)
NOTES_CONTENT="$(cat "$2")"
shift 2
;;
--signature)
SIGNATURE="$2"
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown argument: $1" >&2
usage
exit 1
;;
esac
done
if [ -z "$FILE_PATH" ] || [ -z "$DOWNLOAD_URL" ] || [ -z "$VERSION" ] || [ -z "$OUTPUT_PATH" ]; then
echo "❌ Missing required arguments." >&2
usage
exit 1
fi
if [ ! -f "$FILE_PATH" ]; then
echo "❌ File not found: $FILE_PATH" >&2
exit 1
fi
ensure_signature() {
if [ -n "$SIGNATURE" ]; then
return
fi
if [ -n "${TAURI_PRIVATE_KEY:-}" ]; then
if ! command -v tauri >/dev/null 2>&1; then
echo "❌ Tauri CLI not found; cannot derive signature automatically." >&2
exit 1
fi
echo "🔐 Generating signature via tauri signer"
SIGNATURE="$(tauri signer sign \
--private-key "$TAURI_PRIVATE_KEY" \
${TAURI_PRIVATE_KEY_PASSWORD:+--password "$TAURI_PRIVATE_KEY_PASSWORD"} \
"$FILE_PATH" | tail -n 1)"
fi
if [ -z "$SIGNATURE" ]; then
echo "❌ Provide --signature or set TAURI_PRIVATE_KEY/TAURI_UPDATER_SIGNATURE." >&2
exit 1
fi
}
ensure_signature
if ! command -v shasum >/dev/null 2>&1; then
echo "❌ shasum not found. Install coreutils." >&2
exit 1
fi
SHA512="$(shasum -a 512 "$FILE_PATH" | awk '{print $1}')"
PUB_DATE="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
if ! command -v jq >/dev/null 2>&1; then
echo "❌ jq is required to emit the manifest." >&2
exit 1
fi
jq -n \
--arg version "$VERSION" \
--arg notes "$NOTES_CONTENT" \
--arg pub_date "$PUB_DATE" \
--arg url "$DOWNLOAD_URL" \
--arg sha "$SHA512" \
--arg sig "$SIGNATURE" \
'{
version: $version,
notes: $notes,
pub_date: $pub_date,
platforms: {
"darwin-aarch64": { signature: $sig, url: $url, sha512: $sha },
"darwin-x86_64": { signature: $sig, url: $url, sha512: $sha }
}
}' >"$OUTPUT_PATH"
echo "✅ Update manifest written to $OUTPUT_PATH"
@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.ra-h.sqlite-backup</string>
<key>ProgramArguments</key>
<array>
<string>/bin/bash</string>
<string>-lc</string>
<string>cd /Users/bradleymorris/Desktop/dev/ra-h && bash scripts/database/sqlite-backup.sh</string>
</array>
<key>WorkingDirectory</key>
<string>/Users/bradleymorris/Desktop/dev/ra-h</string>
<key>StartCalendarInterval</key>
<dict>
<key>Hour</key>
<integer>2</integer>
<key>Minute</key>
<integer>0</integer>
</dict>
<key>RunAtLoad</key>
<true/>
<key>StandardOutPath</key>
<string>/Users/bradleymorris/Library/Logs/ra-h-sqlite-backup.log</string>
<key>StandardErrorPath</key>
<string>/Users/bradleymorris/Library/Logs/ra-h-sqlite-backup.log</string>
</dict>
</plist>
+57
View File
@@ -0,0 +1,57 @@
#!/bin/bash
# Export all PostgreSQL data to CSV files for SQLite migration
set -e # Exit on error
echo "Starting PostgreSQL data export..."
# Ensure tmp directory exists
mkdir -p tmp/migrate
# Database connection string
DB_URL="postgresql://rah_user:rah_password@localhost:5432/rah_db"
# Export nodes
echo "Exporting nodes..."
psql "$DB_URL" -c "\COPY (SELECT id, title, content, link, created_at, updated_at FROM nodes ORDER BY id) TO STDOUT WITH CSV HEADER" > tmp/migrate/nodes.csv
# Export dimensions (unnest array into rows)
echo "Exporting dimensions (array to rows)..."
psql "$DB_URL" -c "\COPY (SELECT n.id AS node_id, TRIM(unnest(n.dimensions)) AS dimension FROM nodes n WHERE n.dimensions IS NOT NULL AND array_length(n.dimensions, 1) > 0 ORDER BY n.id) TO STDOUT WITH CSV HEADER" > tmp/migrate/node_dimensions.csv
# Export chunks (without embeddings)
echo "Exporting chunks..."
psql "$DB_URL" -c "\COPY (SELECT id, node_id, chunk_idx, text, created_at FROM chunks ORDER BY id) TO STDOUT WITH CSV HEADER" > tmp/migrate/chunks.csv
# Export embeddings as CSV text (handle NULL embeddings)
echo "Exporting embeddings..."
psql "$DB_URL" -c "\COPY (SELECT id AS chunk_id, CASE WHEN embedding IS NOT NULL THEN trim(both '[]' from embedding::text) ELSE NULL END AS embedding_csv FROM chunks WHERE embedding IS NOT NULL ORDER BY id) TO STDOUT WITH CSV HEADER" > tmp/migrate/chunk_embeddings.csv
# Export edges
echo "Exporting edges..."
psql "$DB_URL" -c "\COPY (SELECT id, from_node_id, to_node_id, source, created_at FROM edges ORDER BY id) TO STDOUT WITH CSV HEADER" > tmp/migrate/edges.csv
# Export chats (for completeness)
echo "Exporting chats..."
psql "$DB_URL" -c "\COPY (SELECT id, chat_type, helper_name, user_message, assistant_message, thread_id, focused_node_id, created_at FROM chats ORDER BY id) TO STDOUT WITH CSV HEADER" > tmp/migrate/chats.csv
# Get counts for verification
echo ""
echo "Export complete! Counts:"
echo "------------------------"
echo -n "Nodes: "
psql "$DB_URL" -t -c "SELECT COUNT(*) FROM nodes"
echo -n "Dimensions: "
psql "$DB_URL" -t -c "SELECT COUNT(*) FROM (SELECT unnest(dimensions) FROM nodes WHERE dimensions IS NOT NULL) AS d"
echo -n "Chunks: "
psql "$DB_URL" -t -c "SELECT COUNT(*) FROM chunks"
echo -n "Chunks with embeddings: "
psql "$DB_URL" -t -c "SELECT COUNT(*) FROM chunks WHERE embedding IS NOT NULL"
echo -n "Edges: "
psql "$DB_URL" -t -c "SELECT COUNT(*) FROM edges"
echo -n "Chats: "
psql "$DB_URL" -t -c "SELECT COUNT(*) FROM chats"
echo ""
echo "Files exported to tmp/migrate/"
echo "Next step: Run ./scripts/migrate/import_to_sqlite.sh"
+148
View File
@@ -0,0 +1,148 @@
#!/bin/bash
# Simplified import without FTS complications
set -e
echo "Starting simplified SQLite import..."
# Remove old database
rm -f rah_trial.db
# Create basic schema first
sqlite3 rah_trial.db <<'SQL'
-- Basic settings
PRAGMA foreign_keys = ON;
PRAGMA journal_mode = WAL;
PRAGMA synchronous = NORMAL;
PRAGMA cache_size = -200000;
PRAGMA temp_store = MEMORY;
PRAGMA mmap_size = 268435456;
-- Create tables without FTS first
CREATE TABLE nodes (
id INTEGER PRIMARY KEY,
title TEXT,
content TEXT,
link TEXT,
created_at TEXT,
updated_at TEXT
);
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 TABLE chunks (
id INTEGER PRIMARY KEY,
node_id INTEGER NOT NULL,
chunk_idx INTEGER,
text TEXT,
created_at TEXT,
FOREIGN KEY (node_id) REFERENCES nodes(id) ON DELETE CASCADE
);
CREATE TABLE chunk_embeddings (
chunk_id INTEGER PRIMARY KEY,
embedding TEXT NOT NULL,
FOREIGN KEY (chunk_id) REFERENCES chunks(id) ON DELETE CASCADE
);
CREATE TABLE edges (
id INTEGER PRIMARY KEY,
from_node_id INTEGER NOT NULL,
to_node_id INTEGER NOT NULL,
source TEXT,
created_at TEXT,
FOREIGN KEY (from_node_id) REFERENCES nodes(id) ON DELETE CASCADE,
FOREIGN KEY (to_node_id) REFERENCES nodes(id) ON DELETE CASCADE
);
CREATE TABLE chats (
id INTEGER PRIMARY KEY,
chat_type TEXT,
helper_name TEXT,
user_message TEXT,
assistant_message TEXT,
thread_id TEXT,
focused_node_id INTEGER,
created_at TEXT,
FOREIGN KEY (focused_node_id) REFERENCES nodes(id) ON DELETE SET NULL
);
-- Create indexes
CREATE INDEX idx_dim_by_dimension ON node_dimensions(dimension, node_id);
CREATE INDEX idx_dim_by_node ON node_dimensions(node_id, dimension);
CREATE INDEX idx_chunks_by_node ON chunks(node_id);
CREATE INDEX idx_chunks_by_node_idx ON chunks(node_id, chunk_idx);
CREATE INDEX idx_edges_from ON edges(from_node_id);
CREATE INDEX idx_edges_to ON edges(to_node_id);
CREATE INDEX idx_chats_thread ON chats(thread_id);
SQL
echo "Schema created. Importing data..."
# Import CSVs with proper handling
sqlite3 rah_trial.db <<'SQL'
.mode csv
-- Import nodes
.import --skip 1 tmp/migrate/nodes.csv nodes
-- Import dimensions
.import --skip 1 tmp/migrate/node_dimensions.csv node_dimensions
-- Import chunks
.import --skip 1 tmp/migrate/chunks.csv chunks
-- Import embeddings
.import --skip 1 tmp/migrate/chunk_embeddings.csv chunk_embeddings
-- Import edges (skip header)
.import --skip 1 tmp/migrate/edges.csv edges
-- Import chats
.import --skip 1 tmp/migrate/chats.csv chats
-- Add FTS after data is loaded
CREATE VIRTUAL TABLE chunks_fts USING fts5(
text,
content=chunks,
content_rowid=id,
tokenize='porter ascii'
);
CREATE VIRTUAL TABLE nodes_fts USING fts5(
title,
content,
content=nodes,
content_rowid=id,
tokenize='porter ascii'
);
-- Populate FTS
INSERT INTO chunks_fts(text) SELECT text FROM chunks;
INSERT INTO nodes_fts(title, content) SELECT title, content FROM nodes;
-- Analyze for optimization
ANALYZE;
SQL
# Verify counts
echo ""
echo "Verifying import..."
sqlite3 rah_trial.db <<'SQL'
.mode list
SELECT 'Nodes: ' || COUNT(*) FROM nodes;
SELECT 'Chunks: ' || COUNT(*) FROM chunks;
SELECT 'Embeddings: ' || COUNT(*) FROM chunk_embeddings;
SELECT 'Dimensions: ' || COUNT(*) FROM node_dimensions;
SELECT 'Edges: ' || COUNT(*) FROM edges;
SELECT 'Chats: ' || COUNT(*) FROM chats;
SELECT 'Database size: ' || ROUND(page_count * page_size / 1024.0 / 1024.0, 2) || ' MB' FROM pragma_page_count(), pragma_page_size();
SQL
echo ""
echo "✅ Import complete!"
+115
View File
@@ -0,0 +1,115 @@
#!/bin/bash
# Import CSV data into SQLite database
set -e # Exit on error
echo "Starting SQLite database import..."
echo "================================"
# Remove old database if exists
if [ -f "rah_trial.db" ]; then
echo "Removing existing rah_trial.db..."
rm -f rah_trial.db
fi
# Create database with schema
echo "Creating database with optimized schema..."
sqlite3 rah_trial.db < scripts/migrate/sqlite_schema.sql
# Import CSV files
echo ""
echo "Importing data from CSV files..."
echo "--------------------------------"
sqlite3 rah_trial.db <<'EOF'
.mode csv
.headers on
-- Import nodes (handle embedded quotes/newlines)
.print "Importing nodes..."
.separator ","
.import tmp/migrate/nodes.csv nodes_temp
INSERT INTO nodes SELECT * FROM nodes_temp WHERE id != 'id';
DROP TABLE nodes_temp;
-- Import node dimensions
.print "Importing dimensions..."
.import tmp/migrate/node_dimensions.csv node_dimensions
-- Import chunks
.print "Importing chunks..."
.separator ","
.import tmp/migrate/chunks.csv chunks_temp
INSERT INTO chunks SELECT * FROM chunks_temp WHERE id != 'id';
DROP TABLE chunks_temp;
-- Import embeddings
.print "Importing embeddings (this may take a moment)..."
.import tmp/migrate/chunk_embeddings.csv chunk_embeddings
-- Import edges
.print "Importing edges..."
.import tmp/migrate/edges.csv edges
-- Import chats
.print "Importing chats..."
.separator ","
.import tmp/migrate/chats.csv chats_temp
INSERT INTO chats SELECT * FROM chats_temp WHERE id != 'id';
DROP TABLE chats_temp;
-- Populate FTS indexes
.print ""
.print "Building full-text search indexes..."
INSERT INTO chunks_fts(rowid, text) SELECT id, text FROM chunks;
INSERT INTO nodes_fts(rowid, title, content) SELECT id, title, content FROM nodes;
-- Run ANALYZE for query optimization
.print "Analyzing tables for query optimization..."
ANALYZE;
-- Verify imports with counts
.print ""
.print "Import complete! Verifying counts:"
.print "===================================="
SELECT printf('Nodes: %d', COUNT(*)) FROM nodes;
SELECT printf('Node dimensions: %d', COUNT(*)) FROM node_dimensions;
SELECT printf('Unique dimensions: %d', COUNT(DISTINCT dimension)) FROM node_dimensions;
SELECT printf('Chunks: %d', COUNT(*)) FROM chunks;
SELECT printf('Chunks with embeddings: %d', COUNT(*)) FROM chunk_embeddings;
SELECT printf('Edges: %d', COUNT(*)) FROM edges;
SELECT printf('Chats: %d', COUNT(*)) FROM chats;
SELECT printf('FTS chunks indexed: %d', COUNT(*)) FROM chunks_fts;
SELECT printf('FTS nodes indexed: %d', COUNT(*)) FROM nodes_fts;
-- Database statistics
.print ""
.print "Database Statistics:"
.print "===================="
SELECT printf('Database size: %.2f MB', page_count * page_size / 1024.0 / 1024.0) FROM pragma_page_count(), pragma_page_size();
SELECT printf('Cache size: %.0f MB', cache_size * -1 / 1000.0) FROM pragma_cache_size();
SELECT printf('Journal mode: %s', journal_mode) FROM pragma_journal_mode();
-- Sample data verification
.print ""
.print "Sample data (first 3 nodes):"
.print "============================="
.mode column
.width 10 40
SELECT id, substr(title, 1, 40) as title FROM nodes LIMIT 3;
.print ""
.print "Sample dimensions for node 5:"
.print "=============================="
SELECT dimension FROM node_dimensions WHERE node_id = 5 LIMIT 5;
EOF
echo ""
echo "✅ SQLite database created successfully: rah_trial.db"
echo ""
echo "Database is ready for testing!"
echo "Next steps:"
echo " 1. Install sqlite-vec extension for vector search (optional)"
echo " 2. Run performance benchmarks"
echo " 3. Compare with PostgreSQL baseline"
+306
View File
@@ -0,0 +1,306 @@
#!/usr/bin/env node
/**
* Full pipeline test: Extract facts, match, and persist to memory table
*/
const Database = require('better-sqlite3');
const OpenAI = require('openai');
const path = require('path');
const os = require('os');
const crypto = require('crypto');
const dbPath = path.join(os.homedir(), 'Library/Application Support/RA-H/db/rah.sqlite');
const db = new Database(dbPath);
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
const MODEL = process.env.MEMORY_MODEL || 'gpt-4o-mini';
async function chatJSON(model, system, user, maxTokens = 1500) {
const payload = {
model,
messages: [
{ role: 'system', content: system },
{ role: 'user', content: user }
]
};
if (model.includes('gpt-5')) {
payload.max_completion_tokens = maxTokens;
payload.response_format = { type: 'json_object' };
} else {
payload.temperature = 0.3;
payload.max_tokens = maxTokens;
}
const completion = await openai.chat.completions.create(payload);
const text = completion.choices[0]?.message?.content || '{}';
try {
return JSON.parse(text);
} catch {
const m = text.match(/\{[\s\S]*\}/);
if (m) {
try { return JSON.parse(m[0]); } catch {}
}
throw new Error('LLM did not return valid JSON');
}
}
function getExistingFacts() {
const rows = db.prepare(`
SELECT id, entity_id, content, metadata
FROM memory
WHERE type='big_memory' AND entity_id LIKE 'fact:%'
`).all();
return rows.map(r => {
const meta = safeParse(r.metadata);
return {
id: r.id,
entity_id: r.entity_id,
text: (r.content || '').trim(),
meta: meta
};
});
}
async function extractFacts(batchJson) {
const system = `You are given a JSON array of the last 10100 activity logs. Each entry has id and fields: for chats {id,type:'chat',ts,helper,user,assistant}, for nodes {id,type:'node',ts,title}, for edges {id,type:'edge',ts,from_title,to_title}.
Extract ANY potentially important, durable facts the user explicitly stated or clearly implied — facts that help refine their research/thinking/learning process over time.
Keep each fact atomic and canonical (<=160 chars). Do not invent or generalize beyond evidence.
Acceptable, generic fact types include: identity/role, relationships, goals/projects, interests/domains, learning styles, preferences, beliefs/world model, workflows/tools, constraints/availability, and assets/channels (e.g., podcast/newsletter titles).
Return STRICT JSON only:
{ "facts": [ { "text": string, "explicit": boolean, "sources": [log_id, ...] } ] }
Rules:
- text is a single atomic fact (<=160 chars), canonical phrasing.
- explicit = true only for clear first-person/possessive or labeled statements (e.g., "my…", "I…", "partner named…").
- sources: include 13 representative log ids (from the provided id fields) for each fact.`;
const result = await chatJSON(MODEL, system, batchJson, 1500);
return Array.isArray(result?.facts) ? result.facts : [];
}
async function matchFacts(candidates, existing) {
const system = `Given CANDIDATE facts and EXISTING facts, decide for each candidate whether to:
- REINFORCE one or more existing facts (near-duplicates or same meaning), and/or
- create NEW canonical fact texts when it's genuinely new.
Definitions:
- Similar (reinforce): same meaning with minor wording differences; same named entity with different surface form; specific vs broader phrasing where the core claim aligns.
- New: a distinct fact not covered by any existing entry.
Normalization guidance:
- Prefer simple, canonical phrasing (no quotes unless part of a title).
- Keep <=160 chars; avoid trailing punctuation unless it's part of a title.
- Maintain names/titles/capitalization as they appear; no speculation.
Return STRICT JSON only:
{
"actions": [
{ "candidate": number, "reinforce": [existing_id, ...], "new": [canonical_text, ...] }
]
}
Rules:
- A candidate may reinforce multiple existing items (1→many) if they're all clearly near-duplicates.
- A candidate may also create NEW text(s) if there's a distinct fact not covered by existing items.
- If neither applies (ambiguous/noisy), leave both arrays empty.`;
const payload = {
candidates,
existing_facts: existing.map(e => ({
id: e.entity_id,
text: e.text,
reinforcement_count: e.meta?.reinforcement_count || 0,
reinforcement_score: e.meta?.reinforcement_score || 0
}))
};
try {
const res = await chatJSON(MODEL, system, JSON.stringify(payload), 1500);
if (res && Array.isArray(res.actions)) return { actions: res.actions };
} catch (e) {
console.warn('Match LLM failed, using fallback:', e.message);
}
const actions = candidates.map((c, i) => ({ candidate: i, reinforce: [], new: [c.text] }));
return { actions };
}
function persistMatches(candidates, existing, matchActions) {
const nowIso = new Date().toISOString();
const existingByEntity = new Map(existing.map(e => [e.entity_id, e]));
let reinforced = 0;
let created = 0;
db.transaction(() => {
for (const act of (matchActions.actions || [])) {
const cand = candidates[act.candidate];
if (!cand || !cand.text) continue;
// REINFORCE existing facts
for (const target of (act.reinforce || [])) {
const ex = existingByEntity.get(String(target));
if (!ex) continue;
const meta = ex.meta || {};
const scoreInc = cand.explicit ? 5 : 1;
meta.reinforcement_count = (meta.reinforcement_count || 0) + 1;
meta.reinforcement_score = (meta.reinforcement_score || 0) + scoreInc;
meta.last_seen = nowIso;
meta.first_seen = meta.first_seen || nowIso;
const srcs = new Set(Array.isArray(meta.sources) ? meta.sources : []);
(cand.sources || []).forEach(s => srcs.add(s));
meta.sources = Array.from(srcs).slice(0, 20);
meta.explicit = !!(meta.explicit || cand.explicit);
meta.user_score = meta.user_score || 0;
db.prepare(`UPDATE memory SET metadata = ? WHERE id = ?`)
.run(JSON.stringify(meta), ex.id);
reinforced++;
console.log(` ↑ REINFORCED fact:${ex.entity_id.substring(5, 13)}... (count: ${meta.reinforcement_count}, score: ${meta.reinforcement_score})`);
}
// CREATE new facts
for (const newText of (act.new || [])) {
const txt = String(newText || cand.text).trim().slice(0, 160);
if (!txt) continue;
const meta = {
reinforcement_count: 1,
reinforcement_score: cand.explicit ? 5 : 1,
explicit: !!cand.explicit,
cross_source: false,
sources: (cand.sources || []).slice(0, 20),
first_seen: nowIso,
last_seen: nowIso,
user_score: 0
};
const entity = `fact:${crypto.randomUUID()}`;
db.prepare(`
INSERT INTO memory(type, entity_id, version, content, metadata, is_current)
VALUES('big_memory', ?, 1, ?, ?, 1)
`).run(entity, txt, JSON.stringify(meta));
created++;
console.log(` + NEW fact: "${txt.substring(0, 60)}${txt.length > 60 ? '...' : ''}"`);
}
}
})();
return { reinforced, created };
}
function safeParse(s) {
try { return s ? JSON.parse(s) : {}; } catch { return {}; }
}
async function runFullPipeline() {
console.log('═══════════════════════════════════════════════════════');
console.log('🧪 FULL MEMORY PIPELINE TEST (Last 100 Logs)');
console.log('═══════════════════════════════════════════════════════\n');
// 1. Fetch logs
const batch = db.prepare(`
SELECT id, ts, table_name, action, summary, snapshot_json, chat_helper,
chat_user_full, chat_assistant_full, node_title, edge_from_title, edge_to_title
FROM logs_v ORDER BY id DESC LIMIT 100
`).all().reverse();
console.log(`📊 Fetched ${batch.length} logs (ID ${batch[0].id}${batch[batch.length-1].id})\n`);
// 2. Build batch JSON
const inputJson = [];
for (const r of batch) {
if (r.table_name === 'chats') {
inputJson.push({
id: r.id, type: 'chat', ts: r.ts,
helper: r.chat_helper || null,
user: r.chat_user_full || '',
assistant: r.chat_assistant_full || ''
});
} else if (r.table_name === 'nodes') {
inputJson.push({
id: r.id, type: 'node', ts: r.ts,
title: r.node_title || r.summary || ''
});
} else if (r.table_name === 'edges') {
inputJson.push({
id: r.id, type: 'edge', ts: r.ts,
from_title: r.edge_from_title || '',
to_title: r.edge_to_title || ''
});
}
}
// 3. Get existing facts (before)
const existingBefore = getExistingFacts();
console.log(`📋 Existing facts in DB: ${existingBefore.length}\n`);
// 4. Extract candidates
console.log(`🤖 Extracting facts using ${MODEL}...\n`);
const candidates = await extractFacts(JSON.stringify(inputJson, null, 2));
console.log(`✅ Extracted ${candidates.length} candidate facts:\n`);
candidates.forEach((c, i) => {
console.log(`${i+1}. "${c.text.substring(0, 80)}${c.text.length > 80 ? '...' : ''}"`);
console.log(` Explicit: ${c.explicit}, Sources: [${c.sources.slice(0, 3).join(', ')}]`);
});
console.log();
// 5. Match against existing
console.log('🔍 Matching against existing facts...\n');
const matchActions = await matchFacts(candidates, existingBefore);
console.log(`📝 Match decisions: ${matchActions.actions.length} actions\n`);
// 6. Persist
console.log('💾 Persisting to database...\n');
const { reinforced, created } = persistMatches(candidates, existingBefore, matchActions);
// 7. Get existing facts (after)
const existingAfter = getExistingFacts();
console.log('\n═══════════════════════════════════════════════════════');
console.log('📊 RESULTS');
console.log('═══════════════════════════════════════════════════════\n');
console.log(`Facts before: ${existingBefore.length}`);
console.log(`Facts after: ${existingAfter.length}`);
console.log(`Reinforced: ${reinforced}`);
console.log(`Created: ${created}`);
console.log();
// 8. Show top facts by score
const sorted = existingAfter
.map(f => ({
text: f.text,
count: f.meta?.reinforcement_count || 0,
score: f.meta?.reinforcement_score || 0
}))
.sort((a, b) => b.score - a.score)
.slice(0, 10);
console.log('🏆 Top 10 facts by score:\n');
sorted.forEach((f, i) => {
console.log(`${i+1}. [score: ${f.score}, count: ${f.count}]`);
console.log(` "${f.text}"`);
console.log();
});
db.close();
console.log('✅ Pipeline complete!\n');
}
runFullPipeline().catch(e => {
console.error('❌ Pipeline failed:', e.message);
console.error(e.stack);
db.close();
process.exit(1);
});
+164
View File
@@ -0,0 +1,164 @@
#!/usr/bin/env node
/**
* Standalone test script to verify memory extraction on last 100 logs
*/
const Database = require('better-sqlite3');
const OpenAI = require('openai');
const path = require('path');
const os = require('os');
const dbPath = path.join(os.homedir(), 'Library/Application Support/RA-H/db/rah.sqlite');
const db = new Database(dbPath);
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
async function chatJSON(model, system, user, maxTokens = 1500) {
const payload = {
model,
messages: [
{ role: 'system', content: system },
{ role: 'user', content: user }
]
};
if (model.includes('gpt-5')) {
payload.max_completion_tokens = maxTokens;
payload.response_format = { type: 'json_object' };
} else {
payload.temperature = 0.3;
payload.max_tokens = maxTokens;
}
const completion = await openai.chat.completions.create(payload);
const text = completion.choices[0]?.message?.content || '{}';
try {
return JSON.parse(text);
} catch {
const m = text.match(/\{[\s\S]*\}/);
if (m) {
try { return JSON.parse(m[0]); } catch {}
}
throw new Error('LLM did not return valid JSON');
}
}
async function testExtraction() {
console.log('📊 Testing memory extraction on last 100 logs\n');
// Get last 100 logs
const batch = db.prepare(`
SELECT id, ts, table_name, action, summary, snapshot_json, chat_helper,
chat_user_full, chat_assistant_full, node_title, edge_from_title, edge_to_title
FROM logs_v ORDER BY id DESC LIMIT 100
`).all().reverse();
console.log(`✅ Fetched ${batch.length} logs (ID ${batch[0].id}${batch[batch.length-1].id})\n`);
// Build input JSON
const inputJson = [];
for (const r of batch) {
if (r.table_name === 'chats') {
inputJson.push({
id: r.id,
type: 'chat',
ts: r.ts,
helper: r.chat_helper || null,
user: r.chat_user_full || '',
assistant: r.chat_assistant_full || ''
});
} else if (r.table_name === 'nodes') {
inputJson.push({
id: r.id,
type: 'node',
ts: r.ts,
title: r.node_title || r.summary || ''
});
} else if (r.table_name === 'edges') {
inputJson.push({
id: r.id,
type: 'edge',
ts: r.ts,
from_title: r.edge_from_title || '',
to_title: r.edge_to_title || ''
});
}
}
console.log('📝 Sample entries:');
console.log('First:', JSON.stringify(inputJson[0], null, 2));
console.log('Last:', JSON.stringify(inputJson[inputJson.length-1], null, 2));
console.log();
// Look for Paige mentions
const paigeLogs = inputJson.filter(e =>
e.type === 'chat' && (e.user.toLowerCase().includes('paige') || e.assistant.toLowerCase().includes('paige'))
);
console.log(`🔍 Found ${paigeLogs.length} entries mentioning "paige":`);
paigeLogs.forEach(log => {
console.log(` ID ${log.id}: "${log.user.substring(0, 60)}..."`);
});
console.log();
// Extract facts
const MODEL = process.env.MEMORY_MODEL || 'gpt-4o-mini';
console.log(`🤖 Using model: ${MODEL}\n`);
const system = `You are given a JSON array of the last 10100 activity logs. Each entry has id and fields: for chats {id,type:'chat',ts,helper,user,assistant}, for nodes {id,type:'node',ts,title}, for edges {id,type:'edge',ts,from_title,to_title}.
Extract ANY potentially important, durable facts the user explicitly stated or clearly implied — facts that help refine their research/thinking/learning process over time.
Keep each fact atomic and canonical (<=160 chars). Do not invent or generalize beyond evidence.
Acceptable, generic fact types include: identity/role, relationships, goals/projects, interests/domains, learning styles, preferences, beliefs/world model, workflows/tools, constraints/availability, and assets/channels (e.g., podcast/newsletter titles).
Return STRICT JSON only:
{ "facts": [ { "text": string, "explicit": boolean, "sources": [log_id, ...] } ] }
Rules:
- text is a single atomic fact (<=160 chars), canonical phrasing.
- explicit = true only for clear first-person/possessive or labeled statements (e.g., "my…", "I…", "partner named…").
- sources: include 13 representative log ids (from the provided id fields) for each fact.`;
try {
console.log('⏳ Calling LLM...\n');
const result = await chatJSON(MODEL, system, JSON.stringify(inputJson, null, 2), 1500);
console.log('════════════════════════════════════════');
console.log('📋 EXTRACTION RESULT');
console.log('════════════════════════════════════════\n');
if (!result || !result.facts || result.facts.length === 0) {
console.log('❌ NO FACTS EXTRACTED\n');
console.log('Raw response:', JSON.stringify(result, null, 2));
return;
}
console.log(`✅ Extracted ${result.facts.length} facts:\n`);
result.facts.forEach((f, i) => {
console.log(`${i+1}. "${f.text}"`);
console.log(` Explicit: ${f.explicit}`);
console.log(` Sources: [${f.sources.join(', ')}]`);
console.log();
});
// Check for Paige facts
const paigeFacts = result.facts.filter(f =>
f.text.toLowerCase().includes('paige')
);
if (paigeFacts.length > 0) {
console.log(`🎯 Found ${paigeFacts.length} fact(s) about Paige!`);
} else {
console.log(`⚠️ No facts extracted about Paige (despite ${paigeLogs.length} mentions in logs)`);
}
} catch (e) {
console.error('❌ ERROR:', e.message);
console.error(e.stack);
} finally {
db.close();
}
}
testExtraction().catch(console.error);
+95
View File
@@ -0,0 +1,95 @@
#!/usr/bin/env node
/**
* Test script for memory UPDATE operation
*
* This script:
* 1. Shows current memory facts
* 2. Triggers the memory pipeline manually
* 3. Shows updated facts after processing
*/
const Database = require('better-sqlite3');
const path = require('path');
const os = require('os');
const dbPath = path.join(os.homedir(), 'Library/Application Support/RA-H/db/rah.sqlite');
const db = new Database(dbPath);
console.log('=== MEMORY UPDATE OPERATION TEST ===\n');
// 1. Show current facts
console.log('📊 CURRENT MEMORY FACTS:\n');
const currentFacts = db.prepare(`
SELECT
id,
entity_id,
content,
json_extract(metadata, '$.reinforcement_count') as count,
json_extract(metadata, '$.reinforcement_score') as score,
json_extract(metadata, '$.entities') as entities
FROM memory
WHERE type='big_memory' AND entity_id LIKE 'fact:%' AND is_current=1
ORDER BY json_extract(metadata, '$.reinforcement_score') DESC
`).all();
currentFacts.forEach((fact, i) => {
console.log(`${i + 1}. [Score: ${fact.score}, Count: ${fact.count}]`);
console.log(` "${fact.content}"`);
if (fact.entities) {
try {
const entities = JSON.parse(fact.entities);
if (entities && entities.length > 0) {
console.log(` Entities: ${entities.join(', ')}`);
}
} catch {}
}
console.log();
});
// 2. Check pipeline state
console.log('\n📈 PIPELINE STATE:\n');
const pipelineState = db.prepare(`
SELECT last_processed_log_id, last_run_at
FROM memory_pipeline_state
WHERE id=1
`).get();
const maxLogId = db.prepare('SELECT MAX(id) as max FROM logs').get();
console.log(`Last processed log: ${pipelineState?.last_processed_log_id || 0}`);
console.log(`Max log ID: ${maxLogId?.max || 0}`);
console.log(`Logs pending: ${(maxLogId?.max || 0) - (pipelineState?.last_processed_log_id || 0)}`);
console.log(`Last run: ${pipelineState?.last_run_at || 'never'}\n`);
// 3. Show recent logs that mention key entities
console.log('\n📝 RECENT LOGS (mentioning ra-h, Paige, knowledge management):\n');
const recentLogs = db.prepare(`
SELECT id, ts, table_name, summary, chat_user_full
FROM logs_v
WHERE
(chat_user_full LIKE '%ra-h%' OR
chat_user_full LIKE '%Paige%' OR
chat_user_full LIKE '%knowledge management%' OR
chat_user_full LIKE '%dogfood%' OR
chat_user_full LIKE '%beta%' OR
summary LIKE '%ra-h%' OR
summary LIKE '%Paige%')
ORDER BY id DESC
LIMIT 15
`).all();
recentLogs.forEach(log => {
const preview = log.chat_user_full
? log.chat_user_full.substring(0, 100)
: log.summary.substring(0, 100);
console.log(`Log ${log.id} [${log.ts}]: ${preview}${preview.length >= 100 ? '...' : ''}`);
});
console.log('\n✅ To trigger pipeline manually:');
console.log(' 1. Start dev server: npm run dev');
console.log(' 2. Create 10+ new logs (chat, create nodes, etc.)');
console.log(' 3. Pipeline will auto-run and show UPDATE operations in console (if MEMORY_DEBUG=1)');
console.log('\n Or run this script again after interacting with the app to see updated facts.\n');
db.close();
+225
View File
@@ -0,0 +1,225 @@
#!/usr/bin/env node
/**
* Test script for Python to TypeScript migration
* Tests extractors and embeddings using TypeScript modules
*/
const fs = require('fs');
const path = require('path');
const Module = require('module');
const ts = require('typescript');
const { getSubtitles } = require('youtube-captions-scraper');
function isNetworkError(error) {
if (!error) return false;
const message = typeof error === 'string' ? error : error.message || '';
const code = error.code || '';
return [
'fetch failed',
'ENOTFOUND',
'ECONNRESET',
'EAI_AGAIN',
'ECONNREFUSED',
'ETIMEDOUT'
].some(token => message.includes(token)) || ['ENOTFOUND', 'ECONNRESET', 'EAI_AGAIN', 'ECONNREFUSED', 'ETIMEDOUT'].includes(code);
}
function requireTs(modulePath) {
const resolved = path.resolve(__dirname, '..', modulePath);
const source = fs.readFileSync(resolved, 'utf8');
const transpiled = ts.transpileModule(source, {
compilerOptions: {
module: ts.ModuleKind.CommonJS,
target: ts.ScriptTarget.ES2020,
esModuleInterop: true,
moduleResolution: ts.ModuleResolutionKind.Node16,
resolveJsonModule: true,
skipLibCheck: true
},
fileName: resolved
});
const tempModule = new Module(resolved, module);
tempModule.filename = resolved;
tempModule.paths = Module._nodeModulePaths(path.dirname(resolved));
tempModule._compile(transpiled.outputText, resolved);
return tempModule.exports;
}
async function testTranscriptLibrary() {
console.log('🔍 Testing youtube-captions-scraper library directly...');
const testUrls = [
'dQw4w9WgXcQ', // Rick Roll
'MnrJzXM7a6o', // TED Talk
'jNQXAC9IVRw' // Popular video
];
let sawNetworkError = false;
let sawEmptyTranscript = false;
for (const videoId of testUrls) {
console.log(`\nTesting video: ${videoId}`);
try {
const transcript = await getSubtitles({ videoID: videoId });
if (!transcript || transcript.length === 0) {
sawEmptyTranscript = true;
console.log('⚠️ Library returned 0 segments (likely geo-blocked). Trying next video...');
continue;
}
console.log(`✅ Success! Got ${transcript.length} segments`);
console.log('First segment:', transcript[0]);
console.log('Sample formatted:', `[${transcript[0].start.toFixed(1)}s] ${transcript[0].text}`);
return 'passed'; // Exit on first success
} catch (error) {
if (isNetworkError(error)) {
sawNetworkError = true;
console.log('⚠️ Network unavailable, skipping this video');
} else {
console.log('❌ Failed:', error.message);
}
}
}
if (sawNetworkError) {
console.log('\n⚠️ Network unavailable for transcript tests, marking as SKIPPED');
return 'skipped';
}
if (sawEmptyTranscript) {
console.log('\n⚠️ Library could not fetch transcripts for any sample video (likely YouTube changes). Marking as SKIPPED.');
return 'skipped';
}
console.log('\n❌ No transcripts could be extracted from any test video');
return 'failed';
}
console.log('🧪 Testing TypeScript Migration...\n');
async function testYouTubeExtractor() {
console.log('📺 Testing YouTube Extractor...');
try {
const { extractYouTube } = requireTs('src/services/typescript/extractors/youtube.ts');
const result = await extractYouTube('https://www.youtube.com/watch?v=dQw4w9WgXcQ');
console.log('✅ YouTube extraction successful');
console.log(` Title: ${result.metadata.title}`);
console.log(` Channel: ${result.metadata.author_name}`);
console.log(` Transcript length: ${result.chunk.length} chars\n`);
return 'passed';
} catch (error) {
if (isNetworkError(error)) {
console.warn('⚠️ Network unavailable, skipping YouTube extractor test\n');
return 'skipped';
}
console.error('❌ YouTube extraction failed:', error.message, '\n');
return 'failed';
}
}
async function testWebsiteExtractor() {
console.log('🌐 Testing Website Extractor...');
try {
const { extractWebsite } = requireTs('src/services/typescript/extractors/website.ts');
const result = await extractWebsite('https://example.com');
console.log('✅ Website extraction successful');
console.log(` Title: ${result.metadata.title}`);
console.log(` Content length: ${result.chunk.length} chars\n`);
return 'passed';
} catch (error) {
if (isNetworkError(error)) {
console.warn('⚠️ Network unavailable, skipping website extractor test\n');
return 'skipped';
}
console.error('❌ Website extraction failed:', error.message, '\n');
return 'failed';
}
}
async function testPDFExtractor() {
console.log('📄 Testing PDF Extractor...');
try {
const { extractPaper } = requireTs('src/services/typescript/extractors/paper.ts');
// Using a sample PDF URL
const samplePdf = 'https://raw.githubusercontent.com/mozilla/pdf.js/master/web/compressed.tracemonkey-pldi-09.pdf';
const result = await extractPaper(samplePdf);
console.log('✅ PDF extraction successful');
console.log(` Pages: ${result.metadata.pages}`);
console.log(` Text length: ${result.metadata.text_length} chars\n`);
return 'passed';
} catch (error) {
if (isNetworkError(error)) {
console.warn('⚠️ Network unavailable, skipping PDF extractor test\n');
return 'skipped';
}
console.error('❌ PDF extraction failed:', error.message, '\n');
return 'failed';
}
}
async function testNodeEmbedding() {
console.log('🔮 Testing Node Embedding...');
try {
// This would require a real node ID and database connection
console.log('⚠️ Skipping node embedding test (requires database and API key)\n');
return 'skipped';
} catch (error) {
console.error('❌ Node embedding failed:', error.message, '\n');
return 'failed';
}
}
async function runTests() {
console.log('Environment:');
console.log(` Node version: ${process.version}\n`);
// First test the library directly
const libraryStatus = await testTranscriptLibrary();
const results = {
library: libraryStatus,
youtube: await testYouTubeExtractor(),
website: await testWebsiteExtractor(),
pdf: await testPDFExtractor(),
embedding: await testNodeEmbedding()
};
console.log('📊 Test Results:');
console.log('─────────────────');
let passed = 0;
let failed = 0;
let skipped = 0;
for (const [test, result] of Object.entries(results)) {
if (result === 'passed') {
console.log(` ${test}: ✅ PASSED`);
passed++;
} else if (result === 'skipped') {
console.log(` ${test}: ⚠️ SKIPPED`);
skipped++;
} else {
console.log(` ${test}: ❌ FAILED`);
failed++;
}
}
console.log('\n📈 Summary:');
console.log(` Passed: ${passed}`);
console.log(` Failed: ${failed}`);
console.log(` Skipped: ${skipped}`);
if (failed === 0) {
console.log('\n🎉 No hard failures. Re-run skipped tests when network/database access is available.');
} else {
console.log('\n⚠️ Some tests failed. Check the errors above.');
}
process.exit(failed > 0 ? 1 : 0);
}
// Run tests
runTests().catch(error => {
console.error('Fatal error:', error);
process.exit(1);
});
+66
View File
@@ -0,0 +1,66 @@
#!/usr/bin/env bash
set -euo pipefail
# Smoke test the universal binaries by loading them under both architectures.
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
DIST_ROOT="${DIST_ROOT:-$REPO_ROOT/dist/local-app}"
APP_DIR="$DIST_ROOT/app"
NODE_BIN="$DIST_ROOT/bin/node"
assert_exists() {
local path="$1"
if [ ! -e "$path" ]; then
echo "❌ Missing required artifact: $path" >&2
exit 1
fi
}
check_universal() {
local path="$1"
if ! file "$path" | grep -q "arm64"; then
echo "$path is missing arm64 slice" >&2
exit 1
fi
if ! file "$path" | grep -q "x86_64"; then
echo "$path is missing x86_64 slice" >&2
exit 1
fi
}
run_dual_arch_node() {
local arch="$1"
local script="$2"
if [ "$arch" = "x86_64" ]; then
arch -x86_64 "$NODE_BIN" -e "$script"
else
arch -arm64 "$NODE_BIN" -e "$script"
fi
}
main() {
assert_exists "$NODE_BIN"
assert_exists "$APP_DIR/node_modules/better-sqlite3/build/Release/better_sqlite3.node"
assert_exists "$DIST_ROOT/vendor/sqlite-extensions/vec0.dylib"
echo "️ Verifying universal slices"
check_universal "$NODE_BIN"
check_universal "$APP_DIR/node_modules/better-sqlite3/build/Release/better_sqlite3.node"
check_universal "$DIST_ROOT/vendor/sqlite-extensions/vec0.dylib"
echo "️ Running runtime smoke tests"
run_dual_arch_node arm64 "console.log('arm64 runtime ok')"
run_dual_arch_node x86_64 "console.log('x86 runtime ok')"
(
cd "$APP_DIR"
run_dual_arch_node arm64 "require('./node_modules/better-sqlite3'); console.log('better-sqlite3 arm64 ok')"
run_dual_arch_node x86_64 "require('./node_modules/better-sqlite3'); console.log('better-sqlite3 x86 ok')"
)
echo "✅ Universal build smoke test passed"
}
main "$@"
+20
View File
@@ -0,0 +1,20 @@
const Database = require('better-sqlite3');
const path = require('path');
const dbPath = process.argv[2];
if (!dbPath) {
console.error('Usage: node tmp-init-vec.js <dbPath>');
process.exit(1);
}
const db = new Database(dbPath);
const vecPath = path.join(process.cwd(), 'vendor', 'sqlite-extensions', 'vec0.dylib');
db.loadExtension(vecPath);
db.exec(`
CREATE VIRTUAL TABLE IF NOT EXISTS vec_nodes USING vec0(node_id INTEGER PRIMARY KEY, embedding FLOAT[1536]);
CREATE VIRTUAL TABLE IF NOT EXISTS vec_chunks USING vec0(chunk_id INTEGER PRIMARY KEY, embedding FLOAT[1536]);
`);
console.log('✓ vec tables ensured');
db.close();
+3
View File
@@ -0,0 +1,3 @@
#!/usr/bin/env bash
echo "Xcode 15.0"
echo "Build version 15A000"