Migrate conversation search to use PERSONAL_SUPERPOWERS_DIR

Updates conversation indexing and search to use the new personal superpowers
directory structure with environment variable support.

Changes:
- Added src/paths.ts for centralized directory resolution
- Updated db.ts, indexer.ts, verify.ts to use paths.ts
- Created migrate-to-config.sh for data migration from ~/.clank
- Updated all documentation references from ~/.clank to ~/.config/superpowers
- Database paths automatically updated during migration

Migration tested with 5,017 conversations and 6,385 exchanges.
This commit is contained in:
Jesse Vincent
2025-10-10 17:14:40 -07:00
parent 6c0ab4cfec
commit a48a7e5b1b
8 changed files with 203 additions and 35 deletions

View File

@@ -1,13 +1,9 @@
import Database from 'better-sqlite3';
import { ConversationExchange } from './types.js';
import path from 'path';
import os from 'os';
import fs from 'fs';
import * as sqliteVec from 'sqlite-vec';
function getDbPath(): string {
return process.env.TEST_DB_PATH || path.join(os.homedir(), '.clank', 'conversation-index', 'db.sqlite');
}
import { getDbPath } from './paths.js';
export function migrateSchema(db: Database.Database): void {
const hasColumn = db.prepare(`

View File

@@ -2,9 +2,9 @@
import { verifyIndex, repairIndex } from './verify.js';
import { indexSession, indexUnprocessed, indexConversations } from './indexer.js';
import { initDatabase } from './db.js';
import { getDbPath, getArchiveDir } from './paths.js';
import fs from 'fs';
import path from 'path';
import os from 'os';
const command = process.argv[2];
@@ -74,14 +74,14 @@ async function main() {
console.log('Rebuilding entire index...');
// Delete database
const dbPath = path.join(os.homedir(), '.clank', 'conversation-index', 'db.sqlite');
const dbPath = getDbPath();
if (fs.existsSync(dbPath)) {
fs.unlinkSync(dbPath);
console.log('Deleted existing database');
}
// Delete all summary files
const archiveDir = path.join(os.homedir(), '.clank', 'conversation-archive');
const archiveDir = getArchiveDir();
if (fs.existsSync(archiveDir)) {
const projects = fs.readdirSync(archiveDir);
for (const project of projects) {

View File

@@ -6,6 +6,7 @@ import { parseConversation } from './parser.js';
import { initEmbeddings, generateExchangeEmbedding } from './embeddings.js';
import { summarizeConversation } from './summarizer.js';
import { ConversationExchange } from './types.js';
import { getArchiveDir, getExcludeConfigPath } from './paths.js';
// Set max output tokens for Claude SDK (used by summarizer)
process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS = '20000';
@@ -19,10 +20,6 @@ function getProjectsDir(): string {
return process.env.TEST_PROJECTS_DIR || path.join(os.homedir(), '.claude', 'projects');
}
function getArchiveDir(): string {
return process.env.TEST_ARCHIVE_DIR || path.join(os.homedir(), '.clank', 'conversation-archive');
}
// Projects to exclude from indexing (configurable via env or config file)
function getExcludedProjects(): string[] {
// Check env variable first
@@ -31,7 +28,7 @@ function getExcludedProjects(): string[] {
}
// Check for config file
const configPath = path.join(os.homedir(), '.clank', 'conversation-index', 'exclude.txt');
const configPath = getExcludeConfigPath();
if (fs.existsSync(configPath)) {
const content = fs.readFileSync(configPath, 'utf-8');
return content.split('\n').map(line => line.trim()).filter(line => line && !line.startsWith('#'));
@@ -71,7 +68,7 @@ export async function indexConversations(
console.log('Scanning for conversation files...');
const PROJECTS_DIR = getProjectsDir();
const ARCHIVE_DIR = getArchiveDir();
const ARCHIVE_DIR = getArchiveDir(); // Now uses paths.ts
const projects = fs.readdirSync(PROJECTS_DIR);
let totalExchanges = 0;
@@ -195,7 +192,7 @@ export async function indexSession(sessionId: string, concurrency: number = 1):
// Find the conversation file for this session
const PROJECTS_DIR = getProjectsDir();
const ARCHIVE_DIR = getArchiveDir();
const ARCHIVE_DIR = getArchiveDir(); // Now uses paths.ts
const projects = fs.readdirSync(PROJECTS_DIR);
const excludedProjects = getExcludedProjects();
let found = false;
@@ -268,7 +265,7 @@ export async function indexUnprocessed(concurrency: number = 1): Promise<void> {
await initEmbeddings();
const PROJECTS_DIR = getProjectsDir();
const ARCHIVE_DIR = getArchiveDir();
const ARCHIVE_DIR = getArchiveDir(); // Now uses paths.ts
const projects = fs.readdirSync(PROJECTS_DIR);
const excludedProjects = getExcludedProjects();

View File

@@ -0,0 +1,56 @@
import os from 'os';
import path from 'path';
/**
* Get the personal superpowers directory
*
* Precedence:
* 1. PERSONAL_SUPERPOWERS_DIR env var (if set)
* 2. XDG_CONFIG_HOME/superpowers (if XDG_CONFIG_HOME is set)
* 3. ~/.config/superpowers (default)
*/
export function getSuperpowersDir(): string {
if (process.env.PERSONAL_SUPERPOWERS_DIR) {
return process.env.PERSONAL_SUPERPOWERS_DIR;
}
const xdgConfigHome = process.env.XDG_CONFIG_HOME;
if (xdgConfigHome) {
return path.join(xdgConfigHome, 'superpowers');
}
return path.join(os.homedir(), '.config', 'superpowers');
}
/**
* Get conversation archive directory
*/
export function getArchiveDir(): string {
// Allow test override
if (process.env.TEST_ARCHIVE_DIR) {
return process.env.TEST_ARCHIVE_DIR;
}
return path.join(getSuperpowersDir(), 'conversation-archive');
}
/**
* Get conversation index directory
*/
export function getIndexDir(): string {
return path.join(getSuperpowersDir(), 'conversation-index');
}
/**
* Get database path
*/
export function getDbPath(): string {
return path.join(getIndexDir(), 'db.sqlite');
}
/**
* Get exclude config path
*/
export function getExcludeConfigPath(): string {
return path.join(getIndexDir(), 'exclude.txt');
}

View File

@@ -1,8 +1,8 @@
import fs from 'fs';
import path from 'path';
import os from 'os';
import { parseConversation } from './parser.js';
import { initDatabase, getAllExchanges, getFileLastIndexed } from './db.js';
import { getArchiveDir } from './paths.js';
export interface VerificationResult {
missing: Array<{ path: string; reason: string }>;
@@ -11,11 +11,6 @@ export interface VerificationResult {
corrupted: Array<{ path: string; error: string }>;
}
// Allow overriding paths for testing
function getArchiveDir(): string {
return process.env.TEST_ARCHIVE_DIR || path.join(os.homedir(), '.clank', 'conversation-archive');
}
export async function verifyIndex(): Promise<VerificationResult> {
const result: VerificationResult = {
missing: [],