feat: rename data directory, add global project settings, multi-project CLI commands, and provider badge in model selector

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-04-02 09:10:42 -07:00
parent 1fa8837c6b
commit 566f531361
80 changed files with 857 additions and 652 deletions

View File

@@ -193,7 +193,7 @@ describe("CentralCore Integration", () => {
it("should verify database path and stats", async () => {
const dbPath = central.getDatabasePath();
expect(dbPath).toContain("kb-central.db");
expect(dbPath).toContain("fusion-central.db");
const globalDir = central.getGlobalDir();
expect(globalDir).toBe(tempDir);

View File

@@ -13,8 +13,8 @@ function createTempDir(): string {
// Helper to create a fake kb project structure
function createFakeKbProject(dir: string): void {
mkdirSync(join(dir, ".kb"), { recursive: true });
writeFileSync(join(dir, ".kb", "kb.db"), "");
mkdirSync(join(dir, ".fusion"), { recursive: true });
writeFileSync(join(dir, ".fusion", "fusion.db"), "");
}
describe("FirstRunExperience", () => {

View File

@@ -12,9 +12,9 @@ function createTempDir(): string {
// Helper to create a fake kb project structure
function createFakeKbProject(dir: string): void {
mkdirSync(join(dir, ".kb"), { recursive: true });
mkdirSync(join(dir, ".fusion"), { recursive: true });
// Create an empty file as the database (enough for detection)
writeFileSync(join(dir, ".kb", "kb.db"), "");
writeFileSync(join(dir, ".fusion", "fusion.db"), "");
}
describe("MigrationOrchestrator", () => {
@@ -164,7 +164,7 @@ describe("MigrationOrchestrator", () => {
const projectDir = join(tempDir, "incomplete-project");
mkdirSync(projectDir, { recursive: true });
mkdirSync(join(projectDir, ".fusion"), { recursive: true });
// Create directory but no kb.db file
// Create directory but no fusion.db file
const detected = await orchestrator.detectExistingProjects(projectDir);

View File

@@ -15,7 +15,7 @@ function createTempDir(): string {
function createFakeFusionProject(dir: string): void {
const fusionDir = join(dir, ".fusion");
mkdirSync(fusionDir, { recursive: true });
const db = new DatabaseSync(join(fusionDir, "kb.db"));
const db = new DatabaseSync(join(fusionDir, "fusion.db"));
db.exec("CREATE TABLE IF NOT EXISTS sanity (id INTEGER PRIMARY KEY)");
db.close();
}
@@ -125,7 +125,7 @@ describe("TaskStore Backward Compatibility", () => {
createFakeFusionProject(projectDir);
process.chdir(projectDir);
const centralDb = join(tempDir, "kb-central.db");
const centralDb = join(tempDir, "fusion-central.db");
await centralCore.close();
rmSync(centralDb, { force: true });
centralCore = new CentralCore(tempDir);
@@ -135,7 +135,7 @@ describe("TaskStore Backward Compatibility", () => {
expect(store).toBeInstanceOf(TaskStore);
const task = await store.createTask({ description: "legacy task" });
expect(task.id).toBe("FN-001");
expect(existsSync(join(projectDir, ".fusion", "kb.db"))).toBe(true);
expect(existsSync(join(projectDir, ".fusion", "fusion.db"))).toBe(true);
expect(existsSync(join(projectDir, ".fusion", "tasks", task.id, "task.json"))).toBe(true);
});

View File

@@ -1,8 +1,8 @@
/**
* AgentStore - Filesystem-based persistence for agent lifecycle management
*
* Agents are stored at `.kb/agents/{agentId}.json` with their metadata.
* Heartbeat events are appended to `.kb/agents/{agentId}-heartbeats.jsonl`.
* Agents are stored at `.fusion/agents/{agentId}.json` with their metadata.
* Heartbeat events are appended to `.fusion/agents/{agentId}-heartbeats.jsonl`.
*
* File Structure:
* - agents/{agentId}.json: Agent metadata (id, name, role, state, taskId, timestamps, metadata)
@@ -51,7 +51,7 @@ type TypedEventEmitter<Events extends Record<string, unknown[]>> = {
/** Options for AgentStore constructor */
export interface AgentStoreOptions {
/** Root directory for kb data (default: .kb) */
/** Root directory for kb data (default: .fusion) */
rootDir?: string;
}

View File

@@ -28,7 +28,7 @@ describe("BackupManager", () => {
kbDir = join(tempDir, ".fusion");
await mkdir(kbDir, { recursive: true });
// Create a dummy database file
writeFileSync(join(kbDir, "kb.db"), "dummy database content");
writeFileSync(join(kbDir, "fusion.db"), "dummy database content");
backupManager = new BackupManager(kbDir);
});
@@ -48,7 +48,7 @@ describe("BackupManager", () => {
it("should copy database content correctly", async () => {
const backup = await backupManager.createBackup();
const originalContent = readFileSync(join(kbDir, "kb.db"), "utf-8");
const originalContent = readFileSync(join(kbDir, "fusion.db"), "utf-8");
const backupContent = readFileSync(backup.path, "utf-8");
expect(backupContent).toBe(originalContent);
@@ -186,13 +186,13 @@ describe("BackupManager", () => {
const backup = await backupManager.createBackup();
// Modify the original database
await writeFile(join(kbDir, "kb.db"), "modified content");
await writeFile(join(kbDir, "fusion.db"), "modified content");
// Restore the backup
await backupManager.restoreBackup(backup.filename, { createPreRestoreBackup: false });
// Verify the restore
const restoredContent = readFileSync(join(kbDir, "kb.db"), "utf-8");
const restoredContent = readFileSync(join(kbDir, "fusion.db"), "utf-8");
expect(restoredContent).toBe("dummy database content");
});
@@ -313,7 +313,7 @@ describe("createBackupManager", () => {
const tempDir = mkdtempSync(join(tmpdir(), "kb-backup-test-"));
const kbDir = join(tempDir, ".fusion");
await mkdir(kbDir, { recursive: true });
writeFileSync(join(kbDir, "kb.db"), "test");
writeFileSync(join(kbDir, "fusion.db"), "test");
const settings: Partial<ProjectSettings> = {
autoBackupDir: "custom/backups",
@@ -349,7 +349,7 @@ describe("runBackupCommand", () => {
tempDir = mkdtempSync(join(tmpdir(), "kb-backup-test-"));
kbDir = join(tempDir, ".fusion");
await mkdir(kbDir, { recursive: true });
writeFileSync(join(kbDir, "kb.db"), "dummy database content");
writeFileSync(join(kbDir, "fusion.db"), "dummy database content");
});
afterEach(async () => {
@@ -437,7 +437,7 @@ describe("runBackupCommand", () => {
it("should return failure when database file is missing", async () => {
// Remove the database
await rm(join(kbDir, "kb.db"));
await rm(join(kbDir, "fusion.db"));
const settings: ProjectSettings = {
maxConcurrent: 2,

View File

@@ -39,7 +39,7 @@ export class BackupManager {
/**
* Creates a new BackupManager instance.
* @param kbDir - Absolute path to the .kb directory
* @param kbDir - Absolute path to the .fusion directory
* @param options - Backup configuration options
*/
constructor(kbDir: string, options?: BackupOptions) {
@@ -61,7 +61,7 @@ export class BackupManager {
* @returns BackupInfo for the newly created backup
*/
async createBackup(): Promise<BackupInfo> {
const sourcePath = join(this.kbDir, "kb.db");
const sourcePath = join(this.kbDir, "fusion.db");
const backupDirPath = this.getBackupDirPath();
// Ensure backup directory exists
@@ -188,7 +188,7 @@ export class BackupManager {
): Promise<void> {
const backupDirPath = this.getBackupDirPath();
const sourcePath = join(backupDirPath, filename);
const targetPath = join(this.kbDir, "kb.db");
const targetPath = join(this.kbDir, "fusion.db");
// Verify source exists
try {
@@ -281,7 +281,7 @@ export function validateBackupDir(dir: string): boolean {
/**
* Factory function to create a BackupManager with project settings.
* @param kbDir - Absolute path to the .kb directory
* @param kbDir - Absolute path to the .fusion directory
* @param settings - Project settings containing backup configuration
* @returns Configured BackupManager instance
*/
@@ -303,7 +303,7 @@ export function createBackupManager(
* at the automation/scheduler level. This allows manual backups via CLI even when
* auto-backup is disabled.
*
* @param kbDir - Absolute path to the .kb directory
* @param kbDir - Absolute path to the .fusion directory
* @param settings - Project settings
* @returns Result of the backup operation
*/

View File

@@ -18,9 +18,9 @@ function tempDir(prefix: string): string {
// Helper to create a fake kb project
function createFakeKbProject(dir: string): void {
const kbDir = join(dir, ".kb");
const kbDir = join(dir, ".fusion");
mkdirSync(kbDir, { recursive: true });
writeFileSync(join(kbDir, "kb.db"), "SQLite format 3\x00");
writeFileSync(join(kbDir, "fusion.db"), "SQLite format 3\x00");
}
describe("Backward Compatibility Layer", () => {
@@ -216,7 +216,7 @@ describe("Backward Compatibility Layer", () => {
it("should return legacy mode when no central DB", async () => {
// Close and remove central DB
await central.close();
rmSync(join(tempGlobalDir, "kb-central.db"), { force: true });
rmSync(join(tempGlobalDir, "fusion-central.db"), { force: true });
// Re-create central but don't init
central = new CentralCore(tempGlobalDir);
@@ -232,7 +232,7 @@ describe("Backward Compatibility Layer", () => {
it("should report legacy mode correctly", async () => {
// Close and remove central DB
await central.close();
rmSync(join(tempGlobalDir, "kb-central.db"), { force: true });
rmSync(join(tempGlobalDir, "fusion-central.db"), { force: true });
central = new CentralCore(tempGlobalDir);

View File

@@ -33,7 +33,7 @@ describe("CentralCore", () => {
it("should initialize and create database", async () => {
await central.init();
expect(central.isInitialized()).toBe(true);
expect(central.getDatabasePath()).toBe(join(tempDir, "kb-central.db"));
expect(central.getDatabasePath()).toBe(join(tempDir, "fusion-central.db"));
});
it("should be idempotent on multiple init calls", async () => {
@@ -919,7 +919,7 @@ describe("CentralCore", () => {
it("should get database path", async () => {
const path = central.getDatabasePath();
expect(path).toBe(join(tempDir, "kb-central.db"));
expect(path).toBe(join(tempDir, "fusion-central.db"));
});
it("should get global directory", async () => {

View File

@@ -4,7 +4,7 @@
* Provides project registry, health tracking, unified activity feed,
* and global concurrency management across all registered projects.
*
* The central database is located at `~/.pi/kb/kb-central.db`.
* The central database is located at `~/.pi/fusion/fusion-central.db`.
*
* @example
* ```typescript
@@ -43,7 +43,7 @@ import type {
ProjectSettings,
} from "./types.js";
import { CentralDatabase, toJson, toJsonNullable, fromJson } from "./central-db.js";
import { defaultGlobalDir } from "./global-settings.js";
import { resolveGlobalDir } from "./global-settings.js";
// ── Event Types ───────────────────────────────────────────────────────────
@@ -71,13 +71,13 @@ export class CentralCore extends EventEmitter<CentralCoreEvents> {
/**
* Create a CentralCore instance.
* @param globalDir — Directory for central database. Defaults to `~/.pi/kb/`.
* @param globalDir — Directory for central database. Defaults to `~/.pi/fusion/`.
* Accepts a custom path for testing.
*/
constructor(globalDir?: string) {
super();
this.setMaxListeners(100);
this.globalDir = globalDir ?? defaultGlobalDir();
this.globalDir = resolveGlobalDir(globalDir);
}
/**
@@ -818,10 +818,10 @@ export class CentralCore extends EventEmitter<CentralCoreEvents> {
/**
* Get the path to the central database file.
*
* @returns Absolute path to kb-central.db
* @returns Absolute path to fusion-central.db
*/
getDatabasePath(): string {
return this.db?.getPath() ?? join(this.globalDir, "kb-central.db");
return this.db?.getPath() ?? join(this.globalDir, "fusion-central.db");
}
/**

View File

@@ -22,7 +22,7 @@ describe("CentralDatabase", () => {
it("should create database at the specified path", () => {
db.init();
const dbPath = db.getPath();
expect(dbPath).toBe(join(tempDir, "kb-central.db"));
expect(dbPath).toBe(join(tempDir, "fusion-central.db"));
// Verify file exists
const stats = statSync(dbPath);
expect(stats.isFile()).toBe(true);

View File

@@ -5,7 +5,7 @@
* synchronous transaction handling. The database runs in WAL mode
* for concurrent reader/writer access.
*
* This database is stored at `~/.pi/kb/kb-central.db` and serves as the
* This database is stored at `~/.pi/fusion/fusion-central.db` and serves as the
* coordination hub for all projects, storing the project registry,
* unified activity feed, global concurrency limits, and project health.
*/
@@ -13,8 +13,8 @@
import { DatabaseSync } from "node:sqlite";
import { join } from "node:path";
import { mkdirSync, existsSync } from "node:fs";
import { homedir } from "node:os";
import type { Statement } from "./db.js";
import { resolveGlobalDir } from "./global-settings.js";
// ── JSON Helpers (reused from db.ts) ─────────────────────────────────────
@@ -95,13 +95,6 @@ CREATE TABLE IF NOT EXISTS __meta (
// ── Central Database Class ────────────────────────────────────────────────
/**
* Default directory for central kb data: `~/.pi/kb/`
*/
function defaultGlobalDir(): string {
return join(homedir(), ".pi", "kb");
}
export class CentralDatabase {
private db: DatabaseSync;
private readonly dbPath: string;
@@ -110,8 +103,8 @@ export class CentralDatabase {
private transactionDepth = 0;
constructor(globalDir?: string) {
this.globalDir = globalDir ?? defaultGlobalDir();
this.dbPath = join(this.globalDir, "kb-central.db");
this.globalDir = resolveGlobalDir(globalDir);
this.dbPath = join(this.globalDir, "fusion-central.db");
// Ensure directory exists
if (!existsSync(this.globalDir)) {
@@ -256,7 +249,7 @@ export class CentralDatabase {
/**
* Create a new CentralDatabase instance (does NOT initialize schema).
* Callers must call `db.init()` separately.
* @param globalDir - Path to the global kb directory (e.g., `~/.pi/kb/`)
* @param globalDir - Path to the global fusion directory (e.g., `~/.pi/fusion/`)
* @returns CentralDatabase instance (not yet initialized)
*/
export function createCentralDatabase(globalDir?: string): CentralDatabase {

View File

@@ -16,6 +16,7 @@ import type { Database } from "./db.js";
import { toJson, toJsonNullable, normalizeTaskComments } from "./db.js";
import type { Task, BoardConfig, ActivityLogEntry, ArchivedTaskEntry } from "./types.js";
import type { ScheduledTask } from "./automation.js";
import { resolveGlobalDir } from "./global-settings.js";
// ── Detection ────────────────────────────────────────────────────────
@@ -24,7 +25,7 @@ import type { ScheduledTask } from "./automation.js";
* Returns true if migration is needed.
*/
export function detectLegacyData(kbDir: string): boolean {
const hasDb = existsSync(join(kbDir, "kb.db"));
const hasDb = existsSync(join(kbDir, "fusion.db"));
if (hasDb) return false;
return (
@@ -45,7 +46,7 @@ export function getMigrationStatus(kbDir: string): {
hasDatabase: boolean;
needsMigration: boolean;
} {
const hasDatabase = existsSync(join(kbDir, "kb.db"));
const hasDatabase = existsSync(join(kbDir, "fusion.db"));
const hasLegacy =
existsSync(join(kbDir, "tasks")) ||
existsSync(join(kbDir, "config.json")) ||
@@ -539,13 +540,13 @@ async function createBackups(kbDir: string): Promise<void> {
*
* Returns true if:
* - Central DB doesn't exist AND
* - cwd has `.kb/kb.db` (existing single-project)
* - cwd has `.fusion/fusion.db` or `.fusion/fusion.db` (existing single-project)
*
* @param cwd — Current working directory to check
* @param globalDir — Directory for central database. Defaults to `~/.pi/kb/`.
* @param globalDir — Directory for central database. Defaults to `~/.pi/fusion/`.
*/
export function needsCentralMigration(cwd: string, globalDir?: string): boolean {
const centralDbPath = join(globalDir ?? join(homedir(), ".pi", "kb"), "kb-central.db");
const centralDbPath = join(resolveGlobalDir(globalDir), "fusion-central.db");
if (existsSync(centralDbPath)) {
return false;
}
@@ -555,7 +556,14 @@ export function needsCentralMigration(cwd: string, globalDir?: string): boolean
const root = dirname(current) === current ? current : "/";
while (true) {
const dbPath = join(current, ".kb", "kb.db");
const fusionDbPath = join(current, ".fusion", "fusion.db");
if (existsSync(fusionDbPath)) {
try {
const stat = statSync(fusionDbPath);
if (stat.isFile() && stat.size > 0) return true;
} catch { /* fall through */ }
}
const dbPath = join(current, ".fusion", "fusion.db");
if (existsSync(dbPath)) {
try {
const stat = statSync(dbPath);
@@ -581,7 +589,7 @@ export function needsCentralMigration(cwd: string, globalDir?: string): boolean
* Detect existing projects by walking up from cwd.
*
* @param cwd — Starting directory (default: process.cwd())
* @param globalDir — Directory for central database. Defaults to `~/.pi/kb/`.
* @param globalDir — Directory for central database. Defaults to `~/.pi/fusion/`.
* @returns Array of detected projects
*/
export async function detectExistingProjects(

View File

@@ -33,7 +33,7 @@ describe("Database", () => {
describe("initialization", () => {
it("creates the database file", () => {
expect(existsSync(join(kbDir, "kb.db"))).toBe(true);
expect(existsSync(join(kbDir, "fusion.db"))).toBe(true);
});
it("creates the .fusion directory if missing", () => {
@@ -1024,7 +1024,7 @@ describe("createDatabase factory", () => {
const db = createDatabase(kbDir);
// DB file exists (created on open) but schema not initialized
expect(existsSync(join(kbDir, "kb.db"))).toBe(true);
expect(existsSync(join(kbDir, "fusion.db"))).toBe(true);
// Schema is NOT yet created — querying __meta would fail
expect(() => db.getSchemaVersion()).toThrow();
@@ -1048,7 +1048,7 @@ describe("createDatabase factory", () => {
const kbDir = join(tmpDir, ".fusion");
const db = createDatabase(kbDir);
expect(db.getPath()).toBe(join(kbDir, "kb.db"));
expect(db.getPath()).toBe(join(kbDir, "fusion.db"));
db.close();
});

View File

@@ -315,7 +315,7 @@ export class Database {
private transactionDepth = 0;
constructor(private kbDir: string) {
this.dbPath = join(kbDir, "kb.db");
this.dbPath = join(kbDir, "fusion.db");
// Ensure .fusion directory exists
if (!existsSync(kbDir)) {

View File

@@ -1,5 +1,5 @@
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import { GlobalSettingsStore } from "./global-settings.js";
import { GlobalSettingsStore, defaultGlobalDir } from "./global-settings.js";
import { DEFAULT_GLOBAL_SETTINGS } from "./types.js";
import { readFile, rm, writeFile, mkdir } from "node:fs/promises";
import { join } from "node:path";
@@ -13,6 +13,7 @@ function makeTmpDir(): string {
describe("GlobalSettingsStore", () => {
let dir: string;
let store: GlobalSettingsStore;
const originalHome = process.env.HOME;
beforeEach(() => {
dir = makeTmpDir();
@@ -21,6 +22,11 @@ describe("GlobalSettingsStore", () => {
afterEach(async () => {
await rm(dir, { recursive: true, force: true });
if (originalHome === undefined) {
delete process.env.HOME;
} else {
process.env.HOME = originalHome;
}
});
describe("init()", () => {
@@ -60,6 +66,30 @@ describe("GlobalSettingsStore", () => {
const settings = await store.getSettings();
expect(settings.themeMode).toBe("light");
});
it("adopts the legacy ~/.pi/kb directory when ~/.pi/fusion does not exist", async () => {
const homeDir = makeTmpDir();
process.env.HOME = homeDir;
const legacyDir = join(homeDir, ".pi", "kb");
await mkdir(legacyDir, { recursive: true });
await writeFile(
join(legacyDir, "settings.json"),
JSON.stringify({ themeMode: "light" }),
);
const defaultStore = new GlobalSettingsStore();
await defaultStore.init();
expect(defaultStore.getSettingsPath()).toBe(join(defaultGlobalDir(), "settings.json"));
expect(existsSync(join(homeDir, ".pi", "fusion", "settings.json"))).toBe(true);
expect(existsSync(join(homeDir, ".pi", "kb"))).toBe(false);
const settings = await defaultStore.getSettings();
expect(settings.themeMode).toBe("light");
await rm(homeDir, { recursive: true, force: true });
});
});
describe("getSettings()", () => {

View File

@@ -1,5 +1,5 @@
/**
* Global settings store — manages user-level settings in `~/.pi/kb/settings.json`.
* Global settings store — manages user-level settings in `~/.pi/fusion/settings.json`.
*
* Global settings persist across all kb projects for the current user.
* They include UI theme preferences, default AI model selection, and
@@ -9,17 +9,47 @@
*/
import { homedir } from "node:os";
import { join } from "node:path";
import { dirname, join } from "node:path";
import { mkdir, readFile, writeFile, rename } from "node:fs/promises";
import { existsSync } from "node:fs";
import { existsSync, mkdirSync, renameSync } from "node:fs";
import type { GlobalSettings } from "./types.js";
import { DEFAULT_GLOBAL_SETTINGS } from "./types.js";
/** Default directory for global kb settings: `~/.pi/kb/` */
export function defaultGlobalDir(): string {
/** Legacy directory for global settings before the rename to fusion. */
export function legacyGlobalDir(): string {
return join(homedir(), ".pi", "kb");
}
/** Default directory for global fusion settings: `~/.pi/fusion/` */
export function defaultGlobalDir(): string {
return join(homedir(), ".pi", "fusion");
}
/**
* Resolve the active global directory.
*
* If the new `~/.pi/fusion` directory does not exist but the legacy
* `~/.pi/kb` directory does, move the legacy directory into place so
* existing settings and central metadata continue to work after upgrade.
*/
export function resolveGlobalDir(dir?: string): string {
if (dir) return dir;
const preferredDir = defaultGlobalDir();
const legacyDir = legacyGlobalDir();
if (!existsSync(preferredDir) && existsSync(legacyDir)) {
try {
mkdirSync(dirname(preferredDir), { recursive: true });
renameSync(legacyDir, preferredDir);
} catch {
return legacyDir;
}
}
return preferredDir;
}
export class GlobalSettingsStore {
private readonly settingsPath: string;
private readonly dir: string;
@@ -29,11 +59,11 @@ export class GlobalSettingsStore {
/**
* Create a GlobalSettingsStore.
* @param dir — Directory to store settings.json. Defaults to `~/.pi/kb/`.
* @param dir — Directory to store settings.json. Defaults to `~/.pi/fusion/`.
* Accepts a custom path for testing.
*/
constructor(dir?: string) {
this.dir = dir ?? defaultGlobalDir();
this.dir = resolveGlobalDir(dir);
this.settingsPath = join(this.dir, "settings.json");
}

View File

@@ -94,7 +94,7 @@ export class MigrationOrchestrator {
* Detect existing kb projects by walking the filesystem.
*
* Scans from the starting path up to maxDepth levels deep, looking for
* directories containing `.kb/kb.db`.
* directories containing `.fusion/fusion.db` (or legacy `.fusion/fusion.db`)
*
* Security notes:
* - Only scans from the specified startPath
@@ -154,7 +154,7 @@ export class MigrationOrchestrator {
return;
}
// Check if this directory is a kb project (has .kb/kb.db)
// Check if this directory is a kb project (has .fusion/fusion.db or .fusion/fusion.db)
const hasKbDb = this.isKbProject(dir);
if (hasKbDb) {
const name = this.generateProjectName(dir);
@@ -201,15 +201,28 @@ export class MigrationOrchestrator {
/**
* Check if a directory contains a valid kb project.
* Validates that .kb/kb.db exists and is a file.
* Validates that .fusion/fusion.db (or legacy .fusion/fusion.db) exists and is a file.
*/
private isKbProject(dir: string): boolean {
const kbPath = join(dir, ".kb");
// Check current layout: .fusion/fusion.db
const fusionPath = join(dir, ".fusion");
if (existsSync(fusionPath)) {
const fusionDb = join(fusionPath, "fusion.db");
if (existsSync(fusionDb)) {
try {
const stats = statSync(fusionDb);
if (stats.isFile()) return true;
} catch { /* fall through */ }
}
}
// Fall back to legacy layout: .fusion/fusion.db
const kbPath = join(dir, ".fusion");
if (!existsSync(kbPath)) {
return false;
}
const dbPath = join(kbPath, "kb.db");
const dbPath = join(kbPath, "fusion.db");
if (!existsSync(dbPath)) {
return false;
}
@@ -233,7 +246,7 @@ export class MigrationOrchestrator {
/**
* Auto-register detected projects in the central registry.
*
* - Filters to projects with valid kb.db
* - Filters to projects with valid fusion.db
* - Skips already-registered projects
* - Generates unique names (appends number if conflict: name, name-2, name-3)
* - Sets isolationMode to 'in-process' for migrated projects

View File

@@ -23,10 +23,10 @@ function tempDir(prefix: string): string {
// Helper to create a fake kb project
function createFakeKbProject(dir: string): void {
const kbDir = join(dir, ".kb");
const kbDir = join(dir, ".fusion");
mkdirSync(kbDir, { recursive: true });
// Create empty kb.db file (SQLite needs actual format, but for detection an empty file works)
writeFileSync(join(kbDir, "kb.db"), "SQLite format 3\x00");
// Create empty fusion.db file (SQLite needs actual format, but for detection an empty file works)
writeFileSync(join(kbDir, "fusion.db"), "SQLite format 3\x00");
}
// Helper to create a fake git remote
@@ -64,7 +64,7 @@ describe("FirstRunDetector", () => {
});
describe("detectFirstRunState", () => {
it("should detect fresh-install when no central DB and no local .kb/", async () => {
it("should detect fresh-install when no central DB and no local .fusion/", async () => {
const tempProjectDir = tempDir("kb-fresh-");
process.chdir(tempProjectDir);
@@ -76,7 +76,7 @@ describe("FirstRunDetector", () => {
rmSync(tempProjectDir, { recursive: true, force: true });
});
it("should detect needs-migration when local .kb/ exists but no central DB", async () => {
it("should detect needs-migration when local .fusion/ exists but no central DB", async () => {
const tempProjectDir = tempDir("kb-needs-migration-");
createFakeKbProject(tempProjectDir);
process.chdir(tempProjectDir);
@@ -159,7 +159,7 @@ describe("FirstRunDetector", () => {
process.chdir(tempProjectDir);
mkdirSync(tempGlobalDir, { recursive: true });
writeFileSync(join(tempGlobalDir, "kb-central.db"), "not a sqlite database");
writeFileSync(join(tempGlobalDir, "fusion-central.db"), "not a sqlite database");
const detector = new FirstRunDetector(tempGlobalDir);
const state = await detector.detectFirstRunState();
@@ -174,7 +174,7 @@ describe("FirstRunDetector", () => {
process.chdir(tempProjectDir);
mkdirSync(tempGlobalDir, { recursive: true });
writeFileSync(join(tempGlobalDir, "kb-central.db"), "not a sqlite database");
writeFileSync(join(tempGlobalDir, "fusion-central.db"), "not a sqlite database");
const detector = new FirstRunDetector(tempGlobalDir);
const state = await detector.detectFirstRunState();
@@ -216,7 +216,7 @@ describe("FirstRunDetector", () => {
rmSync(tempProjectDir, { recursive: true, force: true });
});
it("should walk up directory tree to find .kb/", async () => {
it("should walk up directory tree to find .fusion/", async () => {
const tempProjectDir = tempDir("kb-parent-");
createFakeKbProject(tempProjectDir);
const nestedDir = join(tempProjectDir, "src", "components");
@@ -304,7 +304,7 @@ describe("FirstRunDetector", () => {
describe("getCentralDbPath", () => {
it("should return correct path", () => {
const detector = new FirstRunDetector(tempGlobalDir);
expect(detector.getCentralDbPath()).toBe(join(tempGlobalDir, "kb-central.db"));
expect(detector.getCentralDbPath()).toBe(join(tempGlobalDir, "fusion-central.db"));
});
});
});
@@ -638,15 +638,15 @@ describe("MigrationCoordinator", () => {
it("should return success for fresh-install state", async () => {
// Close and remove central to simulate fresh state
await central.close();
rmSync(join(tempGlobalDir, "kb-central.db"), { force: true });
rmSync(join(tempGlobalDir, "fusion-central.db"), { force: true });
// Create fresh temp dir with no .kb/
// Create fresh temp dir with no .fusion/
const tempFreshDir = tempDir("kb-fresh-coord-");
central = new CentralCore(tempGlobalDir);
await central.init();
// Change to fresh dir (no .kb/)
// Change to fresh dir (no .fusion/)
const originalCwd = process.cwd();
process.chdir(tempFreshDir);
@@ -820,7 +820,7 @@ describe("BackwardCompat", () => {
it("should return true when no central DB", async () => {
// Close and remove central DB
await central.close();
rmSync(join(tempGlobalDir, "kb-central.db"), { force: true });
rmSync(join(tempGlobalDir, "fusion-central.db"), { force: true });
// Need to re-init CentralCore for it to work
central = new CentralCore(tempGlobalDir);

View File

@@ -3,7 +3,7 @@
*
* Handles the transition from single-project to multi-project mode:
* - Detects first-run state (fresh install, needs migration, setup wizard, normal)
* - Auto-discovers existing .kb/ directories for migration
* - Auto-discovers existing .fusion/ directories for migration
* - Coordinates migration to central database
* - Provides backward compatibility for single-project workflows
*
@@ -16,13 +16,14 @@ import { isAbsolute, join, resolve, basename, dirname } from "node:path";
import type { CentralCore } from "./central-core.js";
import { CentralCore as CentralCoreClass } from "./central-core.js";
import type { CentralCoreStub } from "./migration-stubs.js";
import { resolveGlobalDir } from "./global-settings.js";
// ── Types ────────────────────────────────────────────────────────────
/** First-run state detection results */
export type FirstRunState =
| "fresh-install" // No central DB, no .kb/ anywhere
| "needs-migration" // No central DB, but .kb/kb.db exists in cwd
| "fresh-install" // No central DB, no .fusion/ anywhere
| "needs-migration" // No central DB, but .fusion/fusion.db exists in cwd
| "setup-wizard" // Central DB exists but has zero projects
| "normal-operation"; // Central DB exists with projects
@@ -32,7 +33,7 @@ export interface DetectedProject {
path: string;
/** Auto-generated or derived project name */
name: string;
/** Whether the project has a valid kb.db */
/** Whether the project has a valid fusion.db */
hasDb: boolean;
}
@@ -93,7 +94,7 @@ export class FirstRunDetector {
/**
* Create a FirstRunDetector.
* @param globalDir — Directory for central database. Defaults to `~/.pi/kb/`.
* @param globalDir — Directory for central database. Defaults to `~/.pi/fusion/`.
*/
constructor(globalDir?: string) {
this.globalDir = globalDir ?? this.getDefaultGlobalDir();
@@ -104,7 +105,7 @@ export class FirstRunDetector {
*
* Returns one of four states:
* - `"fresh-install"` — No central DB and no kb project found from cwd upward
* - `"needs-migration"` — No central DB, but a `.kb/kb.db` project exists in cwd ancestry
* - `"needs-migration"` — No central DB, but a `.fusion/fusion.db` project exists in cwd ancestry
* - `"setup-wizard"` — Central DB exists and can be read, but has zero projects
* - `"normal-operation"` — Central DB exists with one or more projects
*
@@ -159,7 +160,7 @@ export class FirstRunDetector {
* Check if the central database exists.
*/
hasCentralDb(): boolean {
const centralDbPath = join(this.globalDir, "kb-central.db");
const centralDbPath = join(this.globalDir, "fusion-central.db");
return existsSync(centralDbPath);
}
@@ -167,13 +168,13 @@ export class FirstRunDetector {
* Get the path to the central database.
*/
getCentralDbPath(): string {
return join(this.globalDir, "kb-central.db");
return join(this.globalDir, "fusion-central.db");
}
/**
* Detect existing projects by walking up the directory tree.
*
* Starting from `cwd`, walks up looking for `.kb/kb.db` files.
* Starting from `cwd`, walks up looking for `.fusion/fusion.db` files.
* Stops at home directory or root.
*
* @param cwd — Starting directory (default: process.cwd())
@@ -283,8 +284,8 @@ export class FirstRunDetector {
* Check if a directory contains a valid kb project.
*/
private hasKbProject(dir: string): boolean {
const kbDir = join(dir, ".kb");
const dbPath = join(kbDir, "kb.db");
const kbDir = join(dir, ".fusion");
const dbPath = join(kbDir, "fusion.db");
if (!existsSync(kbDir)) return false;
if (!existsSync(dbPath)) return false;
@@ -298,7 +299,7 @@ export class FirstRunDetector {
}
private getDefaultGlobalDir(): string {
return join(homedir(), ".pi", "kb");
return resolveGlobalDir();
}
}
@@ -504,8 +505,8 @@ export class MigrationCoordinator {
}
private hasKbProject(dir: string): boolean {
const kbDir = join(dir, ".kb");
const dbPath = join(kbDir, "kb.db");
const kbDir = join(dir, ".fusion");
const dbPath = join(kbDir, "fusion.db");
if (!existsSync(kbDir)) return false;
if (!existsSync(dbPath)) return false;

View File

@@ -70,7 +70,7 @@ export class MissionStore extends EventEmitter<MissionStoreEvents> {
/**
* Creates a new MissionStore instance.
*
* @param kbDir - Path to the .kb directory (e.g., /path/to/project/.kb)
* @param kbDir - Path to the .fusion directory (e.g., /path/to/project/.fusion)
* @param db - Shared Database instance (same instance used by TaskStore)
*/
constructor(

View File

@@ -2,7 +2,7 @@
* Settings export and import functionality.
*
* This module provides utilities for exporting and importing kb settings,
* supporting both global (~/.pi/kb/settings.json) and project-level (.kb/config.json)
* supporting both global (~/.pi/fusion/settings.json) and project-level (.fusion/config.json)
* settings for backup, migration, and sharing.
*/
@@ -24,9 +24,9 @@ export interface SettingsExportData {
exportedAt: string;
/** Source identifier (e.g., hostname, project path) */
source?: string;
/** Global settings (user-level, ~/.pi/kb/settings.json) */
/** Global settings (user-level, ~/.pi/fusion/settings.json) */
global?: GlobalSettings;
/** Project settings (project-level, .kb/config.json) */
/** Project settings (project-level, .fusion/config.json) */
project?: Partial<ProjectSettings>;
}

View File

@@ -55,7 +55,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
/**
* Hybrid storage note: task metadata lives in SQLite, while blob files remain on disk.
* Any write to `.kb/tasks/{id}` must recreate the directory on demand, and any read from
* Any write to `.fusion/tasks/{id}` must recreate the directory on demand, and any read from
* optional blob files must tolerate missing files/directories because cleanup, migration,
* or manual filesystem changes can remove them independently of the database row.
*/
@@ -81,7 +81,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
private taskLocks: Map<string, Promise<void>> = new Map();
/** Promise chain for serializing config.json read-modify-write cycles */
private configLock: Promise<void> = Promise.resolve();
/** Global settings store (`~/.pi/kb/settings.json`) */
/** Global settings store (`~/.pi/fusion/settings.json`) */
private globalSettingsStore: GlobalSettingsStore;
/** Polling interval for change detection */
private pollInterval: ReturnType<typeof setInterval> | null = null;
@@ -461,7 +461,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
* Get merged settings: global defaults ← global user prefs ← project overrides.
*
* Returns the combined view that most consumers should use. Project-level
* values in `.kb/config.json` override global values from `~/.pi/kb/settings.json`.
* values in `.fusion/config.json` override global values from `~/.pi/fusion/settings.json`.
*/
async getSettings(): Promise<Settings> {
const [globalSettings, config] = await Promise.all([
@@ -501,7 +501,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
}
/**
* Update project-level settings in `.kb/config.json`.
* Update project-level settings in `.fusion/config.json`.
*
* Accepts `Partial<Settings>` for backward compatibility. Any global-only
* fields in the patch are silently filtered out — they will not be persisted
@@ -530,7 +530,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
}
/**
* Update global (user-level) settings in `~/.pi/kb/settings.json`.
* Update global (user-level) settings in `~/.pi/fusion/settings.json`.
*
* These settings persist across all kb projects for the current user.
* Only fields defined in `GlobalSettings` are accepted.
@@ -1975,7 +1975,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
/**
* Append an agent log entry to the task's agent log file (JSONL format).
* Each entry is a single JSON line appended to `.kb/tasks/{ID}/agent.log`.
* Each entry is a single JSON line appended to `.fusion/tasks/{ID}/agent.log`.
* Also emits an `agent:log` event for live streaming.
*
* @param taskId - The task ID (e.g. "KB-001")

View File

@@ -44,11 +44,11 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
constructor(private rootDir: string, globalSettingsDir?: string) {
super();
this.setMaxListeners(100);
this.kbDir = join(rootDir, ".kb");
this.tasksDir = join(this.kbDir, "tasks");
this.configPath = join(this.kbDir, "config.json");
this.archiveLogPath = join(this.kbDir, "archive.jsonl");
this.activityLogPath = join(this.kbDir, "activity-log.jsonl");
this.fusionDir = join(rootDir, ".fusion");
this.tasksDir = join(this.fusionDir, "tasks");
this.configPath = join(this.fusionDir, "config.json");
this.archiveLogPath = join(this.fusionDir, "archive.jsonl");
this.activityLogPath = join(this.fusionDir, "activity-log.jsonl");
this.globalSettingsStore = new GlobalSettingsStore(globalSettingsDir);
}
@@ -240,7 +240,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
* Get merged settings: global defaults ← global user prefs ← project overrides.
*
* Returns the combined view that most consumers should use. Project-level
* values in `.kb/config.json` override global values from `~/.pi/kb/settings.json`.
* values in `.fusion/config.json` override global values from `~/.pi/kb/settings.json`.
*/
async getSettings(): Promise<Settings> {
const [globalSettings, config] = await Promise.all([
@@ -280,7 +280,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
}
/**
* Update project-level settings in `.kb/config.json`.
* Update project-level settings in `.fusion/config.json`.
*
* Accepts `Partial<Settings>` for backward compatibility. Any global-only
* fields in the patch are silently filtered out — they will not be persisted
@@ -1525,7 +1525,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
/**
* Append an agent log entry to the task's agent log file (JSONL format).
* Each entry is a single JSON line appended to `.kb/tasks/{ID}/agent.log`.
* Each entry is a single JSON line appended to `.fusion/tasks/{ID}/agent.log`.
* Also emits an `agent:log` event for live streaming.
*
* @param taskId - The task ID (e.g. "KB-001")
@@ -2155,7 +2155,7 @@ ${notificationsSection}`;
/**
* Record an activity log entry to the global activity log.
* Appends to .kb/activity-log.jsonl (JSON Lines format).
* Appends to .fusion/activity-log.jsonl (JSON Lines format).
* Auto-generates ID and timestamp.
*/
async recordActivity(entry: Omit<ActivityLogEntry, "id" | "timestamp">): Promise<ActivityLogEntry> {

View File

@@ -44,7 +44,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
constructor(private rootDir: string, globalSettingsDir?: string) {
super();
this.setMaxListeners(100);
this.kbDir = join(rootDir, ".kb");
this.tasksDir = join(this.kbDir, "tasks");
this.configPath = join(this.kbDir, "config.json");
this.archiveLogPath = join(this.kbDir, "archive.jsonl");
this.fusionDir = join(rootDir, ".fusion");
this.tasksDir = join(this.fusionDir, "tasks");
this.configPath = join(this.fusionDir, "config.json");
this.archiveLogPath = join(this.fusionDir, "archive.jsonl");

View File

@@ -488,12 +488,12 @@ export interface TaskCreateInput {
//
// Settings are split into two scopes:
//
// 1. **GlobalSettings** — User preferences stored in `~/.pi/kb/settings.json`.
// 1. **GlobalSettings** — User preferences stored in `~/.pi/fusion/settings.json`.
// These persist across all kb projects for the current user (theme, default
// AI models, notification preferences).
//
// 2. **ProjectSettings** — Project-specific workflow and resource settings stored
// in `.kb/config.json`. These control how the engine operates for this
// in `.fusion/config.json`. These control how the engine operates for this
// particular project (concurrency, merge strategy, worktree management, etc.).
//
// The merged view (`Settings`) combines both scopes: project values override
@@ -507,7 +507,7 @@ export interface TaskCreateInput {
export type SettingsScope = "global" | "project";
/**
* Global (user-level) settings stored in `~/.pi/kb/settings.json`.
* Global (user-level) settings stored in `~/.pi/fusion/settings.json`.
*
* These are user preferences that persist across all kb projects.
* The dashboard UI shows these under a "Global" section.
@@ -556,7 +556,7 @@ export interface GlobalSettings {
}
/**
* Project-level settings stored in `.kb/config.json`.
* Project-level settings stored in `.fusion/config.json`.
*
* These control how the engine operates for this particular project:
* concurrency, merge strategy, worktree management, build/test commands, etc.
@@ -672,7 +672,7 @@ export interface ProjectSettings {
autoBackupSchedule?: string;
/** Number of backup files to retain (oldest deleted when exceeded). Default: 7. */
autoBackupRetention?: number;
/** Directory for backup files, relative to project root. Default: ".kb/backups". */
/** Directory for backup files, relative to project root. Default: ".fusion/backups". */
autoBackupDir?: string;
/** When true, tasks created without titles but with descriptions longer than 140
* characters will automatically receive an AI-generated title (max 60 chars).
@@ -754,7 +754,7 @@ export const DEFAULT_PROJECT_SETTINGS: ProjectSettings = {
autoBackupEnabled: false,
autoBackupSchedule: "0 2 * * *",
autoBackupRetention: 7,
autoBackupDir: ".kb/backups",
autoBackupDir: ".fusion/backups",
autoSummarizeTitles: false,
titleSummarizerProvider: undefined,
titleSummarizerModelId: undefined,
@@ -1137,7 +1137,7 @@ export interface DetectedProject {
path: string;
/** Auto-generated or derived project name */
name: string;
/** Whether the project has a valid kb.db */
/** Whether the project has a valid fusion.db */
hasDb: boolean;
}