mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-12 02:37:45 +08:00
feat: Add core memory clustering visualization and hooks configuration
- Implemented core memory clustering visualization in core-memory-clusters.js - Added functions for loading, rendering, and managing clusters and their members - Created example hooks configuration in hooks-config-example.json for session management - Developed test script for hooks integration in test-hooks.js - Included error handling and notifications for user interactions
This commit is contained in:
@@ -77,7 +77,7 @@ export async function handleCodexLensRoutes(ctx: RouteContext): Promise<boolean>
|
||||
// API: CodexLens Index List - Get all indexed projects with details
|
||||
if (pathname === '/api/codexlens/indexes') {
|
||||
try {
|
||||
// First get config to find index directory
|
||||
// Get config for index directory path
|
||||
const configResult = await executeCodexLens(['config', '--json']);
|
||||
let indexDir = '';
|
||||
|
||||
@@ -85,109 +85,127 @@ export async function handleCodexLensRoutes(ctx: RouteContext): Promise<boolean>
|
||||
try {
|
||||
const config = extractJSON(configResult.output);
|
||||
if (config.success && config.result) {
|
||||
indexDir = config.result.index_root || '';
|
||||
// CLI returns index_dir (not index_root)
|
||||
indexDir = config.result.index_dir || config.result.index_root || '';
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[CodexLens] Failed to parse config for index list:', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Get detailed status including projects
|
||||
const statusResult = await executeCodexLens(['status', '--json']);
|
||||
// Get project list using 'projects list' command
|
||||
const projectsResult = await executeCodexLens(['projects', 'list', '--json']);
|
||||
let indexes: any[] = [];
|
||||
let totalSize = 0;
|
||||
let vectorIndexCount = 0;
|
||||
let normalIndexCount = 0;
|
||||
|
||||
if (projectsResult.success) {
|
||||
try {
|
||||
const projectsData = extractJSON(projectsResult.output);
|
||||
if (projectsData.success && Array.isArray(projectsData.result)) {
|
||||
const { statSync, existsSync } = await import('fs');
|
||||
const { basename, join } = await import('path');
|
||||
|
||||
for (const project of projectsData.result) {
|
||||
// Skip test/temp projects
|
||||
if (project.source_root && (
|
||||
project.source_root.includes('\\Temp\\') ||
|
||||
project.source_root.includes('/tmp/') ||
|
||||
project.total_files === 0
|
||||
)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let projectSize = 0;
|
||||
let hasVectorIndex = false;
|
||||
let hasNormalIndex = true; // All projects have FTS index
|
||||
let lastModified = null;
|
||||
|
||||
// Try to get actual index size from index_root
|
||||
if (project.index_root && existsSync(project.index_root)) {
|
||||
try {
|
||||
const { readdirSync } = await import('fs');
|
||||
const files = readdirSync(project.index_root);
|
||||
for (const file of files) {
|
||||
try {
|
||||
const filePath = join(project.index_root, file);
|
||||
const stat = statSync(filePath);
|
||||
projectSize += stat.size;
|
||||
if (!lastModified || stat.mtime > lastModified) {
|
||||
lastModified = stat.mtime;
|
||||
}
|
||||
// Check for vector/embedding files
|
||||
if (file.includes('vector') || file.includes('embedding') ||
|
||||
file.endsWith('.faiss') || file.endsWith('.npy') ||
|
||||
file.includes('semantic_chunks')) {
|
||||
hasVectorIndex = true;
|
||||
}
|
||||
} catch (e) {
|
||||
// Skip files we can't stat
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Can't read index directory
|
||||
}
|
||||
}
|
||||
|
||||
if (hasVectorIndex) vectorIndexCount++;
|
||||
if (hasNormalIndex) normalIndexCount++;
|
||||
totalSize += projectSize;
|
||||
|
||||
// Use source_root as the display name
|
||||
const displayName = project.source_root ? basename(project.source_root) : `project_${project.id}`;
|
||||
|
||||
indexes.push({
|
||||
id: displayName,
|
||||
path: project.source_root || '',
|
||||
indexPath: project.index_root || '',
|
||||
size: projectSize,
|
||||
sizeFormatted: formatSize(projectSize),
|
||||
fileCount: project.total_files || 0,
|
||||
dirCount: project.total_dirs || 0,
|
||||
hasVectorIndex,
|
||||
hasNormalIndex,
|
||||
status: project.status || 'active',
|
||||
lastModified: lastModified ? lastModified.toISOString() : null
|
||||
});
|
||||
}
|
||||
|
||||
// Sort by file count (most files first), then by name
|
||||
indexes.sort((a, b) => {
|
||||
if (b.fileCount !== a.fileCount) return b.fileCount - a.fileCount;
|
||||
return a.id.localeCompare(b.id);
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[CodexLens] Failed to parse projects list:', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Also get summary stats from status command
|
||||
const statusResult = await executeCodexLens(['status', '--json']);
|
||||
let statusSummary: any = {};
|
||||
|
||||
if (statusResult.success) {
|
||||
try {
|
||||
const status = extractJSON(statusResult.output);
|
||||
if (status.success && status.result) {
|
||||
const projectsCount = status.result.projects_count || 0;
|
||||
|
||||
// Try to get project list from index directory
|
||||
if (indexDir) {
|
||||
const { readdirSync, statSync, existsSync } = await import('fs');
|
||||
const { join } = await import('path');
|
||||
const { homedir } = await import('os');
|
||||
|
||||
// Expand ~ in path
|
||||
const expandedDir = indexDir.startsWith('~')
|
||||
? join(homedir(), indexDir.slice(1))
|
||||
: indexDir;
|
||||
|
||||
if (existsSync(expandedDir)) {
|
||||
try {
|
||||
const entries = readdirSync(expandedDir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
if (entry.isDirectory()) {
|
||||
const projectDir = join(expandedDir, entry.name);
|
||||
let projectSize = 0;
|
||||
let hasVectorIndex = false;
|
||||
let hasNormalIndex = false;
|
||||
let fileCount = 0;
|
||||
let lastModified = null;
|
||||
|
||||
try {
|
||||
// Check for index files
|
||||
const projectFiles = readdirSync(projectDir);
|
||||
for (const file of projectFiles) {
|
||||
const filePath = join(projectDir, file);
|
||||
try {
|
||||
const stat = statSync(filePath);
|
||||
projectSize += stat.size;
|
||||
fileCount++;
|
||||
if (!lastModified || stat.mtime > lastModified) {
|
||||
lastModified = stat.mtime;
|
||||
}
|
||||
|
||||
// Check index type
|
||||
if (file.includes('vector') || file.includes('embedding') || file.endsWith('.faiss') || file.endsWith('.npy')) {
|
||||
hasVectorIndex = true;
|
||||
}
|
||||
if (file.includes('fts') || file.endsWith('.db') || file.endsWith('.sqlite')) {
|
||||
hasNormalIndex = true;
|
||||
}
|
||||
} catch (e) {
|
||||
// Skip files we can't stat
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Can't read project directory
|
||||
}
|
||||
|
||||
if (hasVectorIndex) vectorIndexCount++;
|
||||
if (hasNormalIndex) normalIndexCount++;
|
||||
totalSize += projectSize;
|
||||
|
||||
indexes.push({
|
||||
id: entry.name,
|
||||
path: projectDir,
|
||||
size: projectSize,
|
||||
sizeFormatted: formatSize(projectSize),
|
||||
fileCount,
|
||||
hasVectorIndex,
|
||||
hasNormalIndex,
|
||||
lastModified: lastModified ? lastModified.toISOString() : null
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by last modified (most recent first)
|
||||
indexes.sort((a, b) => {
|
||||
if (!a.lastModified) return 1;
|
||||
if (!b.lastModified) return -1;
|
||||
return new Date(b.lastModified).getTime() - new Date(a.lastModified).getTime();
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('[CodexLens] Failed to read index directory:', e.message);
|
||||
}
|
||||
}
|
||||
statusSummary = {
|
||||
totalProjects: status.result.projects_count || indexes.length,
|
||||
totalFiles: status.result.total_files || 0,
|
||||
totalDirs: status.result.total_dirs || 0,
|
||||
indexSizeBytes: status.result.index_size_bytes || totalSize,
|
||||
indexSizeMb: status.result.index_size_mb || 0,
|
||||
embeddings: status.result.embeddings || {}
|
||||
};
|
||||
// Use status total size if available
|
||||
if (status.result.index_size_bytes) {
|
||||
totalSize = status.result.index_size_bytes;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[CodexLens] Failed to parse status for index list:', e.message);
|
||||
console.error('[CodexLens] Failed to parse status:', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,7 +219,8 @@ export async function handleCodexLensRoutes(ctx: RouteContext): Promise<boolean>
|
||||
totalSize,
|
||||
totalSizeFormatted: formatSize(totalSize),
|
||||
vectorIndexCount,
|
||||
normalIndexCount
|
||||
normalIndexCount,
|
||||
...statusSummary
|
||||
}
|
||||
}));
|
||||
} catch (err) {
|
||||
@@ -280,7 +299,8 @@ export async function handleCodexLensRoutes(ctx: RouteContext): Promise<boolean>
|
||||
try {
|
||||
const config = extractJSON(configResult.output);
|
||||
if (config.success && config.result) {
|
||||
responseData.index_dir = config.result.index_root || responseData.index_dir;
|
||||
// CLI returns index_dir (not index_root)
|
||||
responseData.index_dir = config.result.index_dir || config.result.index_root || responseData.index_dir;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[CodexLens] Failed to parse config:', e.message);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as http from 'http';
|
||||
import { URL } from 'url';
|
||||
import { getCoreMemoryStore } from '../core-memory-store.js';
|
||||
import type { CoreMemory } from '../core-memory-store.js';
|
||||
import type { CoreMemory, SessionCluster, ClusterMember, ClusterRelation } from '../core-memory-store.js';
|
||||
|
||||
/**
|
||||
* Route context interface
|
||||
@@ -197,5 +197,329 @@ export async function handleCoreMemoryRoutes(ctx: RouteContext): Promise<boolean
|
||||
return true;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Session Clustering API Endpoints
|
||||
// ============================================================
|
||||
|
||||
// API: Get all clusters
|
||||
if (pathname === '/api/core-memory/clusters' && req.method === 'GET') {
|
||||
const projectPath = url.searchParams.get('path') || initialPath;
|
||||
const status = url.searchParams.get('status') || undefined;
|
||||
|
||||
try {
|
||||
const store = getCoreMemoryStore(projectPath);
|
||||
const clusters = store.listClusters(status);
|
||||
|
||||
// Add member count to each cluster
|
||||
const clustersWithCount = clusters.map(c => ({
|
||||
...c,
|
||||
memberCount: store.getClusterMembers(c.id).length
|
||||
}));
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ success: true, clusters: clustersWithCount }));
|
||||
} catch (error: unknown) {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: (error as Error).message }));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// API: Get cluster detail with members
|
||||
if (pathname.match(/^\/api\/core-memory\/clusters\/[^\/]+$/) && req.method === 'GET') {
|
||||
const clusterId = pathname.split('/').pop()!;
|
||||
const projectPath = url.searchParams.get('path') || initialPath;
|
||||
|
||||
try {
|
||||
const store = getCoreMemoryStore(projectPath);
|
||||
const cluster = store.getCluster(clusterId);
|
||||
|
||||
if (!cluster) {
|
||||
res.writeHead(404, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: 'Cluster not found' }));
|
||||
return true;
|
||||
}
|
||||
|
||||
const members = store.getClusterMembers(clusterId);
|
||||
const relations = store.getClusterRelations(clusterId);
|
||||
|
||||
// Get metadata for each member
|
||||
const membersWithMetadata = members.map(m => ({
|
||||
...m,
|
||||
metadata: store.getSessionMetadata(m.session_id)
|
||||
}));
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
success: true,
|
||||
cluster,
|
||||
members: membersWithMetadata,
|
||||
relations
|
||||
}));
|
||||
} catch (error: unknown) {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: (error as Error).message }));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// API: Auto-cluster sessions
|
||||
if (pathname === '/api/core-memory/clusters/auto' && req.method === 'POST') {
|
||||
handlePostRequest(req, res, async (body) => {
|
||||
const { scope = 'recent', minClusterSize = 2, path: projectPath } = body;
|
||||
const basePath = projectPath || initialPath;
|
||||
|
||||
try {
|
||||
const { SessionClusteringService } = await import('../session-clustering-service.js');
|
||||
const service = new SessionClusteringService(basePath);
|
||||
|
||||
const validScope: 'all' | 'recent' | 'unclustered' =
|
||||
scope === 'all' || scope === 'recent' || scope === 'unclustered' ? scope : 'recent';
|
||||
|
||||
const result = await service.autocluster({
|
||||
scope: validScope,
|
||||
minClusterSize
|
||||
});
|
||||
|
||||
// Broadcast update event
|
||||
broadcastToClients({
|
||||
type: 'CLUSTERS_UPDATED',
|
||||
payload: {
|
||||
...result,
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
...result
|
||||
};
|
||||
} catch (error: unknown) {
|
||||
return { error: (error as Error).message, status: 500 };
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// API: Create new cluster
|
||||
if (pathname === '/api/core-memory/clusters' && req.method === 'POST') {
|
||||
handlePostRequest(req, res, async (body) => {
|
||||
const { name, description, intent, metadata, path: projectPath } = body;
|
||||
|
||||
if (!name) {
|
||||
return { error: 'name is required', status: 400 };
|
||||
}
|
||||
|
||||
const basePath = projectPath || initialPath;
|
||||
|
||||
try {
|
||||
const store = getCoreMemoryStore(basePath);
|
||||
const cluster = store.createCluster({
|
||||
name,
|
||||
description,
|
||||
intent,
|
||||
metadata: metadata ? JSON.stringify(metadata) : undefined
|
||||
});
|
||||
|
||||
// Broadcast update event
|
||||
broadcastToClients({
|
||||
type: 'CLUSTER_UPDATED',
|
||||
payload: {
|
||||
cluster,
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
cluster
|
||||
};
|
||||
} catch (error: unknown) {
|
||||
return { error: (error as Error).message, status: 500 };
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// API: Update cluster (supports both PUT and PATCH)
|
||||
if (pathname.match(/^\/api\/core-memory\/clusters\/[^\/]+$/) && (req.method === 'PUT' || req.method === 'PATCH')) {
|
||||
const clusterId = pathname.split('/').pop()!;
|
||||
|
||||
handlePostRequest(req, res, async (body) => {
|
||||
const { name, description, intent, status, metadata, path: projectPath } = body;
|
||||
const basePath = projectPath || initialPath;
|
||||
|
||||
try {
|
||||
const store = getCoreMemoryStore(basePath);
|
||||
const cluster = store.updateCluster(clusterId, {
|
||||
name,
|
||||
description,
|
||||
intent,
|
||||
status,
|
||||
metadata: metadata ? JSON.stringify(metadata) : undefined
|
||||
});
|
||||
|
||||
if (!cluster) {
|
||||
return { error: 'Cluster not found', status: 404 };
|
||||
}
|
||||
|
||||
// Broadcast update event
|
||||
broadcastToClients({
|
||||
type: 'CLUSTER_UPDATED',
|
||||
payload: {
|
||||
cluster,
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
cluster
|
||||
};
|
||||
} catch (error: unknown) {
|
||||
return { error: (error as Error).message, status: 500 };
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// API: Delete cluster
|
||||
if (pathname.match(/^\/api\/core-memory\/clusters\/[^\/]+$/) && req.method === 'DELETE') {
|
||||
const clusterId = pathname.split('/').pop()!;
|
||||
const projectPath = url.searchParams.get('path') || initialPath;
|
||||
|
||||
try {
|
||||
const store = getCoreMemoryStore(projectPath);
|
||||
const deleted = store.deleteCluster(clusterId);
|
||||
|
||||
if (!deleted) {
|
||||
res.writeHead(404, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: 'Cluster not found' }));
|
||||
return true;
|
||||
}
|
||||
|
||||
// Broadcast update event
|
||||
broadcastToClients({
|
||||
type: 'CLUSTER_UPDATED',
|
||||
payload: {
|
||||
clusterId,
|
||||
deleted: true,
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
});
|
||||
|
||||
res.writeHead(204, { 'Content-Type': 'application/json' });
|
||||
res.end();
|
||||
} catch (error: unknown) {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: (error as Error).message }));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// API: Add member to cluster
|
||||
if (pathname.match(/^\/api\/core-memory\/clusters\/[^\/]+\/members$/) && req.method === 'POST') {
|
||||
const clusterId = pathname.split('/')[4];
|
||||
|
||||
handlePostRequest(req, res, async (body) => {
|
||||
const { session_id, session_type, sequence_order, relevance_score, path: projectPath } = body;
|
||||
|
||||
if (!session_id || !session_type) {
|
||||
return { error: 'session_id and session_type are required', status: 400 };
|
||||
}
|
||||
|
||||
const basePath = projectPath || initialPath;
|
||||
|
||||
try {
|
||||
const store = getCoreMemoryStore(basePath);
|
||||
const member = store.addClusterMember({
|
||||
cluster_id: clusterId,
|
||||
session_id,
|
||||
session_type,
|
||||
sequence_order: sequence_order ?? 0,
|
||||
relevance_score: relevance_score ?? 1.0
|
||||
});
|
||||
|
||||
// Broadcast update event
|
||||
broadcastToClients({
|
||||
type: 'CLUSTER_UPDATED',
|
||||
payload: {
|
||||
clusterId,
|
||||
member,
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
member
|
||||
};
|
||||
} catch (error: unknown) {
|
||||
return { error: (error as Error).message, status: 500 };
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// API: Remove member from cluster
|
||||
if (pathname.match(/^\/api\/core-memory\/clusters\/[^\/]+\/members\/[^\/]+$/) && req.method === 'DELETE') {
|
||||
const parts = pathname.split('/');
|
||||
const clusterId = parts[4];
|
||||
const sessionId = parts[6];
|
||||
const projectPath = url.searchParams.get('path') || initialPath;
|
||||
|
||||
try {
|
||||
const store = getCoreMemoryStore(projectPath);
|
||||
const removed = store.removeClusterMember(clusterId, sessionId);
|
||||
|
||||
if (!removed) {
|
||||
res.writeHead(404, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: 'Member not found' }));
|
||||
return true;
|
||||
}
|
||||
|
||||
// Broadcast update event
|
||||
broadcastToClients({
|
||||
type: 'CLUSTER_UPDATED',
|
||||
payload: {
|
||||
clusterId,
|
||||
removedSessionId: sessionId,
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
});
|
||||
|
||||
res.writeHead(204, { 'Content-Type': 'application/json' });
|
||||
res.end();
|
||||
} catch (error: unknown) {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: (error as Error).message }));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// API: Search sessions by keyword
|
||||
if (pathname === '/api/core-memory/sessions/search' && req.method === 'GET') {
|
||||
const keyword = url.searchParams.get('q') || '';
|
||||
const projectPath = url.searchParams.get('path') || initialPath;
|
||||
|
||||
if (!keyword) {
|
||||
res.writeHead(400, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: 'Query parameter q is required' }));
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
const store = getCoreMemoryStore(projectPath);
|
||||
const results = store.searchSessionsByKeyword(keyword);
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ success: true, results }));
|
||||
} catch (error: unknown) {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: (error as Error).message }));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -138,8 +138,16 @@ function findAllIndexDbs(dir: string): string[] {
|
||||
|
||||
/**
|
||||
* Map codex-lens symbol kinds to graph node types
|
||||
* Returns null for non-code symbols (markdown headings, etc.)
|
||||
*/
|
||||
function mapSymbolKind(kind: string): string {
|
||||
function mapSymbolKind(kind: string): string | null {
|
||||
const kindLower = kind.toLowerCase();
|
||||
|
||||
// Exclude markdown headings
|
||||
if (/^h[1-6]$/.test(kindLower)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const kindMap: Record<string, string> = {
|
||||
'function': 'FUNCTION',
|
||||
'class': 'CLASS',
|
||||
@@ -148,8 +156,13 @@ function mapSymbolKind(kind: string): string {
|
||||
'module': 'MODULE',
|
||||
'interface': 'CLASS', // TypeScript interfaces as CLASS
|
||||
'type': 'CLASS', // Type aliases as CLASS
|
||||
'constant': 'VARIABLE',
|
||||
'property': 'VARIABLE',
|
||||
'parameter': 'VARIABLE',
|
||||
'import': 'MODULE',
|
||||
'export': 'MODULE',
|
||||
};
|
||||
return kindMap[kind.toLowerCase()] || 'VARIABLE';
|
||||
return kindMap[kindLower] || 'VARIABLE';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -224,13 +237,19 @@ async function querySymbols(projectPath: string, fileFilter?: string, moduleFilt
|
||||
|
||||
db.close();
|
||||
|
||||
allNodes.push(...rows.map((row: any) => ({
|
||||
id: `${row.file}:${row.name}:${row.start_line}`,
|
||||
name: row.name,
|
||||
type: mapSymbolKind(row.kind),
|
||||
file: row.file,
|
||||
line: row.start_line,
|
||||
})));
|
||||
// Filter out non-code symbols (markdown headings, etc.)
|
||||
rows.forEach((row: any) => {
|
||||
const type = mapSymbolKind(row.kind);
|
||||
if (type !== null) {
|
||||
allNodes.push({
|
||||
id: `${row.file}:${row.name}:${row.start_line}`,
|
||||
name: row.name,
|
||||
type,
|
||||
file: row.file,
|
||||
line: row.start_line,
|
||||
});
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
console.error(`[Graph] Failed to query symbols from ${dbPath}: ${message}`);
|
||||
|
||||
@@ -202,6 +202,46 @@ export async function handleHooksRoutes(ctx: RouteContext): Promise<boolean> {
|
||||
resolvedSessionId = extractSessionIdFromPath(filePath);
|
||||
}
|
||||
|
||||
// Handle context hooks (session-start, context)
|
||||
if (type === 'session-start' || type === 'context') {
|
||||
try {
|
||||
const projectPath = url.searchParams.get('path') || initialPath;
|
||||
const { SessionClusteringService } = await import('../session-clustering-service.js');
|
||||
const clusteringService = new SessionClusteringService(projectPath);
|
||||
|
||||
const format = url.searchParams.get('format') || 'markdown';
|
||||
|
||||
// Pass type and prompt to getProgressiveIndex
|
||||
// session-start: returns recent sessions by time
|
||||
// context: returns intent-matched sessions based on prompt
|
||||
const index = await clusteringService.getProgressiveIndex({
|
||||
type: type as 'session-start' | 'context',
|
||||
sessionId: resolvedSessionId,
|
||||
prompt: extraData.prompt // Pass user prompt for intent matching
|
||||
});
|
||||
|
||||
// Return context directly
|
||||
return {
|
||||
success: true,
|
||||
type: 'context',
|
||||
format,
|
||||
content: index,
|
||||
sessionId: resolvedSessionId
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[Hooks] Failed to generate context:', error);
|
||||
// Return empty content on failure (fail silently)
|
||||
return {
|
||||
success: true,
|
||||
type: 'context',
|
||||
format: 'markdown',
|
||||
content: '',
|
||||
sessionId: resolvedSessionId,
|
||||
error: (error as Error).message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Broadcast to all connected WebSocket clients
|
||||
const notification = {
|
||||
type: type || 'session_updated',
|
||||
|
||||
Reference in New Issue
Block a user