sync: Phase 3 Agent Features from private repo

Features synced:
- LucideIconPicker component (115 icons for dimension customization)
- Dimension editing modal (descriptions + icons)
- Compound filters (per-column AND filters)
- Improved dimension assignment with keyword tagging
- queryDimensions and getDimension agent tools

New files:
- src/components/common/LucideIconPicker.tsx
- src/tools/database/queryDimensions.ts
- src/tools/database/getDimension.ts

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
“BeeRad”
2025-12-25 10:05:02 +11:00
co-authored by Claude Opus 4.5
parent 57a20899a5
commit c9d85998c1
8 changed files with 1630 additions and 174 deletions
+9 -4
View File
@@ -66,15 +66,20 @@ export async function POST(request: NextRequest) {
.filter(Boolean) .filter(Boolean)
.slice(0, 5); .slice(0, 5);
// Auto-assign locked dimensions for all new nodes // Auto-assign locked dimensions + keyword dimensions for all new nodes
const autoAssignedDimensions = await DimensionService.assignLockedDimensions({ const { locked, keywords } = await DimensionService.assignDimensions({
title: body.title, title: body.title,
content: rawContent || undefined, content: rawContent || undefined,
link: body.link link: body.link
}); });
// Combine provided and auto-assigned dimensions, remove duplicates // Ensure keyword dimensions exist in the database (create if new)
const finalDimensions = [...new Set([...trimmedProvidedDimensions, ...autoAssignedDimensions])] for (const keyword of keywords) {
await DimensionService.ensureKeywordDimension(keyword);
}
// Combine provided, locked, and keyword dimensions, remove duplicates
const finalDimensions = [...new Set([...trimmedProvidedDimensions, ...locked, ...keywords])]
.slice(0, 5); // Ensure we don't exceed 5 total dimensions .slice(0, 5); // Ensure we don't exceed 5 total dimensions
const rawChunk = typeof body.chunk === 'string' ? body.chunk : null; const rawChunk = typeof body.chunk === 'string' ? body.chunk : null;
let chunkToStore = rawChunk; let chunkToStore = rawChunk;
+418
View File
@@ -0,0 +1,418 @@
"use client";
import { useState, useMemo } from 'react';
import {
// Core
Folder,
BookOpen,
Lightbulb,
Star,
Heart,
Briefcase,
Code,
FileText,
Globe,
Hash,
Inbox,
Layers,
Link,
MessageSquare,
Music,
Paperclip,
Search,
Settings,
Tag,
User,
Video,
Zap,
Archive,
Award,
Bell,
Bookmark,
Calendar,
Camera,
Clock,
Cloud,
Coffee,
Compass,
Database,
Flag,
Gift,
Home,
Image,
Key,
Mail,
Map,
Mic,
Package,
Smile,
Target,
// Thinking & Ideas
Brain,
Sparkles,
Wand2,
FlaskConical,
Atom,
// Content & Media
Newspaper,
Podcast,
Youtube,
Twitter,
Rss,
Radio,
Tv,
Film,
// Work & Business
Rocket,
TrendingUp,
PieChart,
Presentation,
LineChart,
BarChart3,
CircleDollarSign,
Wallet,
// People & Social
Users,
UserCircle,
Contact,
MessagesSquare,
Share2,
// Nature & Environment
Leaf,
Sun,
Moon,
Mountain,
Trees,
Flower2,
// Objects & Tools
Wrench,
Hammer,
Paintbrush,
Pencil,
Scissors,
Ruler,
// Tech & Development
Terminal,
GitBranch,
Bug,
Cpu,
Server,
Wifi,
Smartphone,
Laptop,
// Documents & Files
FileCode,
FilePlus,
FolderOpen,
ClipboardList,
ScrollText,
StickyNote,
// Health & Wellness
HeartPulse,
Activity,
Dumbbell,
Apple,
// Navigation & Location
Navigation,
MapPin,
Route,
Signpost,
// Misc Popular
Crown,
Diamond,
Gem,
Shield,
Lock,
Eye,
ThumbsUp,
CheckCircle,
AlertCircle,
Info,
HelpCircle,
Play,
Pause,
SkipForward,
type LucideIcon
} from 'lucide-react';
// Static icon registry - no dynamic imports, works perfectly in Tauri WebView
export const ICON_REGISTRY: Record<string, LucideIcon> = {
// Core
Folder,
BookOpen,
Lightbulb,
Star,
Heart,
Briefcase,
Code,
FileText,
Globe,
Hash,
Inbox,
Layers,
Link,
MessageSquare,
Music,
Paperclip,
Search,
Settings,
Tag,
User,
Video,
Zap,
Archive,
Award,
Bell,
Bookmark,
Calendar,
Camera,
Clock,
Cloud,
Coffee,
Compass,
Database,
Flag,
Gift,
Home,
Image,
Key,
Mail,
Map,
Mic,
Package,
Smile,
Target,
// Thinking & Ideas
Brain,
Sparkles,
Wand2,
FlaskConical,
Atom,
// Content & Media
Newspaper,
Podcast,
Youtube,
Twitter,
Rss,
Radio,
Tv,
Film,
// Work & Business
Rocket,
TrendingUp,
PieChart,
Presentation,
LineChart,
BarChart3,
CircleDollarSign,
Wallet,
// People & Social
Users,
UserCircle,
Contact,
MessagesSquare,
Share2,
// Nature & Environment
Leaf,
Sun,
Moon,
Mountain,
Trees,
Flower2,
// Objects & Tools
Wrench,
Hammer,
Paintbrush,
Pencil,
Scissors,
Ruler,
// Tech & Development
Terminal,
GitBranch,
Bug,
Cpu,
Server,
Wifi,
Smartphone,
Laptop,
// Documents & Files
FileCode,
FilePlus,
FolderOpen,
ClipboardList,
ScrollText,
StickyNote,
// Health & Wellness
HeartPulse,
Activity,
Dumbbell,
Apple,
// Navigation & Location
Navigation,
MapPin,
Route,
Signpost,
// Misc Popular
Crown,
Diamond,
Gem,
Shield,
Lock,
Eye,
ThumbsUp,
CheckCircle,
AlertCircle,
Info,
HelpCircle,
Play,
Pause,
SkipForward,
};
// Helper to get icon component by name
export function getIconByName(name: string): LucideIcon {
return ICON_REGISTRY[name] || Folder;
}
// Render icon by name - useful for dynamic rendering
export function DynamicIcon({
name,
size = 16,
className,
style
}: {
name: string;
size?: number;
className?: string;
style?: React.CSSProperties;
}) {
const IconComponent = getIconByName(name);
return <IconComponent size={size} className={className} style={style} />;
}
interface LucideIconPickerProps {
selectedIcon: string;
onSelect: (iconName: string) => void;
onClose?: () => void;
}
export default function LucideIconPicker({ selectedIcon, onSelect, onClose }: LucideIconPickerProps) {
const [searchQuery, setSearchQuery] = useState('');
const iconNames = Object.keys(ICON_REGISTRY);
const filteredIcons = useMemo(() => {
if (!searchQuery.trim()) return iconNames;
const query = searchQuery.toLowerCase();
return iconNames.filter(name => name.toLowerCase().includes(query));
}, [searchQuery, iconNames]);
return (
<div style={{
display: 'flex',
flexDirection: 'column',
gap: '12px',
maxHeight: '300px'
}}>
{/* Search input */}
<div style={{ position: 'relative' }}>
<Search
size={14}
style={{
position: 'absolute',
left: '10px',
top: '50%',
transform: 'translateY(-50%)',
color: '#666'
}}
/>
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Search icons..."
style={{
width: '100%',
padding: '8px 12px 8px 32px',
background: '#111',
border: '1px solid #333',
borderRadius: '6px',
color: '#e5e5e5',
fontSize: '13px',
outline: 'none'
}}
onFocus={(e) => {
e.currentTarget.style.borderColor = '#22c55e';
}}
onBlur={(e) => {
e.currentTarget.style.borderColor = '#333';
}}
/>
</div>
{/* Icon grid */}
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(8, 1fr)',
gap: '4px',
overflowY: 'auto',
maxHeight: '240px',
padding: '4px'
}}>
{filteredIcons.map((iconName) => {
const IconComponent = ICON_REGISTRY[iconName];
const isSelected = selectedIcon === iconName;
return (
<button
key={iconName}
onClick={() => {
onSelect(iconName);
onClose?.();
}}
title={iconName}
style={{
width: '36px',
height: '36px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: isSelected ? 'rgba(34, 197, 94, 0.15)' : 'transparent',
border: isSelected ? '1px solid #22c55e' : '1px solid transparent',
borderRadius: '6px',
cursor: 'pointer',
color: isSelected ? '#22c55e' : '#888',
transition: 'all 0.15s ease'
}}
onMouseEnter={(e) => {
if (!isSelected) {
e.currentTarget.style.background = '#1a1a1a';
e.currentTarget.style.color = '#e5e5e5';
}
}}
onMouseLeave={(e) => {
if (!isSelected) {
e.currentTarget.style.background = 'transparent';
e.currentTarget.style.color = '#888';
}
}}
>
<IconComponent size={18} />
</button>
);
})}
</div>
{filteredIcons.length === 0 && (
<div style={{
padding: '20px',
textAlign: 'center',
color: '#666',
fontSize: '13px'
}}>
No icons match "{searchQuery}"
</div>
)}
</div>
);
}
File diff suppressed because it is too large Load Diff
+12 -1
View File
@@ -7,6 +7,8 @@ import AddNodeButton from './AddNodeButton';
import Chip from '../common/Chip'; import Chip from '../common/Chip';
import { getNodeIcon } from '@/utils/nodeIcons'; import { getNodeIcon } from '@/utils/nodeIcons';
import SearchModal from './SearchModal'; import SearchModal from './SearchModal';
import { DynamicIcon } from '../common/LucideIconPicker';
import { usePersistentState } from '@/hooks/usePersistentState';
interface NodesPanelProps { interface NodesPanelProps {
selectedNodes: Set<number>; selectedNodes: Set<number>;
@@ -47,6 +49,9 @@ export default function NodesPanel({ selectedNodes, onNodeSelect, onNodeCreated,
const [dragHoverDimension, setDragHoverDimension] = useState<string | null>(null); const [dragHoverDimension, setDragHoverDimension] = useState<string | null>(null);
const draggedNodeRef = useRef<{ id: number; dimensions?: string[] } | null>(null); const draggedNodeRef = useRef<{ id: number; dimensions?: string[] } | null>(null);
// Dimension icons (shared with FolderViewOverlay via localStorage)
const [dimensionIcons] = usePersistentState<Record<string, string>>('ui.dimensionIcons', {});
useEffect(() => { useEffect(() => {
fetchNodes(); fetchNodes();
fetchLockedDimensions(); fetchLockedDimensions();
@@ -774,7 +779,13 @@ export default function NodesPanel({ selectedNodes, onNodeSelect, onNodeCreated,
}} }}
> >
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', flex: 1 }}> <div style={{ display: 'flex', alignItems: 'center', gap: '10px', flex: 1 }}>
{isExpanded ? ( {dimensionIcons[lockedDim.dimension] ? (
<DynamicIcon
name={dimensionIcons[lockedDim.dimension]}
size={16}
style={{ color: isExpanded ? '#7de8a5' : '#64748b' }}
/>
) : isExpanded ? (
<FolderOpen size={16} color="#7de8a5" /> <FolderOpen size={16} color="#7de8a5" />
) : ( ) : (
<Folder size={16} color="#64748b" /> <Folder size={16} color="#64748b" />
+150 -75
View File
@@ -46,39 +46,59 @@ export class DimensionService {
} }
/** /**
* Automatically assign locked dimensions to a node based on its content * Automatically assign locked dimensions + suggest keyword dimensions
* Returns { locked: string[], keywords: string[] }
*/
static async assignDimensions(nodeData: {
title: string;
content?: string;
link?: string;
}): Promise<{ locked: string[]; keywords: string[] }> {
try {
const lockedDimensions = await this.getLockedDimensions();
if (lockedDimensions.length === 0) {
console.log('[DimensionAssignment] No locked dimensions available');
return { locked: [], keywords: [] };
}
const prompt = this.buildAssignmentPrompt(nodeData, lockedDimensions);
console.log(`[DimensionAssignment] Processing: "${nodeData.title}"`);
const response = await generateText({
model: openaiProvider('gpt-4o-mini'),
prompt,
maxOutputTokens: 150, // Increased for two-part response
temperature: 0.1,
});
console.log(`[DimensionAssignment] AI Response:\n${response.text}`);
const result = this.parseAssignmentResponse(response.text, lockedDimensions);
console.log(`[DimensionAssignment] Locked: ${result.locked.join(', ') || 'none'}`);
console.log(`[DimensionAssignment] Keywords: ${result.keywords.join(', ') || 'none'}`);
return result;
} catch (error) {
console.error('[DimensionAssignment] Error:', error);
return { locked: [], keywords: [] };
}
}
/**
* Legacy method for backwards compatibility
* @deprecated Use assignDimensions() instead
*/ */
static async assignLockedDimensions(nodeData: { static async assignLockedDimensions(nodeData: {
title: string; title: string;
content?: string; content?: string;
link?: string; link?: string;
}): Promise<string[]> { }): Promise<string[]> {
try { const result = await this.assignDimensions(nodeData);
const lockedDimensions = await this.getLockedDimensions(); return result.locked;
if (lockedDimensions.length === 0) {
console.log('No locked dimensions available for assignment');
return [];
}
const prompt = this.buildAssignmentPrompt(nodeData, lockedDimensions);
const response = await generateText({
model: openaiProvider('gpt-4o-mini'),
prompt,
maxOutputTokens: 100,
temperature: 0.1, // Low temperature for consistent results
});
const assignedDimensions = this.parseAssignmentResponse(response.text, lockedDimensions);
console.log(`Assigned dimensions for "${nodeData.title}": ${assignedDimensions.join(', ')}`);
return assignedDimensions;
} catch (error) {
console.error('Error assigning dimensions:', error);
return []; // Graceful fallback
}
} }
/** /**
@@ -122,80 +142,135 @@ export class DimensionService {
} }
/** /**
* Build AI prompt for dimension assignment * Build AI prompt for dimension assignment (locked + keyword dimensions)
*/ */
private static buildAssignmentPrompt( private static buildAssignmentPrompt(
nodeData: { title: string; content?: string; link?: string }, nodeData: { title: string; content?: string; link?: string },
lockedDimensions: LockedDimension[] lockedDimensions: LockedDimension[]
): string { ): string {
const contentPreview = nodeData.content?.slice(0, 500) || ''; const contentPreview = nodeData.content?.slice(0, 1000) || '';
const dimensionsList = lockedDimensions
.map(d => `- ${d.name}: ${d.description || `Infer purpose from name: ${d.name}`}`)
.join('\n');
return `Analyze this node content and assign appropriate dimensions from the available locked dimensions. // Only include dimensions that have descriptions
const dimensionsWithDescriptions = lockedDimensions.filter(d => d.description && d.description.trim().length > 0);
Node: const dimensionsList = dimensionsWithDescriptions
.map(d => `DIMENSION: "${d.name}"\nDESCRIPTION: ${d.description}`)
.join('\n---\n');
return `You are categorizing a knowledge node. You will:
1. Assign LOCKED dimensions (from a provided list)
2. Suggest KEYWORD dimensions (to aid searchability)
=== NODE TO CATEGORIZE ===
Title: ${nodeData.title} Title: ${nodeData.title}
Content: ${contentPreview}${nodeData.content && nodeData.content.length > 500 ? '...' : ''} Content: ${contentPreview}${nodeData.content && nodeData.content.length > 1000 ? '...' : ''}
URL: ${nodeData.link || 'none'} URL: ${nodeData.link || 'none'}
Available Locked Dimensions: === PART 1: LOCKED DIMENSIONS ===
CRITICAL: Read each dimension's DESCRIPTION carefully.
The description defines what belongs in that dimension.
Only assign if the content CLEARLY matches the description.
If unsure, skip it — better to miss than assign incorrectly.
Maximum 3 locked dimensions.
AVAILABLE LOCKED DIMENSIONS:
${dimensionsList} ${dimensionsList}
Return only the dimension names that best match this content, maximum 3 dimensions. === PART 2: KEYWORD DIMENSIONS ===
Respond with just the dimension names, one per line. Suggest 1-3 simple, lowercase keywords that:
If no dimensions are appropriate, respond with "none". - Capture the ESSENCE of this content
- Make it easily SEARCHABLE
- Are thoughtful and aid organization
Examples of good responses: Good keywords: ai, podcast, paper, productivity, machine-learning, book, video, interview, tutorial, framework
Work If unsure what keywords fit, respond with "none" — no noise is better than bad tags.
Learning
or === RESPONSE FORMAT ===
LOCKED:
[dimension names from the list above, one per line, or "none"]
Research KEYWORDS:
Ideas [lowercase keyword suggestions, one per line, or "none"]`;
Tech
or
none`;
} }
/** /**
* Parse AI response and validate dimension names * Parse AI response and extract locked + keyword dimensions
*/ */
private static parseAssignmentResponse( private static parseAssignmentResponse(
response: string, response: string,
availableDimensions: LockedDimension[] availableDimensions: LockedDimension[]
): string[] { ): { locked: string[]; keywords: string[] } {
const lines = response.trim().toLowerCase().split('\n'); const lockedDimensions: string[] = [];
const availableNames = availableDimensions.map(d => d.name.toLowerCase()); const keywordDimensions: string[] = [];
const validDimensions: string[] = [];
for (const line of lines) { // Split response into LOCKED and KEYWORDS sections
const dimensionName = line.trim(); const lockedMatch = response.match(/LOCKED:\s*([\s\S]*?)(?=KEYWORDS:|$)/i);
const keywordsMatch = response.match(/KEYWORDS:\s*([\s\S]*?)$/i);
if (dimensionName === 'none') {
break; // Parse LOCKED section
} if (lockedMatch) {
const lockedLines = lockedMatch[1].trim().split('\n');
// Find matching dimension (case-insensitive) for (const line of lockedLines) {
const matchedDimension = availableDimensions.find( const dimensionName = line.trim().toLowerCase();
d => d.name.toLowerCase() === dimensionName
); if (dimensionName === 'none' || dimensionName === '') {
continue;
if (matchedDimension && !validDimensions.includes(matchedDimension.name)) { }
validDimensions.push(matchedDimension.name);
// Find matching dimension (case-insensitive)
// Limit to 3 dimensions max const matchedDimension = availableDimensions.find(
if (validDimensions.length >= 3) { d => d.name.toLowerCase() === dimensionName
break; );
if (matchedDimension && !lockedDimensions.includes(matchedDimension.name)) {
lockedDimensions.push(matchedDimension.name);
// Limit to 3 locked dimensions
if (lockedDimensions.length >= 3) {
break;
}
} }
} }
} }
return validDimensions; // Parse KEYWORDS section
if (keywordsMatch) {
const keywordLines = keywordsMatch[1].trim().split('\n');
for (const line of keywordLines) {
// Clean and normalize keyword
const keyword = line.trim().toLowerCase()
.replace(/[^a-z0-9-]/g, '') // Only allow lowercase letters, numbers, hyphens
.slice(0, 30); // Max 30 chars per keyword
if (keyword === 'none' || keyword === '' || keyword.length < 2) {
continue;
}
if (!keywordDimensions.includes(keyword)) {
keywordDimensions.push(keyword);
// Limit to 3 keywords
if (keywordDimensions.length >= 3) {
break;
}
}
}
}
return { locked: lockedDimensions, keywords: keywordDimensions };
}
/**
* Create or get a keyword dimension (unlocked)
*/
static async ensureKeywordDimension(keyword: string): Promise<void> {
const sqlite = getSQLiteClient();
// INSERT OR IGNORE - if dimension exists, do nothing
sqlite.query(`
INSERT OR IGNORE INTO dimensions(name, description, is_priority, updated_at)
VALUES (?, ?, 0, CURRENT_TIMESTAMP)
`, [keyword, null]);
} }
} }
+94
View File
@@ -0,0 +1,94 @@
import { tool } from 'ai';
import { z } from 'zod';
import { DimensionService } from '@/services/database/dimensionService';
import { getSQLiteClient } from '@/services/database/sqlite-client';
export const getDimensionTool = tool({
description: 'Get detailed information about a specific dimension by name, including its description, priority status, and node count.',
inputSchema: z.object({
name: z.string().describe('The exact name of the dimension to retrieve')
}),
execute: async ({ name }) => {
console.log('📁 GetDimension tool called for:', name);
try {
const trimmedName = name.trim();
if (!trimmedName) {
return {
success: false,
error: 'Dimension name is required',
data: null
};
}
// Get dimension details from service
const dimension = await DimensionService.getDimensionByName(trimmedName);
// Get node count for this dimension
const sqlite = getSQLiteClient();
const countResult = sqlite.query(`
SELECT COUNT(*) AS count
FROM node_dimensions
WHERE dimension = ?
`, [trimmedName]);
const nodeCount = countResult.rows.length > 0
? Number((countResult.rows[0] as { count: number }).count)
: 0;
if (!dimension) {
// Dimension might exist in node_dimensions but not in dimensions table
if (nodeCount > 0) {
return {
success: true,
data: {
name: trimmedName,
description: null,
isPriority: false,
nodeCount,
exists: true,
hasMetadata: false
},
message: `Dimension "${trimmedName}" exists with ${nodeCount} nodes but has no metadata (description/priority not set).`
};
}
return {
success: false,
error: `Dimension "${trimmedName}" not found`,
data: null
};
}
const result = {
name: dimension.name,
description: dimension.description,
isPriority: dimension.is_priority,
nodeCount,
updatedAt: dimension.updated_at,
exists: true,
hasMetadata: true
};
// Build descriptive message
const parts: string[] = [];
parts.push(`Dimension: ${result.name}`);
if (result.isPriority) parts.push('Status: 🔒 Priority (locked)');
parts.push(`Nodes: ${result.nodeCount}`);
if (result.description) parts.push(`Description: ${result.description}`);
parts.push(`Last updated: ${result.updatedAt}`);
return {
success: true,
data: result,
message: parts.join('\n')
};
} catch (error) {
console.error('GetDimension tool error:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to get dimension',
data: null
};
}
}
});
+108
View File
@@ -0,0 +1,108 @@
import { tool } from 'ai';
import { z } from 'zod';
export const queryDimensionsTool = tool({
description: 'Query dimensions with optional filtering by name, priority status, or search term. Returns dimensions with their node counts.',
inputSchema: z.object({
filters: z.object({
search: z.string().describe('Search term to match against dimension names').optional(),
isPriority: z.boolean().describe('Filter by priority (locked) status').optional(),
limit: z.number().min(1).max(100).default(50).describe('Maximum number of results to return')
}).optional()
}),
execute: async ({ filters = {} }) => {
console.log('📁 QueryDimensions tool called with filters:', JSON.stringify(filters, null, 2));
try {
const limit = filters.limit || 50;
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000';
// Use existing API endpoint for dimension listing
const response = await fetch(`${baseUrl}/api/dimensions/popular`);
if (!response.ok) {
let errorMessage = 'Failed to query dimensions';
try {
const errorResult = await response.json();
errorMessage = errorResult.error || errorMessage;
} catch {
errorMessage = `Failed to query dimensions: ${response.status} ${response.statusText}`;
}
return {
success: false,
error: errorMessage,
data: { dimensions: [], count: 0 }
};
}
const result = await response.json();
if (!result.success || !result.data) {
return {
success: false,
error: 'Invalid response from dimensions API',
data: { dimensions: [], count: 0 }
};
}
// Apply filters
let dimensions = result.data as Array<{
dimension: string;
count: number;
isPriority: boolean;
description: string | null;
}>;
// Filter by search term
if (filters.search) {
const searchLower = filters.search.toLowerCase();
dimensions = dimensions.filter(d =>
d.dimension.toLowerCase().includes(searchLower)
);
}
// Filter by priority status
if (filters.isPriority !== undefined) {
dimensions = dimensions.filter(d => d.isPriority === filters.isPriority);
}
// Apply limit
const limitedDimensions = dimensions.slice(0, limit);
// Format for display
const formattedDimensions = limitedDimensions.map(d => ({
name: d.dimension,
count: d.count,
isPriority: d.isPriority,
description: d.description
}));
// Build message
const filterParts: string[] = [];
if (filters.search) filterParts.push(`matching "${filters.search}"`);
if (filters.isPriority !== undefined) filterParts.push(filters.isPriority ? 'priority only' : 'non-priority only');
const filterDesc = filterParts.length > 0 ? ` (${filterParts.join(', ')})` : '';
const dimensionList = formattedDimensions
.map(d => `${d.name}${d.isPriority ? ' 🔒' : ''} (${d.count} nodes)${d.description ? ` - ${d.description}` : ''}`)
.join('\n');
return {
success: true,
data: {
dimensions: formattedDimensions,
count: formattedDimensions.length,
total_available: dimensions.length,
filters_applied: filters
},
message: `Found ${formattedDimensions.length} dimensions${filterDesc}:\n${dimensionList || 'No dimensions found'}`
};
} catch (error) {
console.error('QueryDimensions tool error:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to query dimensions',
data: { dimensions: [], count: 0 }
};
}
}
});
+4
View File
@@ -11,6 +11,8 @@ import { updateDimensionTool } from '../database/updateDimension';
import { lockDimensionTool } from '../database/lockDimension'; import { lockDimensionTool } from '../database/lockDimension';
import { unlockDimensionTool } from '../database/unlockDimension'; import { unlockDimensionTool } from '../database/unlockDimension';
import { deleteDimensionTool } from '../database/deleteDimension'; import { deleteDimensionTool } from '../database/deleteDimension';
import { queryDimensionsTool } from '../database/queryDimensions';
import { getDimensionTool } from '../database/getDimension';
import { searchContentEmbeddingsTool } from '../other/searchContentEmbeddings'; import { searchContentEmbeddingsTool } from '../other/searchContentEmbeddings';
import { webSearchTool } from '../other/webSearch'; import { webSearchTool } from '../other/webSearch';
import { thinkTool } from '../other/think'; import { thinkTool } from '../other/think';
@@ -27,6 +29,8 @@ const CORE_TOOLS: Record<string, any> = {
queryNodes: queryNodesTool, queryNodes: queryNodesTool,
getNodesById: getNodesByIdTool, getNodesById: getNodesByIdTool,
queryEdge: queryEdgeTool, queryEdge: queryEdgeTool,
queryDimensions: queryDimensionsTool,
getDimension: getDimensionTool,
searchContentEmbeddings: searchContentEmbeddingsTool, searchContentEmbeddings: searchContentEmbeddingsTool,
}; };