pomera-ai-commander 1.2.1 → 1.2.3
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/core/backup_recovery_manager.py +169 -3
- package/core/data_directory.py +549 -0
- package/core/diff_utils.py +239 -0
- package/core/efficient_line_numbers.py +30 -0
- package/core/mcp/find_replace_diff.py +334 -0
- package/core/mcp/tool_registry.py +369 -9
- package/core/memento.py +275 -0
- package/mcp.json +1 -1
- package/migrate_data.py +127 -0
- package/package.json +5 -2
- package/pomera.py +408 -10
- package/pomera_mcp_server.py +2 -2
- package/requirements.txt +1 -0
- package/scripts/Dockerfile.alpine +43 -0
- package/scripts/Dockerfile.gui-test +54 -0
- package/scripts/Dockerfile.linux +43 -0
- package/scripts/Dockerfile.test-linux +80 -0
- package/scripts/Dockerfile.ubuntu +39 -0
- package/scripts/README.md +53 -0
- package/scripts/build-all.bat +113 -0
- package/scripts/build-docker.bat +53 -0
- package/scripts/build-docker.sh +55 -0
- package/scripts/build-optimized.bat +101 -0
- package/scripts/build.sh +78 -0
- package/scripts/docker-compose.test.yml +27 -0
- package/scripts/docker-compose.yml +32 -0
- package/scripts/postinstall.js +62 -0
- package/scripts/requirements-minimal.txt +33 -0
- package/scripts/test-linux-simple.bat +28 -0
- package/scripts/validate-release-workflow.py +450 -0
- package/tools/diff_viewer.py +797 -52
- package/tools/find_replace.py +551 -12
- package/tools/notes_widget.py +48 -8
- package/tools/regex_extractor.py +5 -5
|
@@ -196,7 +196,14 @@ class ToolRegistry:
|
|
|
196
196
|
self._register_html_tool()
|
|
197
197
|
self._register_list_comparator_tool()
|
|
198
198
|
|
|
199
|
+
# Safe Update Tool (Phase 5) - for AI-initiated updates
|
|
200
|
+
self._register_safe_update_tool()
|
|
201
|
+
|
|
202
|
+
# Find & Replace Diff Tool (Phase 6) - regex find/replace with diff preview and Notes backup
|
|
203
|
+
self._register_find_replace_diff_tool()
|
|
204
|
+
|
|
199
205
|
self._logger.info(f"Registered {len(self._tools)} built-in MCP tools")
|
|
206
|
+
|
|
200
207
|
|
|
201
208
|
def _register_case_tool(self) -> None:
|
|
202
209
|
"""Register the Case Tool."""
|
|
@@ -1844,9 +1851,14 @@ class ToolRegistry:
|
|
|
1844
1851
|
|
|
1845
1852
|
def _get_notes_db_path(self) -> str:
|
|
1846
1853
|
"""Get the path to the notes database."""
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1854
|
+
try:
|
|
1855
|
+
from core.data_directory import get_database_path
|
|
1856
|
+
return get_database_path('notes.db')
|
|
1857
|
+
except ImportError:
|
|
1858
|
+
# Fallback to legacy behavior
|
|
1859
|
+
import os
|
|
1860
|
+
project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
1861
|
+
return os.path.join(project_root, 'notes.db')
|
|
1850
1862
|
|
|
1851
1863
|
def _get_notes_connection(self):
|
|
1852
1864
|
"""Get a connection to the notes database."""
|
|
@@ -1856,13 +1868,41 @@ class ToolRegistry:
|
|
|
1856
1868
|
conn.row_factory = sqlite3.Row
|
|
1857
1869
|
return conn
|
|
1858
1870
|
|
|
1871
|
+
def _sanitize_text(self, text: str) -> str:
|
|
1872
|
+
"""
|
|
1873
|
+
Sanitize text by removing invalid UTF-8 surrogate characters.
|
|
1874
|
+
|
|
1875
|
+
Lone surrogates (U+D800 to U+DFFF) are invalid in UTF-8 and cause
|
|
1876
|
+
encoding errors. This function removes them while preserving valid content.
|
|
1877
|
+
|
|
1878
|
+
Args:
|
|
1879
|
+
text: Input text that may contain invalid surrogates
|
|
1880
|
+
|
|
1881
|
+
Returns:
|
|
1882
|
+
Sanitized text safe for UTF-8 encoding and database storage
|
|
1883
|
+
"""
|
|
1884
|
+
if not text:
|
|
1885
|
+
return text
|
|
1886
|
+
|
|
1887
|
+
# Encode to UTF-8 with 'surrogatepass' to handle surrogates,
|
|
1888
|
+
# then decode with 'replace' to replace invalid sequences
|
|
1889
|
+
try:
|
|
1890
|
+
# This two-step process handles lone surrogates:
|
|
1891
|
+
# 1. surrogatepass allows encoding surrogates (normally forbidden in UTF-8)
|
|
1892
|
+
# 2. errors='replace' replaces invalid sequences with replacement char
|
|
1893
|
+
sanitized = text.encode('utf-8', errors='surrogatepass').decode('utf-8', errors='replace')
|
|
1894
|
+
return sanitized
|
|
1895
|
+
except Exception:
|
|
1896
|
+
# Fallback: manually filter out surrogate characters
|
|
1897
|
+
return ''.join(c for c in text if not (0xD800 <= ord(c) <= 0xDFFF))
|
|
1898
|
+
|
|
1859
1899
|
def _handle_notes_save(self, args: Dict[str, Any]) -> str:
|
|
1860
1900
|
"""Handle saving a new note."""
|
|
1861
1901
|
from datetime import datetime
|
|
1862
1902
|
|
|
1863
|
-
title = args.get("title", "")
|
|
1864
|
-
input_content = args.get("input_content", "")
|
|
1865
|
-
output_content = args.get("output_content", "")
|
|
1903
|
+
title = self._sanitize_text(args.get("title", ""))
|
|
1904
|
+
input_content = self._sanitize_text(args.get("input_content", ""))
|
|
1905
|
+
output_content = self._sanitize_text(args.get("output_content", ""))
|
|
1866
1906
|
|
|
1867
1907
|
if not title:
|
|
1868
1908
|
return "Error: Title is required"
|
|
@@ -2030,15 +2070,15 @@ class ToolRegistry:
|
|
|
2030
2070
|
|
|
2031
2071
|
if "title" in args:
|
|
2032
2072
|
updates.append("Title = ?")
|
|
2033
|
-
values.append(args["title"])
|
|
2073
|
+
values.append(self._sanitize_text(args["title"]))
|
|
2034
2074
|
|
|
2035
2075
|
if "input_content" in args:
|
|
2036
2076
|
updates.append("Input = ?")
|
|
2037
|
-
values.append(args["input_content"])
|
|
2077
|
+
values.append(self._sanitize_text(args["input_content"]))
|
|
2038
2078
|
|
|
2039
2079
|
if "output_content" in args:
|
|
2040
2080
|
updates.append("Output = ?")
|
|
2041
|
-
values.append(args["output_content"])
|
|
2081
|
+
values.append(self._sanitize_text(args["output_content"]))
|
|
2042
2082
|
|
|
2043
2083
|
if not updates:
|
|
2044
2084
|
conn.close()
|
|
@@ -2352,9 +2392,329 @@ class ToolRegistry:
|
|
|
2352
2392
|
result_lines.extend(in_both if in_both else ["(none)"])
|
|
2353
2393
|
|
|
2354
2394
|
return "\n".join(result_lines)
|
|
2395
|
+
|
|
2396
|
+
def _register_safe_update_tool(self) -> None:
|
|
2397
|
+
"""Register the Safe Update Tool for AI-initiated updates."""
|
|
2398
|
+
self.register(MCPToolAdapter(
|
|
2399
|
+
name="pomera_safe_update",
|
|
2400
|
+
description="Check update safety and get backup instructions before updating Pomera. "
|
|
2401
|
+
"IMPORTANT: In portable mode, npm/pip updates WILL DELETE user data. "
|
|
2402
|
+
"Always call this with action='check' before initiating any update.",
|
|
2403
|
+
input_schema={
|
|
2404
|
+
"type": "object",
|
|
2405
|
+
"properties": {
|
|
2406
|
+
"action": {
|
|
2407
|
+
"type": "string",
|
|
2408
|
+
"enum": ["check", "backup", "get_update_command"],
|
|
2409
|
+
"description": "check=analyze risks, backup=create backup to safe location, "
|
|
2410
|
+
"get_update_command=get recommended update command"
|
|
2411
|
+
},
|
|
2412
|
+
"backup_path": {
|
|
2413
|
+
"type": "string",
|
|
2414
|
+
"description": "For backup action: directory to save backup (default: user's Documents folder)"
|
|
2415
|
+
}
|
|
2416
|
+
},
|
|
2417
|
+
"required": ["action"]
|
|
2418
|
+
},
|
|
2419
|
+
handler=self._handle_safe_update
|
|
2420
|
+
))
|
|
2421
|
+
|
|
2422
|
+
def _handle_safe_update(self, args: Dict[str, Any]) -> str:
|
|
2423
|
+
"""Handle safe update tool execution."""
|
|
2424
|
+
import json
|
|
2425
|
+
import os
|
|
2426
|
+
import platform
|
|
2427
|
+
from pathlib import Path
|
|
2428
|
+
|
|
2429
|
+
action = args.get("action", "check")
|
|
2430
|
+
|
|
2431
|
+
# Determine installation mode
|
|
2432
|
+
try:
|
|
2433
|
+
from core.data_directory import is_portable_mode, get_user_data_dir, get_data_directory_info
|
|
2434
|
+
portable = is_portable_mode()
|
|
2435
|
+
data_info = get_data_directory_info()
|
|
2436
|
+
except ImportError:
|
|
2437
|
+
portable = False
|
|
2438
|
+
data_info = {"error": "data_directory module not available"}
|
|
2439
|
+
|
|
2440
|
+
# Get current version
|
|
2441
|
+
try:
|
|
2442
|
+
import importlib.metadata
|
|
2443
|
+
version = importlib.metadata.version("pomera-ai-commander")
|
|
2444
|
+
except Exception:
|
|
2445
|
+
version = "unknown"
|
|
2446
|
+
|
|
2447
|
+
# Get installation directory
|
|
2448
|
+
install_dir = Path(__file__).parent.parent.parent
|
|
2449
|
+
|
|
2450
|
+
# Find existing databases
|
|
2451
|
+
databases_found = []
|
|
2452
|
+
for db_name in ["settings.db", "notes.db", "settings.json"]:
|
|
2453
|
+
db_path = install_dir / db_name
|
|
2454
|
+
if db_path.exists():
|
|
2455
|
+
databases_found.append({
|
|
2456
|
+
"name": db_name,
|
|
2457
|
+
"path": str(db_path),
|
|
2458
|
+
"size_bytes": db_path.stat().st_size
|
|
2459
|
+
})
|
|
2460
|
+
|
|
2461
|
+
if action == "check":
|
|
2462
|
+
# Return risk assessment
|
|
2463
|
+
result = {
|
|
2464
|
+
"current_version": version,
|
|
2465
|
+
"installation_mode": "portable" if portable else "platform_dirs",
|
|
2466
|
+
"installation_dir": str(install_dir),
|
|
2467
|
+
"data_at_risk": portable and len(databases_found) > 0,
|
|
2468
|
+
"databases_in_install_dir": databases_found,
|
|
2469
|
+
"backup_required": portable and len(databases_found) > 0,
|
|
2470
|
+
"platform": platform.system(),
|
|
2471
|
+
}
|
|
2472
|
+
|
|
2473
|
+
if portable and databases_found:
|
|
2474
|
+
result["warning"] = (
|
|
2475
|
+
"CRITICAL: Portable mode detected with databases in installation directory. "
|
|
2476
|
+
"Running 'npm update' or 'pip install --upgrade' WILL DELETE these files! "
|
|
2477
|
+
"You MUST run pomera_safe_update with action='backup' before updating."
|
|
2478
|
+
)
|
|
2479
|
+
result["recommended_action"] = "backup"
|
|
2480
|
+
else:
|
|
2481
|
+
result["info"] = (
|
|
2482
|
+
"Safe to update. Data is stored in user data directory and will survive package updates."
|
|
2483
|
+
)
|
|
2484
|
+
result["recommended_action"] = "get_update_command"
|
|
2485
|
+
|
|
2486
|
+
return json.dumps(result, indent=2)
|
|
2487
|
+
|
|
2488
|
+
elif action == "backup":
|
|
2489
|
+
# Create backup to safe location
|
|
2490
|
+
backup_dir = args.get("backup_path")
|
|
2491
|
+
|
|
2492
|
+
if not backup_dir:
|
|
2493
|
+
# Default to user's Documents folder
|
|
2494
|
+
if platform.system() == "Windows":
|
|
2495
|
+
backup_dir = os.path.join(os.environ.get("USERPROFILE", ""), "Documents", "pomera-backup")
|
|
2496
|
+
else:
|
|
2497
|
+
backup_dir = os.path.join(os.path.expanduser("~"), "Documents", "pomera-backup")
|
|
2498
|
+
|
|
2499
|
+
backup_path = Path(backup_dir)
|
|
2500
|
+
backup_path.mkdir(parents=True, exist_ok=True)
|
|
2501
|
+
|
|
2502
|
+
import shutil
|
|
2503
|
+
from datetime import datetime
|
|
2504
|
+
|
|
2505
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
2506
|
+
backed_up = []
|
|
2507
|
+
|
|
2508
|
+
for db in databases_found:
|
|
2509
|
+
src = Path(db["path"])
|
|
2510
|
+
dst = backup_path / f"{src.stem}_{timestamp}{src.suffix}"
|
|
2511
|
+
try:
|
|
2512
|
+
shutil.copy2(src, dst)
|
|
2513
|
+
backed_up.append({"name": db["name"], "backup_path": str(dst)})
|
|
2514
|
+
except Exception as e:
|
|
2515
|
+
backed_up.append({"name": db["name"], "error": str(e)})
|
|
2516
|
+
|
|
2517
|
+
result = {
|
|
2518
|
+
"backup_location": str(backup_path),
|
|
2519
|
+
"files_backed_up": backed_up,
|
|
2520
|
+
"success": all("error" not in b for b in backed_up),
|
|
2521
|
+
"next_step": "You can now safely update Pomera. Use action='get_update_command' for the command."
|
|
2522
|
+
}
|
|
2523
|
+
|
|
2524
|
+
return json.dumps(result, indent=2)
|
|
2525
|
+
|
|
2526
|
+
elif action == "get_update_command":
|
|
2527
|
+
# Return appropriate update command
|
|
2528
|
+
result = {
|
|
2529
|
+
"npm_update": "npm update pomera-ai-commander",
|
|
2530
|
+
"pip_update": "pip install --upgrade pomera-ai-commander",
|
|
2531
|
+
"github_releases": "https://github.com/matbanik/Pomera-AI-Commander/releases",
|
|
2532
|
+
"note": "After updating, run the application to trigger automatic data migration."
|
|
2533
|
+
}
|
|
2534
|
+
|
|
2535
|
+
if portable and databases_found:
|
|
2536
|
+
result["warning"] = (
|
|
2537
|
+
"Data in installation directory detected! "
|
|
2538
|
+
"Ensure you have backed up (action='backup') before running update."
|
|
2539
|
+
)
|
|
2540
|
+
|
|
2541
|
+
return json.dumps(result, indent=2)
|
|
2542
|
+
|
|
2543
|
+
else:
|
|
2544
|
+
return json.dumps({"error": f"Unknown action: {action}"})
|
|
2545
|
+
|
|
2546
|
+
def _register_find_replace_diff_tool(self) -> None:
|
|
2547
|
+
"""Register the Find & Replace Diff Tool."""
|
|
2548
|
+
self.register(MCPToolAdapter(
|
|
2549
|
+
name="pomera_find_replace_diff",
|
|
2550
|
+
description="Regex find/replace with diff preview and automatic backup to Notes. "
|
|
2551
|
+
"Designed for AI agent workflows requiring verification and rollback capability. "
|
|
2552
|
+
"Operations: validate (check regex), preview (show diff), execute (replace+backup), recall (retrieve previous).",
|
|
2553
|
+
input_schema={
|
|
2554
|
+
"type": "object",
|
|
2555
|
+
"properties": {
|
|
2556
|
+
"operation": {
|
|
2557
|
+
"type": "string",
|
|
2558
|
+
"enum": ["validate", "preview", "execute", "recall"],
|
|
2559
|
+
"description": "validate=check regex syntax, preview=show compact diff, execute=replace+backup to Notes, recall=retrieve by note_id"
|
|
2560
|
+
},
|
|
2561
|
+
"text": {
|
|
2562
|
+
"type": "string",
|
|
2563
|
+
"description": "Input text to process (for validate/preview/execute)"
|
|
2564
|
+
},
|
|
2565
|
+
"find_pattern": {
|
|
2566
|
+
"type": "string",
|
|
2567
|
+
"description": "Regex pattern to find"
|
|
2568
|
+
},
|
|
2569
|
+
"replace_pattern": {
|
|
2570
|
+
"type": "string",
|
|
2571
|
+
"description": "Replacement string (supports backreferences \\1, \\2, etc.)",
|
|
2572
|
+
"default": ""
|
|
2573
|
+
},
|
|
2574
|
+
"flags": {
|
|
2575
|
+
"type": "array",
|
|
2576
|
+
"items": {"type": "string", "enum": ["i", "m", "s", "x"]},
|
|
2577
|
+
"default": [],
|
|
2578
|
+
"description": "Regex flags: i=ignore case, m=multiline, s=dotall, x=verbose"
|
|
2579
|
+
},
|
|
2580
|
+
"context_lines": {
|
|
2581
|
+
"type": "integer",
|
|
2582
|
+
"default": 2,
|
|
2583
|
+
"minimum": 0,
|
|
2584
|
+
"maximum": 10,
|
|
2585
|
+
"description": "Lines of context in diff output (for preview)"
|
|
2586
|
+
},
|
|
2587
|
+
"save_to_notes": {
|
|
2588
|
+
"type": "boolean",
|
|
2589
|
+
"default": True,
|
|
2590
|
+
"description": "Save operation to Notes for rollback (for execute)"
|
|
2591
|
+
},
|
|
2592
|
+
"note_id": {
|
|
2593
|
+
"type": "integer",
|
|
2594
|
+
"description": "Note ID to recall (for recall operation)"
|
|
2595
|
+
}
|
|
2596
|
+
},
|
|
2597
|
+
"required": ["operation"]
|
|
2598
|
+
},
|
|
2599
|
+
handler=self._handle_find_replace_diff
|
|
2600
|
+
))
|
|
2601
|
+
|
|
2602
|
+
def _handle_find_replace_diff(self, args: Dict[str, Any]) -> str:
|
|
2603
|
+
"""Handle find/replace diff tool execution."""
|
|
2604
|
+
import json
|
|
2605
|
+
from core.mcp.find_replace_diff import validate_regex, preview_replace, execute_replace, recall_operation
|
|
2606
|
+
|
|
2607
|
+
operation = args.get("operation", "")
|
|
2608
|
+
|
|
2609
|
+
if operation == "validate":
|
|
2610
|
+
pattern = args.get("find_pattern", "")
|
|
2611
|
+
flags = args.get("flags", [])
|
|
2612
|
+
result = validate_regex(pattern, flags)
|
|
2613
|
+
return json.dumps(result, ensure_ascii=False)
|
|
2614
|
+
|
|
2615
|
+
elif operation == "preview":
|
|
2616
|
+
text = args.get("text", "")
|
|
2617
|
+
find_pattern = args.get("find_pattern", "")
|
|
2618
|
+
replace_pattern = args.get("replace_pattern", "")
|
|
2619
|
+
flags = args.get("flags", [])
|
|
2620
|
+
context_lines = args.get("context_lines", 2)
|
|
2621
|
+
|
|
2622
|
+
if not text:
|
|
2623
|
+
return json.dumps({"success": False, "error": "text is required for preview"})
|
|
2624
|
+
if not find_pattern:
|
|
2625
|
+
return json.dumps({"success": False, "error": "find_pattern is required for preview"})
|
|
2626
|
+
|
|
2627
|
+
result = preview_replace(text, find_pattern, replace_pattern, flags, context_lines)
|
|
2628
|
+
return json.dumps(result, ensure_ascii=False)
|
|
2629
|
+
|
|
2630
|
+
elif operation == "execute":
|
|
2631
|
+
text = args.get("text", "")
|
|
2632
|
+
find_pattern = args.get("find_pattern", "")
|
|
2633
|
+
replace_pattern = args.get("replace_pattern", "")
|
|
2634
|
+
flags = args.get("flags", [])
|
|
2635
|
+
save_to_notes = args.get("save_to_notes", True)
|
|
2636
|
+
|
|
2637
|
+
if not text:
|
|
2638
|
+
return json.dumps({"success": False, "error": "text is required for execute"})
|
|
2639
|
+
if not find_pattern:
|
|
2640
|
+
return json.dumps({"success": False, "error": "find_pattern is required for execute"})
|
|
2641
|
+
|
|
2642
|
+
# Create notes handler if saving is requested
|
|
2643
|
+
notes_handler = None
|
|
2644
|
+
if save_to_notes:
|
|
2645
|
+
notes_handler = self._create_notes_handler()
|
|
2646
|
+
|
|
2647
|
+
result = execute_replace(text, find_pattern, replace_pattern, flags, save_to_notes, notes_handler)
|
|
2648
|
+
return json.dumps(result, ensure_ascii=False)
|
|
2649
|
+
|
|
2650
|
+
elif operation == "recall":
|
|
2651
|
+
note_id = args.get("note_id")
|
|
2652
|
+
if note_id is None:
|
|
2653
|
+
return json.dumps({"success": False, "error": "note_id is required for recall"})
|
|
2654
|
+
|
|
2655
|
+
notes_getter = self._create_notes_getter()
|
|
2656
|
+
result = recall_operation(note_id, notes_getter)
|
|
2657
|
+
return json.dumps(result, ensure_ascii=False)
|
|
2658
|
+
|
|
2659
|
+
else:
|
|
2660
|
+
return json.dumps({"success": False, "error": f"Unknown operation: {operation}"})
|
|
2661
|
+
|
|
2662
|
+
def _create_notes_handler(self):
|
|
2663
|
+
"""Create a handler function for saving to notes."""
|
|
2664
|
+
registry = self # Capture reference
|
|
2665
|
+
|
|
2666
|
+
def save_to_notes(title: str, input_content: str, output_content: str) -> int:
|
|
2667
|
+
"""Save operation to notes and return note_id."""
|
|
2668
|
+
try:
|
|
2669
|
+
from datetime import datetime
|
|
2670
|
+
# Sanitize text to prevent UTF-8 surrogate errors
|
|
2671
|
+
sanitized_title = registry._sanitize_text(title)
|
|
2672
|
+
sanitized_input = registry._sanitize_text(input_content)
|
|
2673
|
+
sanitized_output = registry._sanitize_text(output_content)
|
|
2674
|
+
|
|
2675
|
+
conn = registry._get_notes_connection()
|
|
2676
|
+
now = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
|
2677
|
+
cursor = conn.execute('''
|
|
2678
|
+
INSERT INTO notes (Created, Modified, Title, Input, Output)
|
|
2679
|
+
VALUES (?, ?, ?, ?, ?)
|
|
2680
|
+
''', (now, now, sanitized_title, sanitized_input, sanitized_output))
|
|
2681
|
+
note_id = cursor.lastrowid
|
|
2682
|
+
conn.commit()
|
|
2683
|
+
conn.close()
|
|
2684
|
+
return note_id
|
|
2685
|
+
except Exception as e:
|
|
2686
|
+
registry._logger.warning(f"Failed to save to notes: {e}")
|
|
2687
|
+
return -1
|
|
2688
|
+
return save_to_notes
|
|
2689
|
+
|
|
2690
|
+
def _create_notes_getter(self):
|
|
2691
|
+
"""Create a getter function for retrieving notes."""
|
|
2692
|
+
registry = self # Capture reference
|
|
2693
|
+
|
|
2694
|
+
def get_note(note_id: int) -> Dict[str, Any]:
|
|
2695
|
+
"""Get note by ID."""
|
|
2696
|
+
try:
|
|
2697
|
+
conn = registry._get_notes_connection()
|
|
2698
|
+
row = conn.execute('SELECT * FROM notes WHERE id = ?', (note_id,)).fetchone()
|
|
2699
|
+
conn.close()
|
|
2700
|
+
if row:
|
|
2701
|
+
return {
|
|
2702
|
+
'id': row[0],
|
|
2703
|
+
'created': row[1],
|
|
2704
|
+
'modified': row[2],
|
|
2705
|
+
'title': row[3],
|
|
2706
|
+
'input_content': row[4],
|
|
2707
|
+
'output_content': row[5]
|
|
2708
|
+
}
|
|
2709
|
+
return None
|
|
2710
|
+
except Exception as e:
|
|
2711
|
+
registry._logger.warning(f"Failed to get note: {e}")
|
|
2712
|
+
return None
|
|
2713
|
+
return get_note
|
|
2355
2714
|
|
|
2356
2715
|
|
|
2357
2716
|
# Singleton instance for convenience
|
|
2717
|
+
|
|
2358
2718
|
_default_registry: Optional[ToolRegistry] = None
|
|
2359
2719
|
|
|
2360
2720
|
|