cozo-memory 1.0.4 → 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +211 -3
- package/dist/api_bridge.js +6 -4
- package/dist/cli-commands.js +204 -0
- package/dist/cli.js +410 -0
- package/dist/download-model.js +3 -1
- package/dist/embedding-service.js +81 -12
- package/dist/hybrid-search.js +8 -3
- package/dist/index.js +62 -10
- package/dist/memory-service.js +88 -5
- package/dist/temporal-normalizer.js +2 -0
- package/dist/test-hybrid-debug.js +52 -0
- package/dist/test-mcp-search.js +47 -0
- package/dist/test-pdf-ingest.js +2 -0
- package/dist/test-qwen3-bilingual.js +2 -0
- package/dist/test-search-simple.js +27 -0
- package/dist/timestamp-utils.js +44 -0
- package/dist/tui-blessed.js +789 -0
- package/dist/tui-launcher.js +61 -0
- package/dist/tui.js +131 -0
- package/dist/tui.py +481 -0
- package/package.json +21 -2
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
/**
|
|
4
|
+
* Launcher for Python Textual TUI
|
|
5
|
+
* This spawns the Python TUI process
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
const child_process_1 = require("child_process");
|
|
9
|
+
const path_1 = require("path");
|
|
10
|
+
const fs_1 = require("fs");
|
|
11
|
+
function findPythonCommand() {
|
|
12
|
+
// Try different Python commands
|
|
13
|
+
const commands = ['python3', 'python', 'py'];
|
|
14
|
+
for (const cmd of commands) {
|
|
15
|
+
try {
|
|
16
|
+
const result = (0, child_process_1.spawn)(cmd, ['--version'], { stdio: 'pipe' });
|
|
17
|
+
if (result) {
|
|
18
|
+
return cmd;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
catch (e) {
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return 'python3'; // Default fallback
|
|
26
|
+
}
|
|
27
|
+
function main() {
|
|
28
|
+
const tuiPath = (0, path_1.join)(__dirname, 'tui.py');
|
|
29
|
+
if (!(0, fs_1.existsSync)(tuiPath)) {
|
|
30
|
+
console.error('Error: tui.py not found at', tuiPath);
|
|
31
|
+
console.error('Please ensure the Python TUI file exists.');
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
const pythonCmd = findPythonCommand();
|
|
35
|
+
console.log('Starting CozoDB Memory TUI...');
|
|
36
|
+
console.log('Note: Requires Python 3.7+ and textual package');
|
|
37
|
+
console.log('Install with: pip install textual\n');
|
|
38
|
+
const tuiProcess = (0, child_process_1.spawn)(pythonCmd, [tuiPath], {
|
|
39
|
+
stdio: 'inherit',
|
|
40
|
+
shell: false
|
|
41
|
+
});
|
|
42
|
+
tuiProcess.on('error', (error) => {
|
|
43
|
+
console.error('Failed to start TUI:', error.message);
|
|
44
|
+
console.error('\nMake sure Python and textual are installed:');
|
|
45
|
+
console.error(' pip install textual');
|
|
46
|
+
process.exit(1);
|
|
47
|
+
});
|
|
48
|
+
tuiProcess.on('exit', (code) => {
|
|
49
|
+
if (code !== 0 && code !== null) {
|
|
50
|
+
console.error(`\nTUI exited with code ${code}`);
|
|
51
|
+
console.error('If you see import errors, install textual:');
|
|
52
|
+
console.error(' pip install textual');
|
|
53
|
+
}
|
|
54
|
+
process.exit(code || 0);
|
|
55
|
+
});
|
|
56
|
+
// Handle Ctrl+C gracefully
|
|
57
|
+
process.on('SIGINT', () => {
|
|
58
|
+
tuiProcess.kill('SIGINT');
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
main();
|
package/dist/tui.js
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
/**
|
|
4
|
+
* Modern TUI for CozoDB Memory using Ink (React for CLI)
|
|
5
|
+
* Usage: cozo-memory-tui
|
|
6
|
+
*
|
|
7
|
+
* Note: This requires dynamic import due to Ink being ESM-only
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
async function main() {
|
|
11
|
+
try {
|
|
12
|
+
// Dynamic import for ESM modules
|
|
13
|
+
const { render, Box, Text, useInput, useApp } = await import('ink');
|
|
14
|
+
const React = await import('react');
|
|
15
|
+
const { CLICommands } = await import('./cli-commands.js');
|
|
16
|
+
const { useState, useEffect } = React;
|
|
17
|
+
const cli = new CLICommands();
|
|
18
|
+
let initialized = false;
|
|
19
|
+
const App = () => {
|
|
20
|
+
const { exit } = useApp();
|
|
21
|
+
const [state, setState] = useState({
|
|
22
|
+
screen: 'menu',
|
|
23
|
+
selectedIndex: 0,
|
|
24
|
+
result: null,
|
|
25
|
+
loading: false,
|
|
26
|
+
error: null
|
|
27
|
+
});
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
const initCLI = async () => {
|
|
30
|
+
if (!initialized) {
|
|
31
|
+
try {
|
|
32
|
+
await cli.init();
|
|
33
|
+
initialized = true;
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
setState(prev => ({ ...prev, error: error.message }));
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
initCLI();
|
|
41
|
+
return () => {
|
|
42
|
+
if (initialized) {
|
|
43
|
+
cli.close();
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
}, []);
|
|
47
|
+
useInput((input, key) => {
|
|
48
|
+
if (input === 'q' || (key.ctrl && input === 'c')) {
|
|
49
|
+
exit();
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (key.escape) {
|
|
53
|
+
setState(prev => ({ ...prev, screen: 'menu', selectedIndex: 0, result: null, error: null }));
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (state.screen === 'menu') {
|
|
57
|
+
if (key.upArrow) {
|
|
58
|
+
setState(prev => ({
|
|
59
|
+
...prev,
|
|
60
|
+
selectedIndex: Math.max(0, prev.selectedIndex - 1)
|
|
61
|
+
}));
|
|
62
|
+
}
|
|
63
|
+
else if (key.downArrow) {
|
|
64
|
+
setState(prev => ({
|
|
65
|
+
...prev,
|
|
66
|
+
selectedIndex: Math.min(5, prev.selectedIndex + 1)
|
|
67
|
+
}));
|
|
68
|
+
}
|
|
69
|
+
else if (key.return) {
|
|
70
|
+
const screens = ['entity', 'search', 'graph', 'system', 'result', 'menu'];
|
|
71
|
+
const newScreen = screens[state.selectedIndex];
|
|
72
|
+
if (state.selectedIndex === 4) {
|
|
73
|
+
setState(prev => ({ ...prev, loading: true }));
|
|
74
|
+
cli.health().then(result => {
|
|
75
|
+
setState(prev => ({ ...prev, screen: 'result', result, loading: false }));
|
|
76
|
+
}).catch(error => {
|
|
77
|
+
setState(prev => ({ ...prev, error: error.message, loading: false }));
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
setState(prev => ({ ...prev, screen: newScreen }));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
if (state.loading) {
|
|
87
|
+
return React.createElement(Box, { flexDirection: 'column', padding: 1 }, React.createElement(Text, { color: 'yellow' }, '⏳ Loading...'));
|
|
88
|
+
}
|
|
89
|
+
if (state.error) {
|
|
90
|
+
return React.createElement(Box, { flexDirection: 'column', padding: 1 }, React.createElement(Text, { color: 'red' }, `❌ Error: ${state.error}`), React.createElement(Text, { dimColor: true }, 'Press ESC to return to menu'));
|
|
91
|
+
}
|
|
92
|
+
return React.createElement(Box, { flexDirection: 'column', padding: 1 }, React.createElement(Header), state.screen === 'menu' && React.createElement(MainMenu, { selectedIndex: state.selectedIndex }), state.screen === 'entity' && React.createElement(EntityScreen), state.screen === 'search' && React.createElement(SearchScreen), state.screen === 'graph' && React.createElement(GraphScreen), state.screen === 'system' && React.createElement(SystemScreen), state.screen === 'result' && React.createElement(ResultScreen, { result: state.result }), React.createElement(Footer));
|
|
93
|
+
};
|
|
94
|
+
const Header = () => React.createElement(Box, {
|
|
95
|
+
flexDirection: 'column',
|
|
96
|
+
marginBottom: 1,
|
|
97
|
+
borderStyle: 'round',
|
|
98
|
+
borderColor: 'cyan',
|
|
99
|
+
padding: 1
|
|
100
|
+
}, React.createElement(Text, { bold: true, color: 'cyan' }, '🧠 CozoDB Memory - Interactive TUI'), React.createElement(Text, { dimColor: true }, 'Local-first persistent memory for AI agents'));
|
|
101
|
+
const Footer = () => React.createElement(Box, {
|
|
102
|
+
marginTop: 1,
|
|
103
|
+
borderStyle: 'single',
|
|
104
|
+
borderColor: 'gray',
|
|
105
|
+
padding: 1
|
|
106
|
+
}, React.createElement(Text, { dimColor: true }, '↑↓: Navigate | Enter: Select | ESC: Back | Q: Quit'));
|
|
107
|
+
const MainMenu = ({ selectedIndex }) => {
|
|
108
|
+
const menuItems = [
|
|
109
|
+
{ icon: '📦', label: 'Entity Operations', desc: 'Create, read, update, delete entities' },
|
|
110
|
+
{ icon: '🔍', label: 'Search & Context', desc: 'Hybrid search, context retrieval' },
|
|
111
|
+
{ icon: '🕸️', label: 'Graph Operations', desc: 'Explore, PageRank, communities' },
|
|
112
|
+
{ icon: '⚙️', label: 'System Management', desc: 'Health, metrics, export/import' },
|
|
113
|
+
{ icon: '💚', label: 'Quick Health Check', desc: 'Run system health check' },
|
|
114
|
+
{ icon: '❌', label: 'Exit', desc: 'Quit application' }
|
|
115
|
+
];
|
|
116
|
+
return React.createElement(Box, { flexDirection: 'column' }, React.createElement(Box, { marginBottom: 1 }, React.createElement(Text, { bold: true, color: 'yellow' }, 'Main Menu')), ...menuItems.map((item, index) => React.createElement(Box, { key: index, marginLeft: 2 }, React.createElement(Text, { color: selectedIndex === index ? 'green' : 'white' }, `${selectedIndex === index ? '▶ ' : ' '}${item.icon} ${item.label}`), selectedIndex === index && React.createElement(Text, { dimColor: true }, ` - ${item.desc}`))));
|
|
117
|
+
};
|
|
118
|
+
const EntityScreen = () => React.createElement(Box, { flexDirection: 'column' }, React.createElement(Text, { bold: true, color: 'cyan' }, '📦 Entity Operations'), React.createElement(Box, { marginTop: 1, flexDirection: 'column', marginLeft: 2 }, React.createElement(Text, null, '• Create Entity: ', React.createElement(Text, { dimColor: true }, 'cozo-memory entity create -n "Name" -t "Type"')), React.createElement(Text, null, '• Get Entity: ', React.createElement(Text, { dimColor: true }, 'cozo-memory entity get -i <id>')), React.createElement(Text, null, '• Delete Entity: ', React.createElement(Text, { dimColor: true }, 'cozo-memory entity delete -i <id>')), React.createElement(Text, null, '• Add Observation: ', React.createElement(Text, { dimColor: true }, 'cozo-memory obs add -i <id> -t "Text"'))), React.createElement(Box, { marginTop: 1 }, React.createElement(Text, { color: 'yellow' }, '💡 Use the CLI commands shown above for entity operations')));
|
|
119
|
+
const SearchScreen = () => React.createElement(Box, { flexDirection: 'column' }, React.createElement(Text, { bold: true, color: 'cyan' }, '🔍 Search & Context'), React.createElement(Box, { marginTop: 1, flexDirection: 'column', marginLeft: 2 }, React.createElement(Text, null, '• Search: ', React.createElement(Text, { dimColor: true }, 'cozo-memory search query -q "your query"')), React.createElement(Text, null, '• Context: ', React.createElement(Text, { dimColor: true }, 'cozo-memory search context -q "query" -w 5')), React.createElement(Text, null, '• Advanced: ', React.createElement(Text, { dimColor: true }, 'With filters, entity types, time ranges'))), React.createElement(Box, { marginTop: 1 }, React.createElement(Text, { color: 'yellow' }, '💡 Hybrid search combines vector, keyword, and graph signals')));
|
|
120
|
+
const GraphScreen = () => React.createElement(Box, { flexDirection: 'column' }, React.createElement(Text, { bold: true, color: 'cyan' }, '🕸️ Graph Operations'), React.createElement(Box, { marginTop: 1, flexDirection: 'column', marginLeft: 2 }, React.createElement(Text, null, '• Explore: ', React.createElement(Text, { dimColor: true }, 'cozo-memory graph explore -s <id> -h 3')), React.createElement(Text, null, '• PageRank: ', React.createElement(Text, { dimColor: true }, 'cozo-memory graph pagerank')), React.createElement(Text, null, '• Communities: ', React.createElement(Text, { dimColor: true }, 'cozo-memory graph communities')), React.createElement(Text, null, '• Path Finding: ', React.createElement(Text, { dimColor: true }, 'cozo-memory graph explore -s <id1> -e <id2>'))), React.createElement(Box, { marginTop: 1 }, React.createElement(Text, { color: 'yellow' }, '💡 Graph algorithms help discover implicit relationships')));
|
|
121
|
+
const SystemScreen = () => React.createElement(Box, { flexDirection: 'column' }, React.createElement(Text, { bold: true, color: 'cyan' }, '⚙️ System Management'), React.createElement(Box, { marginTop: 1, flexDirection: 'column', marginLeft: 2 }, React.createElement(Text, null, '• Health: ', React.createElement(Text, { dimColor: true }, 'cozo-memory system health')), React.createElement(Text, null, '• Metrics: ', React.createElement(Text, { dimColor: true }, 'cozo-memory system metrics')), React.createElement(Text, null, '• Export JSON: ', React.createElement(Text, { dimColor: true }, 'cozo-memory export json -o backup.json')), React.createElement(Text, null, '• Export Markdown: ', React.createElement(Text, { dimColor: true }, 'cozo-memory export markdown -o notes.md')), React.createElement(Text, null, '• Export Obsidian: ', React.createElement(Text, { dimColor: true }, 'cozo-memory export obsidian -o vault.zip')), React.createElement(Text, null, '• Import: ', React.createElement(Text, { dimColor: true }, 'cozo-memory import file -i data.json -f cozo'))), React.createElement(Box, { marginTop: 1 }, React.createElement(Text, { color: 'yellow' }, '💡 Regular exports recommended for backup')));
|
|
122
|
+
const ResultScreen = ({ result }) => React.createElement(Box, { flexDirection: 'column' }, React.createElement(Text, { bold: true, color: 'green' }, '✓ Result'), React.createElement(Box, { marginTop: 1, borderStyle: 'round', borderColor: 'green', padding: 1 }, React.createElement(Text, null, JSON.stringify(result, null, 2))));
|
|
123
|
+
render(React.createElement(App));
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
console.error('Failed to start TUI:', error.message);
|
|
127
|
+
console.error('Make sure all dependencies are installed: npm install');
|
|
128
|
+
process.exit(1);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
main();
|
package/dist/tui.py
ADDED
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Modern TUI for CozoDB Memory using Textual
|
|
4
|
+
Features: Mouse support, interactive menus, real-time updates
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from textual.app import App, ComposeResult
|
|
8
|
+
from textual.containers import Container, Horizontal, Vertical, ScrollableContainer
|
|
9
|
+
from textual.widgets import Header, Footer, Button, Static, Input, Tree, Label, DataTable, TabbedContent, TabPane
|
|
10
|
+
from textual.binding import Binding
|
|
11
|
+
from textual import events
|
|
12
|
+
import subprocess
|
|
13
|
+
import json
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
class CozoMemoryTUI(App):
|
|
18
|
+
"""Textual TUI for CozoDB Memory"""
|
|
19
|
+
|
|
20
|
+
CSS = """
|
|
21
|
+
Screen {
|
|
22
|
+
background: $surface;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
#sidebar {
|
|
26
|
+
width: 30;
|
|
27
|
+
background: $panel;
|
|
28
|
+
border-right: solid $primary;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
#main-content {
|
|
32
|
+
width: 1fr;
|
|
33
|
+
padding: 1 2;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
Button {
|
|
37
|
+
width: 100%;
|
|
38
|
+
margin: 1 0;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.success {
|
|
42
|
+
color: $success;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.error {
|
|
46
|
+
color: $error;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.info {
|
|
50
|
+
color: $accent;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
#result-container {
|
|
54
|
+
height: 1fr;
|
|
55
|
+
border: solid $primary;
|
|
56
|
+
padding: 1;
|
|
57
|
+
margin-top: 1;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
Input {
|
|
61
|
+
margin: 1 0;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
Label {
|
|
65
|
+
margin: 1 0;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
DataTable {
|
|
69
|
+
height: 1fr;
|
|
70
|
+
}
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
BINDINGS = [
|
|
74
|
+
Binding("q", "quit", "Quit"),
|
|
75
|
+
Binding("h", "show_help", "Help"),
|
|
76
|
+
Binding("r", "refresh", "Refresh"),
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
def __init__(self):
|
|
80
|
+
super().__init__()
|
|
81
|
+
self.cli_path = self._find_cli_path()
|
|
82
|
+
|
|
83
|
+
def _find_cli_path(self) -> str:
|
|
84
|
+
"""Find the cozo-memory CLI executable"""
|
|
85
|
+
# Try different locations
|
|
86
|
+
possible_paths = [
|
|
87
|
+
Path(__file__).parent.parent / "dist" / "cli.js",
|
|
88
|
+
Path.cwd() / "dist" / "cli.js",
|
|
89
|
+
]
|
|
90
|
+
|
|
91
|
+
for path in possible_paths:
|
|
92
|
+
if path.exists():
|
|
93
|
+
return str(path)
|
|
94
|
+
|
|
95
|
+
# Fallback to global installation
|
|
96
|
+
return "cozo-memory"
|
|
97
|
+
|
|
98
|
+
def compose(self) -> ComposeResult:
|
|
99
|
+
"""Create child widgets"""
|
|
100
|
+
yield Header(show_clock=True)
|
|
101
|
+
|
|
102
|
+
with Horizontal():
|
|
103
|
+
# Sidebar with navigation
|
|
104
|
+
with Vertical(id="sidebar"):
|
|
105
|
+
yield Static("🧠 CozoDB Memory", classes="info")
|
|
106
|
+
yield Button("📊 System Health", id="btn-health", variant="primary")
|
|
107
|
+
yield Button("➕ Create Entity", id="btn-create-entity")
|
|
108
|
+
yield Button("🔍 Search", id="btn-search")
|
|
109
|
+
yield Button("🕸️ Graph Operations", id="btn-graph")
|
|
110
|
+
yield Button("📤 Export", id="btn-export")
|
|
111
|
+
yield Button("📥 Import", id="btn-import")
|
|
112
|
+
yield Button("📋 List Entities", id="btn-list")
|
|
113
|
+
|
|
114
|
+
# Main content area
|
|
115
|
+
with ScrollableContainer(id="main-content"):
|
|
116
|
+
yield Static("Welcome to CozoDB Memory TUI", id="welcome-text", classes="info")
|
|
117
|
+
yield Static("Click a button or use keyboard shortcuts", id="help-text")
|
|
118
|
+
|
|
119
|
+
# Dynamic content container
|
|
120
|
+
with Container(id="result-container"):
|
|
121
|
+
yield Static("Results will appear here...", id="result-text")
|
|
122
|
+
|
|
123
|
+
yield Footer()
|
|
124
|
+
|
|
125
|
+
def _run_cli_command(self, *args, use_json_format: bool = True) -> dict:
|
|
126
|
+
"""Execute CLI command and return JSON result"""
|
|
127
|
+
try:
|
|
128
|
+
cmd = ["node", self.cli_path] + list(args)
|
|
129
|
+
# Only add -f json for commands that support it
|
|
130
|
+
if use_json_format:
|
|
131
|
+
cmd.extend(["-f", "json"])
|
|
132
|
+
|
|
133
|
+
result = subprocess.run(
|
|
134
|
+
cmd,
|
|
135
|
+
capture_output=True,
|
|
136
|
+
text=True,
|
|
137
|
+
timeout=30
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
if result.returncode != 0:
|
|
141
|
+
return {"error": result.stderr or "Command failed"}
|
|
142
|
+
|
|
143
|
+
# Parse JSON output - stdout should be pure JSON
|
|
144
|
+
if use_json_format:
|
|
145
|
+
try:
|
|
146
|
+
# Parse the JSON directly from stdout
|
|
147
|
+
parsed = json.loads(result.stdout)
|
|
148
|
+
return {"success": True, "data": parsed}
|
|
149
|
+
except json.JSONDecodeError as e:
|
|
150
|
+
# If JSON parsing fails, return error with details
|
|
151
|
+
return {"error": f"Failed to parse JSON: {str(e)}\nOutput: {result.stdout[:200]}"}
|
|
152
|
+
else:
|
|
153
|
+
# For non-JSON commands, return raw output
|
|
154
|
+
return {"success": True, "data": {"output": result.stdout}}
|
|
155
|
+
|
|
156
|
+
except subprocess.TimeoutExpired:
|
|
157
|
+
return {"error": "Command timed out"}
|
|
158
|
+
except Exception as e:
|
|
159
|
+
return {"error": str(e)}
|
|
160
|
+
|
|
161
|
+
def _update_result(self, data: dict):
|
|
162
|
+
"""Update the result container with new data"""
|
|
163
|
+
# Always recreate the Static widget to avoid markup issues
|
|
164
|
+
container = self.query_one("#result-container", Container)
|
|
165
|
+
container.remove_children()
|
|
166
|
+
|
|
167
|
+
if "error" in data:
|
|
168
|
+
# Show error without markup
|
|
169
|
+
content = f"ERROR:\n{data['error']}"
|
|
170
|
+
elif "success" in data and data["success"]:
|
|
171
|
+
# Format the data nicely
|
|
172
|
+
formatted = json.dumps(data["data"], indent=2)
|
|
173
|
+
# Truncate if too long
|
|
174
|
+
if len(formatted) > 5000:
|
|
175
|
+
formatted = formatted[:5000] + "\n... (truncated)"
|
|
176
|
+
content = f"SUCCESS:\n{formatted}"
|
|
177
|
+
else:
|
|
178
|
+
# Fallback
|
|
179
|
+
formatted = json.dumps(data, indent=2)
|
|
180
|
+
content = formatted
|
|
181
|
+
|
|
182
|
+
# Create new Static widget with markup disabled
|
|
183
|
+
result_text = Static(content, id="result-text", markup=False)
|
|
184
|
+
container.mount(result_text)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
async def action_show_health(self) -> None:
|
|
189
|
+
"""Show system health"""
|
|
190
|
+
self.query_one("#welcome-text", Static).update("📊 System Health")
|
|
191
|
+
result = self._run_cli_command("system", "health")
|
|
192
|
+
self._update_result(result)
|
|
193
|
+
|
|
194
|
+
async def action_create_entity(self) -> None:
|
|
195
|
+
"""Show create entity form"""
|
|
196
|
+
self.query_one("#welcome-text", Static).update("➕ Create Entity")
|
|
197
|
+
|
|
198
|
+
# Replace result container with form
|
|
199
|
+
container = self.query_one("#result-container", Container)
|
|
200
|
+
await container.remove_children()
|
|
201
|
+
|
|
202
|
+
await container.mount(
|
|
203
|
+
Label("Entity Name:"),
|
|
204
|
+
Input(placeholder="Enter entity name", id="input-name"),
|
|
205
|
+
Label("Entity Type:"),
|
|
206
|
+
Input(placeholder="Enter entity type", id="input-type"),
|
|
207
|
+
Label("Metadata (JSON, optional):"),
|
|
208
|
+
Input(placeholder='{"key": "value"}', id="input-metadata"),
|
|
209
|
+
Button("Create", id="btn-submit-entity", variant="success")
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
async def action_search(self) -> None:
|
|
213
|
+
"""Show search form"""
|
|
214
|
+
self.query_one("#welcome-text", Static).update("🔍 Search Memory")
|
|
215
|
+
|
|
216
|
+
container = self.query_one("#result-container", Container)
|
|
217
|
+
await container.remove_children()
|
|
218
|
+
|
|
219
|
+
await container.mount(
|
|
220
|
+
Label("Search Query:"),
|
|
221
|
+
Input(placeholder="Enter search query", id="input-search-query"),
|
|
222
|
+
Label("Limit (optional):"),
|
|
223
|
+
Input(placeholder="10", id="input-search-limit"),
|
|
224
|
+
Button("Search", id="btn-submit-search", variant="primary")
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
async def action_graph_menu(self) -> None:
|
|
228
|
+
"""Show graph operations menu"""
|
|
229
|
+
self.query_one("#welcome-text", Static).update("🕸️ Graph Operations")
|
|
230
|
+
|
|
231
|
+
container = self.query_one("#result-container", Container)
|
|
232
|
+
await container.remove_children()
|
|
233
|
+
|
|
234
|
+
await container.mount(
|
|
235
|
+
Static("Select a graph operation:", classes="info"),
|
|
236
|
+
Button("📊 PageRank", id="btn-graph-pagerank"),
|
|
237
|
+
Button("🏘️ Communities", id="btn-graph-communities"),
|
|
238
|
+
Button("🔍 Explore from Entity", id="btn-graph-explore")
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
async def action_export_menu(self) -> None:
|
|
242
|
+
"""Show export menu"""
|
|
243
|
+
self.query_one("#welcome-text", Static).update("📤 Export Memory")
|
|
244
|
+
|
|
245
|
+
container = self.query_one("#result-container", Container)
|
|
246
|
+
await container.remove_children()
|
|
247
|
+
|
|
248
|
+
await container.mount(
|
|
249
|
+
Static("Select export format:", classes="info"),
|
|
250
|
+
Label("Output File:"),
|
|
251
|
+
Input(placeholder="export.json", id="input-export-file"),
|
|
252
|
+
Button("Export as JSON", id="btn-export-json"),
|
|
253
|
+
Button("Export as Markdown", id="btn-export-md"),
|
|
254
|
+
Button("Export as Obsidian ZIP", id="btn-export-obsidian")
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
async def action_import_menu(self) -> None:
|
|
258
|
+
"""Show import menu"""
|
|
259
|
+
self.query_one("#welcome-text", Static).update("📥 Import Memory")
|
|
260
|
+
|
|
261
|
+
container = self.query_one("#result-container", Container)
|
|
262
|
+
await container.remove_children()
|
|
263
|
+
|
|
264
|
+
await container.mount(
|
|
265
|
+
Static("Import data from file:", classes="info"),
|
|
266
|
+
Label("Input File:"),
|
|
267
|
+
Input(placeholder="import.json", id="input-import-file"),
|
|
268
|
+
Label("Format:"),
|
|
269
|
+
Input(placeholder="cozo", id="input-import-format"),
|
|
270
|
+
Button("Import", id="btn-submit-import", variant="warning")
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
async def action_list_entities(self) -> None:
|
|
274
|
+
"""List all entities"""
|
|
275
|
+
self.query_one("#welcome-text", Static).update("📋 Entity List")
|
|
276
|
+
|
|
277
|
+
# Get health to show entity count
|
|
278
|
+
result = self._run_cli_command("system", "health")
|
|
279
|
+
|
|
280
|
+
container = self.query_one("#result-container", Container)
|
|
281
|
+
await container.remove_children()
|
|
282
|
+
|
|
283
|
+
if "success" in result and result["success"]:
|
|
284
|
+
health_data = result["data"]
|
|
285
|
+
await container.mount(
|
|
286
|
+
Static(f"Total Entities: {health_data.get('entities', 0)}"),
|
|
287
|
+
Static(f"Total Observations: {health_data.get('observations', 0)}"),
|
|
288
|
+
Static(f"Total Relationships: {health_data.get('relationships', 0)}"),
|
|
289
|
+
Static("\nUse CLI to get detailed entity list:\ncozo-memory entity get -i <entity-id>")
|
|
290
|
+
)
|
|
291
|
+
else:
|
|
292
|
+
await container.mount(Static(f"Error: {result.get('error', 'Unknown error')}"))
|
|
293
|
+
|
|
294
|
+
async def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
295
|
+
"""Handle all button presses"""
|
|
296
|
+
button_id = event.button.id
|
|
297
|
+
|
|
298
|
+
# Main menu buttons
|
|
299
|
+
if button_id == "btn-health":
|
|
300
|
+
await self.action_show_health()
|
|
301
|
+
elif button_id == "btn-create-entity":
|
|
302
|
+
await self.action_create_entity()
|
|
303
|
+
elif button_id == "btn-search":
|
|
304
|
+
await self.action_search()
|
|
305
|
+
elif button_id == "btn-graph":
|
|
306
|
+
await self.action_graph_menu()
|
|
307
|
+
elif button_id == "btn-export":
|
|
308
|
+
await self.action_export_menu()
|
|
309
|
+
elif button_id == "btn-import":
|
|
310
|
+
await self.action_import_menu()
|
|
311
|
+
elif button_id == "btn-list":
|
|
312
|
+
await self.action_list_entities()
|
|
313
|
+
|
|
314
|
+
# Form submission buttons
|
|
315
|
+
elif button_id == "btn-submit-entity":
|
|
316
|
+
await self.handle_create_entity()
|
|
317
|
+
elif button_id == "btn-submit-search":
|
|
318
|
+
await self.handle_search()
|
|
319
|
+
elif button_id == "btn-submit-import":
|
|
320
|
+
await self.handle_import()
|
|
321
|
+
|
|
322
|
+
# Graph operation buttons
|
|
323
|
+
elif button_id == "btn-graph-pagerank":
|
|
324
|
+
await self.handle_graph_pagerank()
|
|
325
|
+
elif button_id == "btn-graph-communities":
|
|
326
|
+
await self.handle_graph_communities()
|
|
327
|
+
elif button_id == "btn-graph-explore":
|
|
328
|
+
await self.handle_graph_explore_form()
|
|
329
|
+
|
|
330
|
+
# Export buttons
|
|
331
|
+
elif button_id == "btn-export-json":
|
|
332
|
+
await self.handle_export("json")
|
|
333
|
+
elif button_id == "btn-export-md":
|
|
334
|
+
await self.handle_export("markdown")
|
|
335
|
+
elif button_id == "btn-export-obsidian":
|
|
336
|
+
await self.handle_export("obsidian")
|
|
337
|
+
|
|
338
|
+
# Graph explore submit
|
|
339
|
+
elif button_id == "btn-submit-explore":
|
|
340
|
+
await self.handle_graph_explore()
|
|
341
|
+
|
|
342
|
+
async def handle_create_entity(self) -> None:
|
|
343
|
+
"""Handle entity creation form submission"""
|
|
344
|
+
name = self.query_one("#input-name", Input).value
|
|
345
|
+
entity_type = self.query_one("#input-type", Input).value
|
|
346
|
+
metadata = self.query_one("#input-metadata", Input).value
|
|
347
|
+
|
|
348
|
+
if not name or not entity_type:
|
|
349
|
+
self._update_result({"error": "Name and type are required"})
|
|
350
|
+
return
|
|
351
|
+
|
|
352
|
+
args = ["entity", "create", "-n", name, "-t", entity_type]
|
|
353
|
+
if metadata:
|
|
354
|
+
args.extend(["-m", metadata])
|
|
355
|
+
|
|
356
|
+
result = self._run_cli_command(*args)
|
|
357
|
+
self._update_result(result)
|
|
358
|
+
|
|
359
|
+
async def handle_search(self) -> None:
|
|
360
|
+
"""Handle search form submission"""
|
|
361
|
+
query = self.query_one("#input-search-query", Input).value
|
|
362
|
+
limit = self.query_one("#input-search-limit", Input).value
|
|
363
|
+
|
|
364
|
+
if not query:
|
|
365
|
+
self._update_result({"error": "Search query is required"})
|
|
366
|
+
return
|
|
367
|
+
|
|
368
|
+
args = ["search", "query", "-q", query]
|
|
369
|
+
if limit:
|
|
370
|
+
args.extend(["-l", limit])
|
|
371
|
+
|
|
372
|
+
result = self._run_cli_command(*args)
|
|
373
|
+
self._update_result(result)
|
|
374
|
+
|
|
375
|
+
async def handle_import(self) -> None:
|
|
376
|
+
"""Handle import form submission"""
|
|
377
|
+
file_path = self.query_one("#input-import-file", Input).value
|
|
378
|
+
format_type = self.query_one("#input-import-format", Input).value
|
|
379
|
+
|
|
380
|
+
if not file_path:
|
|
381
|
+
self._update_result({"error": "File path is required"})
|
|
382
|
+
return
|
|
383
|
+
|
|
384
|
+
args = ["import", "file", "-i", file_path, "-f", format_type or "cozo"]
|
|
385
|
+
# Import commands don't use -f json format
|
|
386
|
+
result = self._run_cli_command(*args, use_json_format=False)
|
|
387
|
+
self._update_result(result)
|
|
388
|
+
|
|
389
|
+
async def handle_graph_pagerank(self) -> None:
|
|
390
|
+
"""Execute PageRank"""
|
|
391
|
+
self.query_one("#welcome-text", Static).update("📊 Computing PageRank...")
|
|
392
|
+
result = self._run_cli_command("graph", "pagerank")
|
|
393
|
+
self._update_result(result)
|
|
394
|
+
|
|
395
|
+
async def handle_graph_communities(self) -> None:
|
|
396
|
+
"""Execute community detection"""
|
|
397
|
+
self.query_one("#welcome-text", Static).update("🏘️ Detecting Communities...")
|
|
398
|
+
result = self._run_cli_command("graph", "communities")
|
|
399
|
+
self._update_result(result)
|
|
400
|
+
|
|
401
|
+
async def handle_graph_explore_form(self) -> None:
|
|
402
|
+
"""Show graph explore form"""
|
|
403
|
+
container = self.query_one("#result-container", Container)
|
|
404
|
+
await container.remove_children()
|
|
405
|
+
|
|
406
|
+
await container.mount(
|
|
407
|
+
Label("Start Entity ID:"),
|
|
408
|
+
Input(placeholder="Enter entity ID", id="input-explore-start"),
|
|
409
|
+
Label("Max Hops:"),
|
|
410
|
+
Input(placeholder="3", id="input-explore-hops"),
|
|
411
|
+
Button("Explore", id="btn-submit-explore", variant="primary")
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
async def handle_graph_explore(self) -> None:
|
|
415
|
+
"""Execute graph exploration"""
|
|
416
|
+
start_id = self.query_one("#input-explore-start", Input).value
|
|
417
|
+
hops = self.query_one("#input-explore-hops", Input).value
|
|
418
|
+
|
|
419
|
+
if not start_id:
|
|
420
|
+
self._update_result({"error": "Start entity ID is required"})
|
|
421
|
+
return
|
|
422
|
+
|
|
423
|
+
args = ["graph", "explore", "-s", start_id]
|
|
424
|
+
if hops:
|
|
425
|
+
args.extend(["-h", hops])
|
|
426
|
+
|
|
427
|
+
result = self._run_cli_command(*args)
|
|
428
|
+
self._update_result(result)
|
|
429
|
+
|
|
430
|
+
async def handle_export(self, format_type: str) -> None:
|
|
431
|
+
"""Handle export"""
|
|
432
|
+
file_input = self.query_one("#input-export-file", Input)
|
|
433
|
+
file_path = file_input.value or f"export.{format_type}"
|
|
434
|
+
|
|
435
|
+
if format_type == "json":
|
|
436
|
+
args = ["export", "json", "-o", file_path, "--include-metadata", "--include-relationships", "--include-observations"]
|
|
437
|
+
elif format_type == "markdown":
|
|
438
|
+
args = ["export", "markdown", "-o", file_path]
|
|
439
|
+
else: # obsidian
|
|
440
|
+
args = ["export", "obsidian", "-o", file_path]
|
|
441
|
+
|
|
442
|
+
# Export commands don't use -f json format
|
|
443
|
+
result = self._run_cli_command(*args, use_json_format=False)
|
|
444
|
+
if "success" in result and result["success"]:
|
|
445
|
+
result = {"success": True, "data": {"message": f"Exported to {file_path}"}}
|
|
446
|
+
self._update_result(result)
|
|
447
|
+
|
|
448
|
+
def action_show_help(self) -> None:
|
|
449
|
+
"""Show help information"""
|
|
450
|
+
self.query_one("#welcome-text", Static).update("❓ Help")
|
|
451
|
+
help_text = """
|
|
452
|
+
[bold cyan]Keyboard Shortcuts:[/bold cyan]
|
|
453
|
+
• q - Quit application
|
|
454
|
+
• h - Show this help
|
|
455
|
+
• r - Refresh current view
|
|
456
|
+
|
|
457
|
+
[bold cyan]Mouse Support:[/bold cyan]
|
|
458
|
+
• Click buttons to navigate
|
|
459
|
+
• Scroll with mouse wheel
|
|
460
|
+
• Click input fields to type
|
|
461
|
+
|
|
462
|
+
[bold cyan]CLI Commands:[/bold cyan]
|
|
463
|
+
All operations can also be done via CLI:
|
|
464
|
+
• cozo-memory entity create -n "Name" -t "type"
|
|
465
|
+
• cozo-memory search query -q "search term"
|
|
466
|
+
• cozo-memory graph pagerank
|
|
467
|
+
• cozo-memory export json -o backup.json
|
|
468
|
+
"""
|
|
469
|
+
container = self.query_one("#result-container", Container)
|
|
470
|
+
container.remove_children()
|
|
471
|
+
container.mount(Static(help_text))
|
|
472
|
+
|
|
473
|
+
def action_refresh(self) -> None:
|
|
474
|
+
"""Refresh current view"""
|
|
475
|
+
self.query_one("#welcome-text", Static).update("🔄 Refreshed")
|
|
476
|
+
self.action_show_health()
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
if __name__ == "__main__":
|
|
480
|
+
app = CozoMemoryTUI()
|
|
481
|
+
app.run()
|