Просмотр исходного кода

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.
Jesse Vincent 11 месяцев назад
Родитель
Сommit
a48a7e5b1b

+ 6 - 6
skills/collaboration/remembering-conversations/DEPLOYMENT.md

@@ -77,10 +77,10 @@ If issues found:
 ls -l ~/.claude/hooks/sessionEnd
 
 # Check recent conversations
-ls -lt ~/.clank/conversation-archive/*/*.jsonl | head -5
+ls -lt ~/.config/superpowers/conversation-archive/*/*.jsonl | head -5
 
 # Check database size
-ls -lh ~/.clank/conversation-index/db.sqlite
+ls -lh ~/.config/superpowers/conversation-index/db.sqlite
 
 # Full verification
 ./index-conversations --verify
@@ -190,14 +190,14 @@ sleep 60 && ./index-conversations --repair
 ./index-conversations --verify
 
 # 2. Check database exists and has data
-ls -lh ~/.clank/conversation-index/db.sqlite
+ls -lh ~/.config/superpowers/conversation-index/db.sqlite
 # Should be > 100KB if conversations indexed
 
 # 3. Try text search (exact match)
 ./search-conversations --text "exact phrase from conversation"
 
 # 4. Check for corruption
-sqlite3 ~/.clank/conversation-index/db.sqlite "SELECT COUNT(*) FROM exchanges;"
+sqlite3 ~/.config/superpowers/conversation-index/db.sqlite "SELECT COUNT(*) FROM exchanges;"
 # Should show number > 0
 ```
 
@@ -221,7 +221,7 @@ rm -rf ~/.cache/transformers  # Force re-download
 **Fix:**
 ```bash
 # 1. Backup current database
-cp ~/.clank/conversation-index/db.sqlite ~/.clank/conversation-index/db.sqlite.backup
+cp ~/.config/superpowers/conversation-index/db.sqlite ~/.config/superpowers/conversation-index/db.sqlite.backup
 
 # 2. Rebuild from scratch
 ./index-conversations --rebuild
@@ -300,7 +300,7 @@ cp ~/.clank/conversation-index/db.sqlite ~/.clank/conversation-index/db.sqlite.b
         └── prompts/
             └── search-agent.md    # Subagent template
 
-~/.clank/
+~/.config/superpowers/
 ├── conversation-archive/          # Archived conversations
 │   └── <project>/
 │       ├── <uuid>.jsonl          # Conversation file

+ 7 - 7
skills/collaboration/remembering-conversations/INDEXING.md

@@ -25,7 +25,7 @@ Index, archive, and maintain conversations for search.
 - **Semantic search** across all past conversations
 - **AI summaries** (Claude Haiku with Sonnet fallback)
 - **Recovery modes** (verify, repair, rebuild)
-- **Permanent archive** at `~/.clank/conversation-archive/`
+- **Permanent archive** at `~/.config/superpowers/conversation-archive/`
 
 ## Setup
 
@@ -89,7 +89,7 @@ Handles existing hooks gracefully (merge or replace). Runs in background after e
 
 **Summaries failing:**
 - Check API key: `echo $ANTHROPIC_API_KEY`
-- Check logs in ~/.clank/conversation-index/
+- Check logs in ~/.config/superpowers/conversation-index/
 - Try manual: `./index-conversations --session <uuid>`
 
 **Search not finding results:**
@@ -101,7 +101,7 @@ Handles existing hooks gracefully (merge or replace). Runs in background after e
 
 To exclude specific projects from indexing (e.g., meta-conversations), create:
 
-`~/.clank/conversation-index/exclude.txt`
+`~/.config/superpowers/conversation-index/exclude.txt`
 ```
 # One project name per line
 # Lines starting with # are comments
@@ -115,10 +115,10 @@ export CONVERSATION_SEARCH_EXCLUDE_PROJECTS="project1,project2"
 
 ## Storage
 
-- **Archive:** `~/.clank/conversation-archive/<project>/<uuid>.jsonl`
-- **Summaries:** `~/.clank/conversation-archive/<project>/<uuid>-summary.txt`
-- **Database:** `~/.clank/conversation-index/db.sqlite`
-- **Exclusions:** `~/.clank/conversation-index/exclude.txt` (optional)
+- **Archive:** `~/.config/superpowers/conversation-archive/<project>/<uuid>.jsonl`
+- **Summaries:** `~/.config/superpowers/conversation-archive/<project>/<uuid>-summary.txt`
+- **Database:** `~/.config/superpowers/conversation-index/db.sqlite`
+- **Exclusions:** `~/.config/superpowers/conversation-index/exclude.txt` (optional)
 
 ## Technical Details
 

+ 124 - 0
skills/collaboration/remembering-conversations/tool/migrate-to-config.sh

@@ -0,0 +1,124 @@
+#!/usr/bin/env bash
+# Migrate conversation archive and index from ~/.clank to ~/.config/superpowers
+#
+# IMPORTANT: This preserves all data. The old ~/.clank directory is not deleted,
+# allowing you to verify the migration before removing it manually.
+
+set -euo pipefail
+
+# Determine target directory
+SUPERPOWERS_DIR="${PERSONAL_SUPERPOWERS_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/superpowers}"
+
+OLD_ARCHIVE="$HOME/.clank/conversation-archive"
+OLD_INDEX="$HOME/.clank/conversation-index"
+
+NEW_ARCHIVE="${SUPERPOWERS_DIR}/conversation-archive"
+NEW_INDEX="${SUPERPOWERS_DIR}/conversation-index"
+
+echo "Migration: ~/.clank → ${SUPERPOWERS_DIR}"
+echo ""
+
+# Check if source exists
+if [[ ! -d "$HOME/.clank" ]]; then
+    echo "✅ No ~/.clank directory found. Nothing to migrate."
+    exit 0
+fi
+
+# Check if already migrated
+if [[ -d "$NEW_ARCHIVE" ]] || [[ -d "$NEW_INDEX" ]]; then
+    echo "⚠️  Destination already exists:"
+    [[ -d "$NEW_ARCHIVE" ]] && echo "  - ${NEW_ARCHIVE}"
+    [[ -d "$NEW_INDEX" ]] && echo "  - ${NEW_INDEX}"
+    echo ""
+    echo "Migration appears to have already run."
+    echo "To re-run migration, manually remove destination directories first."
+    exit 1
+fi
+
+# Show what will be migrated
+echo "Source directories:"
+if [[ -d "$OLD_ARCHIVE" ]]; then
+    archive_size=$(du -sh "$OLD_ARCHIVE" | cut -f1)
+    archive_count=$(find "$OLD_ARCHIVE" -name "*.jsonl" | wc -l | tr -d ' ')
+    echo "  Archive: ${OLD_ARCHIVE} (${archive_count} conversations, ${archive_size})"
+else
+    echo "  Archive: Not found"
+fi
+
+if [[ -d "$OLD_INDEX" ]]; then
+    index_size=$(du -sh "$OLD_INDEX" | cut -f1)
+    echo "  Index: ${OLD_INDEX} (${index_size})"
+else
+    echo "  Index: Not found"
+fi
+
+echo ""
+echo "Destination: ${SUPERPOWERS_DIR}"
+echo ""
+
+# Confirm
+read -p "Proceed with migration? [y/N] " -n 1 -r
+echo
+if [[ ! $REPLY =~ ^[Yy]$ ]]; then
+    echo "Migration cancelled."
+    exit 0
+fi
+
+# Ensure destination base exists
+mkdir -p "${SUPERPOWERS_DIR}"
+
+# Migrate archive
+if [[ -d "$OLD_ARCHIVE" ]]; then
+    echo "Copying conversation archive..."
+    cp -r "$OLD_ARCHIVE" "$NEW_ARCHIVE"
+    echo "  ✓ Archive migrated"
+fi
+
+# Migrate index
+if [[ -d "$OLD_INDEX" ]]; then
+    echo "Copying conversation index..."
+    cp -r "$OLD_INDEX" "$NEW_INDEX"
+    echo "  ✓ Index migrated"
+fi
+
+# Update database paths to point to new location
+if [[ -f "$NEW_INDEX/db.sqlite" ]]; then
+    echo "Updating database paths..."
+    sqlite3 "$NEW_INDEX/db.sqlite" "UPDATE exchanges SET archive_path = REPLACE(archive_path, '/.clank/', '/.config/superpowers/') WHERE archive_path LIKE '%/.clank/%';"
+    echo "  ✓ Database paths updated"
+fi
+
+# Verify migration
+echo ""
+echo "Verifying migration..."
+
+if [[ -d "$OLD_ARCHIVE" ]]; then
+    old_count=$(find "$OLD_ARCHIVE" -name "*.jsonl" | wc -l | tr -d ' ')
+    new_count=$(find "$NEW_ARCHIVE" -name "*.jsonl" | wc -l | tr -d ' ')
+
+    if [[ "$old_count" -eq "$new_count" ]]; then
+        echo "  ✓ All $new_count conversations migrated"
+    else
+        echo "  ⚠️  Conversation count mismatch: old=$old_count, new=$new_count"
+        exit 1
+    fi
+fi
+
+if [[ -f "$OLD_INDEX/db.sqlite" ]]; then
+    old_size=$(stat -f%z "$OLD_INDEX/db.sqlite" 2>/dev/null || stat --format=%s "$OLD_INDEX/db.sqlite" 2>/dev/null)
+    new_size=$(stat -f%z "$NEW_INDEX/db.sqlite" 2>/dev/null || stat --format=%s "$NEW_INDEX/db.sqlite" 2>/dev/null)
+    echo "  ✓ Database migrated (${new_size} bytes)"
+fi
+
+echo ""
+echo "✅ Migration complete!"
+echo ""
+echo "Next steps:"
+echo "  1. Test search: ./search-conversations 'test query'"
+echo "  2. Verify results look correct"
+echo "  3. Once verified, manually remove old directory:"
+echo "     rm -rf ~/.clank"
+echo ""
+echo "The old ~/.clank directory is preserved for safety."
+
+exit 0

+ 1 - 5
skills/collaboration/remembering-conversations/tool/src/db.ts

@@ -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(`

+ 3 - 3
skills/collaboration/remembering-conversations/tool/src/index-cli.ts

@@ -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) {

+ 5 - 8
skills/collaboration/remembering-conversations/tool/src/indexer.ts

@@ -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();
 

+ 56 - 0
skills/collaboration/remembering-conversations/tool/src/paths.ts

@@ -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');
+}

+ 1 - 6
skills/collaboration/remembering-conversations/tool/src/verify.ts

@@ -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: [],