Initial commit

This commit is contained in:
ilya-bov
2026-02-25 16:14:15 +03:00
commit 75ab0cecf0
254 changed files with 113531 additions and 0 deletions

216
scripts/install-docker.sh Executable file
View File

@@ -0,0 +1,216 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
ENV_FILE="$ROOT_DIR/.env"
ENV_EXAMPLE_FILE="$ROOT_DIR/.env.example"
DOCKER_BIN="${DOCKER_BIN:-docker}"
read -r -a DOCKER_CMD <<<"$DOCKER_BIN"
require_cmd() {
if ! command -v "$1" >/dev/null 2>&1; then
echo "Missing dependency: $1" >&2
exit 1
fi
}
upsert_env() {
local file="$1"
local key="$2"
local value="$3"
local tmp found line
tmp="$(mktemp)"
found=0
if [[ -f "$file" ]]; then
while IFS= read -r line || [[ -n "$line" ]]; do
if [[ "$line" == "$key="* ]]; then
printf '%s=%s\n' "$key" "$value" >>"$tmp"
found=1
else
printf '%s\n' "$line" >>"$tmp"
fi
done <"$file"
fi
if [[ "$found" -eq 0 ]]; then
printf '%s=%s\n' "$key" "$value" >>"$tmp"
fi
mv "$tmp" "$file"
}
get_env_value() {
local file="$1"
local key="$2"
if [[ ! -f "$file" ]]; then
return 0
fi
grep -E "^${key}=" "$file" | tail -n 1 | cut -d= -f2- || true
}
looks_placeholder() {
local value="$1"
if [[ -z "$value" ]]; then
return 0
fi
case "$value" in
replace-with-* | *replace-with* | changeme | example | ... )
return 0
;;
*)
return 1
;;
esac
}
random_hex() {
local bytes="${1:-32}"
if command -v openssl >/dev/null 2>&1; then
openssl rand -hex "$bytes"
return 0
fi
node -e "process.stdout.write(require('node:crypto').randomBytes(${bytes}).toString('hex'))"
}
wait_for_health() {
local url="$1"
local retries="$2"
local delay="$3"
local i
for i in $(seq 1 "$retries"); do
if command -v curl >/dev/null 2>&1; then
if curl --silent --show-error --fail "$url" >/dev/null 2>&1; then
return 0
fi
else
if node -e "fetch(process.argv[1]).then((r)=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))" "$url"; then
return 0
fi
fi
sleep "$delay"
done
return 1
}
docker_cmd() {
"${DOCKER_CMD[@]}" "$@"
}
prepare_data_dir() {
local data_dir="$ROOT_DIR/data"
mkdir -p "$data_dir"
# The runtime container runs as user "node" (uid/gid 1000).
# If setup is executed as root, fix bind-mount ownership to avoid EACCES at runtime.
if [[ "$(id -u)" -eq 0 ]]; then
chown -R 1000:1000 "$data_dir" 2>/dev/null || true
fi
if [[ ! -w "$data_dir" ]]; then
echo "WARNING: $data_dir is not writable. App may fail with 500 on /dashboard." >&2
echo "Run: sudo chown -R 1000:1000 $data_dir" >&2
fi
}
ensure_data_dir_writable_for_runtime() {
local data_dir="$ROOT_DIR/data"
if docker_cmd run --rm --user 1000:1000 -v "$data_dir:/target" eggent:local \
sh -lc "test -w /target" >/dev/null 2>&1; then
return 0
fi
docker_cmd run --rm --user 0:0 -v "$data_dir:/target" eggent:local \
sh -lc "chown -R 1000:1000 /target" >/dev/null 2>&1 || true
if docker_cmd run --rm --user 1000:1000 -v "$data_dir:/target" eggent:local \
sh -lc "test -w /target" >/dev/null 2>&1; then
return 0
fi
echo "ERROR: data directory is not writable for runtime user (uid 1000)." >&2
echo "Fix and rerun:" >&2
echo " sudo chown -R 1000:1000 $data_dir" >&2
exit 1
}
echo "==> Docker setup (isolated)"
require_cmd "${DOCKER_CMD[0]}"
if ! docker_cmd compose version >/dev/null 2>&1; then
echo "Docker Compose v2 is required (docker compose ...)." >&2
exit 1
fi
cd "$ROOT_DIR"
if [[ ! -f "$ENV_FILE" ]]; then
if [[ ! -f "$ENV_EXAMPLE_FILE" ]]; then
echo "Missing template file: $ENV_EXAMPLE_FILE" >&2
exit 1
fi
cp "$ENV_EXAMPLE_FILE" "$ENV_FILE"
echo "Created .env from .env.example"
fi
EXTERNAL_API_TOKEN_VALUE="$(get_env_value "$ENV_FILE" "EXTERNAL_API_TOKEN")"
if looks_placeholder "$EXTERNAL_API_TOKEN_VALUE"; then
upsert_env "$ENV_FILE" "EXTERNAL_API_TOKEN" "$(random_hex 32)"
echo "Generated EXTERNAL_API_TOKEN in .env"
fi
TELEGRAM_WEBHOOK_SECRET_VALUE="$(get_env_value "$ENV_FILE" "TELEGRAM_WEBHOOK_SECRET")"
if looks_placeholder "$TELEGRAM_WEBHOOK_SECRET_VALUE"; then
upsert_env "$ENV_FILE" "TELEGRAM_WEBHOOK_SECRET" "$(random_hex 24)"
echo "Generated TELEGRAM_WEBHOOK_SECRET in .env"
fi
EGGENT_AUTH_SECRET_VALUE="$(get_env_value "$ENV_FILE" "EGGENT_AUTH_SECRET")"
if looks_placeholder "$EGGENT_AUTH_SECRET_VALUE"; then
upsert_env "$ENV_FILE" "EGGENT_AUTH_SECRET" "$(random_hex 32)"
echo "Generated EGGENT_AUTH_SECRET in .env"
fi
chmod 600 "$ENV_FILE" 2>/dev/null || true
prepare_data_dir
APP_PORT="${APP_PORT:-$(get_env_value "$ENV_FILE" "APP_PORT")}"
APP_PORT="${APP_PORT:-3000}"
APP_BIND_HOST="${APP_BIND_HOST:-$(get_env_value "$ENV_FILE" "APP_BIND_HOST")}"
APP_BIND_HOST="${APP_BIND_HOST:-127.0.0.1}"
upsert_env "$ENV_FILE" "APP_PORT" "$APP_PORT"
upsert_env "$ENV_FILE" "APP_BIND_HOST" "$APP_BIND_HOST"
HEALTH_URL="http://127.0.0.1:${APP_PORT}/api/health"
echo "==> Building image"
docker_cmd compose build app
echo "==> Verifying data directory permissions"
ensure_data_dir_writable_for_runtime
echo "==> Starting container"
docker_cmd compose up -d app
echo "==> Waiting for health: $HEALTH_URL"
if ! wait_for_health "$HEALTH_URL" 90 1; then
echo "Container did not become healthy. Recent logs:" >&2
docker_cmd compose logs --tail 120 app >&2 || true
exit 1
fi
echo ""
echo "Docker setup complete."
echo "App URL:"
if [[ "$APP_BIND_HOST" == "0.0.0.0" ]]; then
echo " http://<server-ip>:${APP_PORT}"
else
echo " http://localhost:${APP_PORT}"
fi
echo ""
echo "Useful commands:"
echo " docker compose logs -f app"
echo " docker compose restart app"
echo " docker compose down"

195
scripts/install-local.sh Executable file
View File

@@ -0,0 +1,195 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
ENV_FILE="$ROOT_DIR/.env"
ENV_EXAMPLE_FILE="$ROOT_DIR/.env.example"
require_cmd() {
if ! command -v "$1" >/dev/null 2>&1; then
echo "Missing dependency: $1" >&2
exit 1
fi
}
warn_if_missing_cmd() {
if ! command -v "$1" >/dev/null 2>&1; then
echo "WARNING: Recommended utility is missing: $1" >&2
fi
}
check_node_version() {
local version major
version="$(node -p "process.versions.node")"
major="${version%%.*}"
if [[ "$major" -lt 20 ]]; then
echo "Node.js 20+ is required (found $version)." >&2
exit 1
fi
}
upsert_env() {
local file="$1"
local key="$2"
local value="$3"
local tmp found line
tmp="$(mktemp)"
found=0
if [[ -f "$file" ]]; then
while IFS= read -r line || [[ -n "$line" ]]; do
if [[ "$line" == "$key="* ]]; then
printf '%s=%s\n' "$key" "$value" >>"$tmp"
found=1
else
printf '%s\n' "$line" >>"$tmp"
fi
done <"$file"
fi
if [[ "$found" -eq 0 ]]; then
printf '%s=%s\n' "$key" "$value" >>"$tmp"
fi
mv "$tmp" "$file"
}
get_env_value() {
local file="$1"
local key="$2"
if [[ ! -f "$file" ]]; then
return 0
fi
grep -E "^${key}=" "$file" | tail -n 1 | cut -d= -f2- || true
}
looks_placeholder() {
local value="$1"
if [[ -z "$value" ]]; then
return 0
fi
case "$value" in
replace-with-* | *replace-with* | changeme | example | ... )
return 0
;;
*)
return 1
;;
esac
}
random_hex() {
local bytes="${1:-32}"
if command -v openssl >/dev/null 2>&1; then
openssl rand -hex "$bytes"
return 0
fi
node -e "process.stdout.write(require('node:crypto').randomBytes(${bytes}).toString('hex'))"
}
wait_for_health() {
local url="$1"
local retries="$2"
local delay="$3"
local i
for i in $(seq 1 "$retries"); do
if command -v curl >/dev/null 2>&1; then
if curl --silent --show-error --fail "$url" >/dev/null 2>&1; then
return 0
fi
else
if node -e "fetch(process.argv[1]).then((r)=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))" "$url"; then
return 0
fi
fi
sleep "$delay"
done
return 1
}
echo "==> Local production setup (npm)"
require_cmd node
require_cmd npm
require_cmd python3
require_cmd curl
check_node_version
warn_if_missing_cmd git
warn_if_missing_cmd jq
warn_if_missing_cmd pip3
warn_if_missing_cmd rg
cd "$ROOT_DIR"
if [[ ! -f "$ENV_FILE" ]]; then
if [[ ! -f "$ENV_EXAMPLE_FILE" ]]; then
echo "Missing template file: $ENV_EXAMPLE_FILE" >&2
exit 1
fi
cp "$ENV_EXAMPLE_FILE" "$ENV_FILE"
echo "Created .env from .env.example"
fi
EXTERNAL_API_TOKEN_VALUE="$(get_env_value "$ENV_FILE" "EXTERNAL_API_TOKEN")"
if looks_placeholder "$EXTERNAL_API_TOKEN_VALUE"; then
upsert_env "$ENV_FILE" "EXTERNAL_API_TOKEN" "$(random_hex 32)"
echo "Generated EXTERNAL_API_TOKEN in .env"
fi
TELEGRAM_WEBHOOK_SECRET_VALUE="$(get_env_value "$ENV_FILE" "TELEGRAM_WEBHOOK_SECRET")"
if looks_placeholder "$TELEGRAM_WEBHOOK_SECRET_VALUE"; then
upsert_env "$ENV_FILE" "TELEGRAM_WEBHOOK_SECRET" "$(random_hex 24)"
echo "Generated TELEGRAM_WEBHOOK_SECRET in .env"
fi
EGGENT_AUTH_SECRET_VALUE="$(get_env_value "$ENV_FILE" "EGGENT_AUTH_SECRET")"
if looks_placeholder "$EGGENT_AUTH_SECRET_VALUE"; then
upsert_env "$ENV_FILE" "EGGENT_AUTH_SECRET" "$(random_hex 32)"
echo "Generated EGGENT_AUTH_SECRET in .env"
fi
chmod 600 "$ENV_FILE" 2>/dev/null || true
mkdir -p "$ROOT_DIR/data"
echo "==> Installing dependencies"
npm install --no-package-lock
echo "==> Building production bundle"
npm run build
HEALTH_PORT="${HEALTH_PORT:-3077}"
HEALTH_URL="http://127.0.0.1:${HEALTH_PORT}/api/health"
START_LOG="$ROOT_DIR/.install-local-start.log"
echo "==> Running smoke check: $HEALTH_URL"
PORT="$HEALTH_PORT" HOSTNAME="127.0.0.1" npm run start >"$START_LOG" 2>&1 &
START_PID=$!
cleanup() {
if kill -0 "$START_PID" >/dev/null 2>&1; then
kill "$START_PID" >/dev/null 2>&1 || true
wait "$START_PID" >/dev/null 2>&1 || true
fi
}
trap cleanup EXIT
if ! wait_for_health "$HEALTH_URL" 45 1; then
echo "Smoke check failed. Server log tail:" >&2
tail -n 120 "$START_LOG" >&2 || true
exit 1
fi
rm -f "$START_LOG"
cleanup
trap - EXIT
echo ""
echo "Setup complete."
echo "Run in production mode:"
echo " npm run start"
echo ""
echo "App URL:"
echo " http://127.0.0.1:3000"

229
scripts/install.sh Executable file
View File

@@ -0,0 +1,229 @@
#!/usr/bin/env bash
set -euo pipefail
REPO_URL="${EGGENT_REPO_URL:-https://github.com/eggent-ai/eggent.git}"
BRANCH="${EGGENT_BRANCH:-main}"
INSTALL_DIR="${EGGENT_INSTALL_DIR:-$HOME/.eggent}"
AUTO_INSTALL_DOCKER="${EGGENT_AUTO_INSTALL_DOCKER:-1}"
log() {
printf '%s\n' "$*"
}
fail() {
printf 'ERROR: %s\n' "$*" >&2
exit 1
}
run_root() {
if [[ "$(id -u)" -eq 0 ]]; then
"$@"
else
if ! command -v sudo >/dev/null 2>&1; then
fail "sudo is required to install system packages"
fi
sudo "$@"
fi
}
command_exists() {
command -v "$1" >/dev/null 2>&1
}
docker_compose_ready() {
if command_exists docker && docker compose version >/dev/null 2>&1; then
return 0
fi
if command_exists sudo && command_exists docker && sudo docker compose version >/dev/null 2>&1; then
return 0
fi
return 1
}
install_curl_if_missing() {
if command_exists curl; then
return
fi
if command_exists apt-get; then
run_root apt-get update
run_root apt-get install -y curl
elif command_exists dnf; then
run_root dnf install -y curl
elif command_exists yum; then
run_root yum install -y curl
else
fail "curl is required for Docker fallback install"
fi
}
install_docker_via_official_script() {
local tmp_script
install_curl_if_missing
tmp_script="$(mktemp)"
curl -fsSL https://get.docker.com -o "$tmp_script"
run_root sh "$tmp_script"
rm -f "$tmp_script"
}
install_git_if_missing() {
if command_exists git; then
return
fi
local os
os="$(uname -s)"
log "==> Installing git"
case "$os" in
Darwin)
command_exists brew || fail "Homebrew is required to install git automatically"
brew install git
;;
Linux)
if command_exists apt-get; then
run_root apt-get update
run_root apt-get install -y git
elif command_exists dnf; then
run_root dnf install -y git
elif command_exists yum; then
run_root yum install -y git
else
fail "Unsupported Linux package manager for git auto-install"
fi
;;
*)
fail "Unsupported OS: $os"
;;
esac
}
install_docker_if_missing() {
if docker_compose_ready; then
return
fi
if [[ "$AUTO_INSTALL_DOCKER" != "1" ]]; then
fail "Docker is not installed. Install Docker manually and rerun."
fi
local os
os="$(uname -s)"
log "==> Installing Docker (best-effort)"
case "$os" in
Darwin)
command_exists brew || fail "Homebrew is required for automatic Docker install on macOS"
brew install --cask docker
if command_exists open; then
open -a Docker >/dev/null 2>&1 || true
fi
;;
Linux)
if command_exists apt-get; then
run_root apt-get update
if ! run_root apt-get install -y docker.io docker-compose-plugin; then
log "==> docker-compose-plugin is unavailable, trying docker-compose-v2"
if ! run_root apt-get install -y docker.io docker-compose-v2; then
log "==> distro Docker packages did not provide Compose v2"
fi
fi
elif command_exists dnf; then
run_root dnf install -y docker docker-compose-plugin || true
elif command_exists yum; then
run_root yum install -y docker docker-compose-plugin || true
else
fail "Unsupported Linux package manager for Docker auto-install"
fi
if command_exists systemctl; then
run_root systemctl enable --now docker >/dev/null 2>&1 || true
fi
if ! docker_compose_ready; then
log "==> Docker Compose v2 is still unavailable, trying official Docker installer"
install_docker_via_official_script
if command_exists systemctl; then
run_root systemctl enable --now docker >/dev/null 2>&1 || true
fi
fi
;;
*)
fail "Unsupported OS: $os"
;;
esac
if ! docker_compose_ready; then
fail "Docker was installed but Compose v2 is unavailable. Install Docker manually and verify: docker compose version"
fi
}
pick_docker_bin() {
if docker info >/dev/null 2>&1; then
printf '%s' "docker"
return
fi
if sudo docker info >/dev/null 2>&1; then
printf '%s' "sudo docker"
return
fi
fail "Docker daemon is not available. Start Docker Desktop/service and rerun."
}
ensure_repo() {
if [[ -d "$INSTALL_DIR/.git" ]]; then
log "==> Updating existing repo in $INSTALL_DIR"
git -C "$INSTALL_DIR" fetch origin "$BRANCH" --depth 1
git -C "$INSTALL_DIR" checkout "$BRANCH"
git -C "$INSTALL_DIR" pull --ff-only origin "$BRANCH"
return
fi
if [[ -d "$INSTALL_DIR" ]]; then
fail "Directory exists and is not a git repo: $INSTALL_DIR"
fi
log "==> Cloning repo to $INSTALL_DIR"
git clone --depth 1 --branch "$BRANCH" "$REPO_URL" "$INSTALL_DIR"
}
main() {
log "==> Eggent one-command installer"
log "Repo: $REPO_URL"
log "Branch: $BRANCH"
log "Install dir: $INSTALL_DIR"
install_git_if_missing
install_docker_if_missing
ensure_repo
local docker_bin
local default_bind_host app_bind_host app_port
app_port="${APP_PORT:-3000}"
default_bind_host="127.0.0.1"
if [[ "$(uname -s)" == "Linux" ]]; then
# One-command installs are often used on VPS hosts where the app should be reachable remotely.
default_bind_host="0.0.0.0"
fi
app_bind_host="${EGGENT_APP_BIND_HOST:-$default_bind_host}"
docker_bin="$(pick_docker_bin)"
cd "$INSTALL_DIR"
chmod +x ./scripts/install-docker.sh
log "==> Running Docker deployment"
APP_BIND_HOST="$app_bind_host" APP_PORT="$app_port" DOCKER_BIN="$docker_bin" ./scripts/install-docker.sh
log ""
log "Done."
if [[ "$app_bind_host" == "0.0.0.0" ]]; then
log "Open: http://<server-ip>:${app_port}"
else
log "Open: http://127.0.0.1:${app_port}"
fi
}
main "$@"

View File

@@ -0,0 +1,79 @@
import path from "path";
import fs from "fs/promises";
import { importKnowledge, queryKnowledge } from "../src/lib/memory/knowledge";
import { AppSettings } from "../src/lib/types";
// Mock settings
const mockSettings: AppSettings = {
chatModel: { provider: "openai", model: "gpt-4o" },
utilityModel: { provider: "openai", model: "gpt-4o-mini" },
embeddingsModel: {
provider: "mock",
model: "text-embedding-3-small",
// Assumes OPENAI_API_KEY is set in env
},
codeExecution: { enabled: false, timeout: 30, maxOutputLength: 1000 },
memory: {
enabled: true,
similarityThreshold: 0.1, // Low threshold for testing
maxResults: 5,
chunkSize: 400,
},
search: { enabled: false, provider: "none" },
general: { darkMode: true, language: "en" },
auth: {
enabled: true,
username: "admin",
passwordHash: "",
mustChangeCredentials: false,
},
};
async function main() {
console.log("Starting Memory Ingestion Test...");
const testDir = path.join(process.cwd(), "data", "test-knowledge");
const testSubdir = "test-project-memory";
// 1. Setup Test Environment
console.log("Setting up test directory:", testDir);
await fs.mkdir(testDir, { recursive: true });
// 2. Create Test Files
// Text file
await fs.writeFile(path.join(testDir, "test.txt"), "The secret code for the project is ALPHA-BETA-GAMMA. It is very confidential.");
console.log("Created test.txt");
// PDF & Image would require real files to test properly,
// but we can at least test the text loader and the import logic.
// We'll skip creating dummy PDFs/Images for this automated run to avoid binary complexity,
// relying on the text test to prove the pipeline works.
// 3. Import Knowledge
console.log("Importing knowledge...");
const result = await importKnowledge(testDir, testSubdir, mockSettings);
console.log("Import result:", result);
if (result.errors.length > 0) {
console.error("Import errors:", result.errors);
}
// 4. Query Knowledge
console.log("Querying knowledge...");
const query = "What is the secret code?";
const answer = await queryKnowledge(query, 3, [testSubdir], mockSettings);
console.log(`Query: "${query}"`);
console.log("Result:", answer);
// 5. Cleanup
console.log("Cleaning up...");
await fs.rm(testDir, { recursive: true, force: true });
// We might want to keep the vector DB for inspection, but strict cleanup removes it too.
// const memoryDir = path.join(process.cwd(), "data", "memory", testSubdir);
// await fs.rm(memoryDir, { recursive: true, force: true });
console.log("Test Complete.");
}
main().catch(console.error);