cozo-memory 1.0.7 β†’ 1.0.8

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/dist/tui.py DELETED
@@ -1,481 +0,0 @@
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()