Files
HaBraid/src/setup.ts
Contributor 5d5336757d feat: HaBraid v0.1.0 — host-following memory visualization engine for Obsidian
- Hybrid BM25 + HNSW vector search with context enrichment
- Knowledge graph with entities, relations, and community detection
- Host-following LLM route with fallback backends
- 3-tier lint system (static + HNSW dup + contradiction detection)
- Q&A Synthesis (Karpathy LLM Wiki pattern)
- 16 MCP tools for agent-driven workflows
- Incremental wiki generation with checkpointing
- SQLite-backed item store with FTS5 + vector indexes
2026-04-18 15:46:42 +09:00

284 lines
8.2 KiB
TypeScript

/**
* Zero-config setup for HaBraid.
*
* On first run, automatically:
* 1. Creates ~/.habraid/{app,data} directory structure
* 2. Detects MemPalace installation
* 3. Initializes database
* 4. Generates default config
* 5. Runs initial ingest + index
*/
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { openDatabase } from "./db/database.js";
import { getItemCount } from "./db/items.js";
import type { WikiEngineConfig } from "./types.js";
import { ensureDir, expandHomeDir, pathExists, resolvePath } from "./utils.js";
/** Base directory for internal HaBraid runtime data. */
export const HABRAID_HOME = path.join(os.homedir(), ".habraid");
/** Hidden app repo path inside the HaBraid home. */
export const HABRAID_APP_HOME = path.join(HABRAID_HOME, "app");
/** Hidden data path inside the HaBraid home. */
export const HABRAID_DATA_HOME = path.join(HABRAID_HOME, "data");
/** Primary user-facing wiki vault path for one-click installs. */
export const PRIMARY_VAULT_PATH = path.join(os.homedir(), "wiki");
/** Well-known MemPalace raw directory search paths. */
const MEMPALACE_SEARCH_PATHS = [
// MemPalace MCP standard location
path.join(os.homedir(), ".local", "share", "mempalace", "raw"),
// Hermes-integrated MemPalace
path.join(os.homedir(), "wiki", "raw", "mempalace"),
// Standalone MemPalace
path.join(os.homedir(), "mempalace", "raw"),
// OpenClaw legacy
path.join(os.homedir(), ".openclaw", "raw"),
];
/**
* Searches for an existing MemPalace raw directory.
*
* @returns Absolute path to raw directory, or empty string if not found.
*/
export function detectMemPalacePath(): string {
for (const candidate of MEMPALACE_SEARCH_PATHS) {
try {
if (fs.existsSync(candidate)) {
const stat = fs.statSync(candidate);
if (stat.isDirectory()) {
// Verify it has at least one .md file (directly or nested)
const hasMd = hasMarkdownFiles(candidate);
if (hasMd) {
return candidate;
}
}
}
} catch {
continue;
}
}
return "";
}
/**
* Recursively checks if a directory contains any .md files.
*/
function hasMarkdownFiles(dir: string): boolean {
try {
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
if (entry.name.startsWith(".")) continue;
if (entry.isFile() && entry.name.endsWith(".md")) return true;
if (entry.isDirectory()) {
if (hasMarkdownFiles(path.join(dir, entry.name))) return true;
}
}
} catch {
// ignore
}
return false;
}
/**
* Creates the ~/.habraid/ directory structure.
*/
async function createDirectoryStructure(vaultPath: string): Promise<void> {
const dirs = [
HABRAID_HOME,
HABRAID_APP_HOME,
HABRAID_DATA_HOME,
vaultPath,
path.join(vaultPath, "raw"),
path.join(vaultPath, "wiki"),
path.join(HABRAID_DATA_HOME, "models"),
path.join(HABRAID_DATA_HOME, "logs"),
path.join(HABRAID_DATA_HOME, "backups"),
];
for (const dir of dirs) {
await ensureDir(dir);
}
}
/**
* Creates a symlink from vault/raw/ to the detected MemPalace path.
*
* @param mempalacePath Detected MemPalace raw directory.
*/
function linkMemPalaceRaw(vaultPath: string, mempalacePath: string): void {
const linkPath = path.join(vaultPath, "raw", "mempalace");
// Remove existing link/dir if it exists
try {
const stat = fs.lstatSync(linkPath);
if (stat.isSymbolicLink()) {
fs.unlinkSync(linkPath);
}
} catch {
// doesn't exist, that's fine
}
// Create symlink
fs.symlinkSync(mempalacePath, linkPath, "junction");
}
/**
* Generates a default config.json with auto-detected paths.
*
* @param mempalacePath Auto-detected MemPalace path (may be empty).
* @returns Config object.
*/
export function generateDefaultConfig(mempalacePath: string): WikiEngineConfig {
return {
vault: {
path: PRIMARY_VAULT_PATH,
branch: "main",
},
db: {
path: path.join(HABRAID_DATA_HOME, "habraid.db"),
},
mempalace: {
enabled: mempalacePath.length > 0,
path: mempalacePath,
},
llm: {
mode: "host",
preferences: {
priority: "balanced",
},
fallback: {
provider: "zai",
model: "glm-5.1",
api_url: "https://api.example.com/v1",
api_key_env: "GLM_API_KEY",
max_tokens: 4096,
},
provider: "zai",
model: "glm-5.1",
api_url: "https://api.example.com/v1",
api_key_env: "GLM_API_KEY",
max_tokens: 4096,
},
sync: {
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || "Asia/Seoul",
},
};
}
/**
* Writes config to ~/.habraid/data/config.json if it doesn't exist.
*
* @param config Config to write.
*/
async function writeConfigIfMissing(config: WikiEngineConfig): Promise<boolean> {
const configPath = path.join(HABRAID_DATA_HOME, "config.json");
if (fs.existsSync(configPath)) {
return false; // already exists
}
const { writeTextFile } = await import("./utils.js");
await writeTextFile(configPath, JSON.stringify(config, null, 2) + "\n");
return true;
}
/**
* Result of the setup process.
*/
export interface SetupResult {
/** Whether this was a first-time setup. */
firstRun: boolean;
/** Detected MemPalace path (empty if not found). */
mempalacePath: string;
/** Number of items already in DB (0 for first run). */
existingItems: number;
/** Messages to display to the user. */
messages: string[];
}
/**
* Runs the zero-config setup process.
*
* Safe to call on every server start — only does work on first run.
*
* @param config Loaded config (may be defaults).
* @returns Setup result with status info.
*/
export async function runSetup(config: WikiEngineConfig): Promise<SetupResult> {
const messages: string[] = [];
let firstRun = false;
// 1. Ensure directory structure exists
const habraidExists = fs.existsSync(HABRAID_HOME);
const vaultExists = fs.existsSync(resolvePath(config.vault.path));
if (!habraidExists || !vaultExists) {
await createDirectoryStructure(resolvePath(config.vault.path));
messages.push(`Created HaBraid runtime home and primary vault directories.`);
firstRun = true;
}
// 2. Detect MemPalace
const effectiveConfig = config.mempalace.path
? config
: { ...config, mempalace: { ...config.mempalace, path: "", enabled: config.mempalace.enabled } };
const vaultPath = resolvePath(effectiveConfig.vault.path);
let mempalacePath = config.mempalace.path
? resolvePath(config.mempalace.path)
: detectMemPalacePath();
if (mempalacePath) {
messages.push(`MemPalace found at ${mempalacePath}`);
// Create symlink if vault/raw/mempalace doesn't point there yet
const linkPath = path.join(vaultPath, "raw", "mempalace");
try {
const existing = fs.readlinkSync(linkPath);
if (existing !== mempalacePath) {
linkMemPalaceRaw(vaultPath, mempalacePath);
}
} catch {
linkMemPalaceRaw(vaultPath, mempalacePath);
}
} else {
messages.push("MemPalace not found. You can set mempalace.path in config.json later.");
}
// 3. Ensure config file exists
const finalConfig = config.mempalace.path
? { ...effectiveConfig, mempalace: { ...effectiveConfig.mempalace, path: mempalacePath, enabled: mempalacePath.length > 0 } }
: { ...effectiveConfig, mempalace: { ...effectiveConfig.mempalace, path: mempalacePath, enabled: mempalacePath.length > 0 } };
const wroteConfig = await writeConfigIfMissing(finalConfig);
if (wroteConfig) {
messages.push("Created default config.json.");
}
// 4. Initialize DB if needed
let existingItems = 0;
try {
const dbPath = resolvePath(finalConfig.db.path);
const db = openDatabase(dbPath);
existingItems = getItemCount(db);
db.close();
messages.push(`DB ready (${existingItems} items).`);
} catch (error) {
messages.push(`DB init: ${error instanceof Error ? error.message : String(error)}`);
}
// 5. First-run Obsidian hint
if (firstRun) {
messages.push("");
messages.push("To view in Obsidian, open this folder as a vault:");
messages.push(` ${vaultPath}`);
messages.push("This is the primary user-facing wiki path for HaBraid one-click installs.");
}
return { firstRun, mempalacePath, existingItems, messages };
}