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:
@@ -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
|
||||
Executable
+286
@@ -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 "$@"
|
||||
Executable
+580
@@ -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 "$@"
|
||||
Executable
+137
@@ -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 "$@"
|
||||
@@ -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);
|
||||
Executable
+20
@@ -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"
|
||||
@@ -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)"
|
||||
Executable
+579
@@ -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 3–5 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."
|
||||
@@ -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."
|
||||
@@ -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"
|
||||
|
||||
Executable
+39
@@ -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"
|
||||
Executable
+56
@@ -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."
|
||||
Executable
+38
@@ -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"
|
||||
@@ -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();
|
||||
Executable
+47
@@ -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
|
||||
Executable
+132
@@ -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>
|
||||
|
||||
Executable
+57
@@ -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"
|
||||
Executable
+148
@@ -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!"
|
||||
Executable
+115
@@ -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"
|
||||
@@ -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 10–100 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 1–3 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);
|
||||
});
|
||||
@@ -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 10–100 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 1–3 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);
|
||||
Executable
+95
@@ -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();
|
||||
Executable
+225
@@ -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);
|
||||
});
|
||||
Executable
+66
@@ -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 "$@"
|
||||
@@ -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();
|
||||
Executable
+3
@@ -0,0 +1,3 @@
|
||||
#!/usr/bin/env bash
|
||||
echo "Xcode 15.0"
|
||||
echo "Build version 15A000"
|
||||
Reference in New Issue
Block a user