makeless 0.1.0__py3-none-any.whl
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.
- agent/planner.py +11 -0
- agent/runtime.py +0 -0
- agent/state.py +40 -0
- app/main.py +0 -0
- execution/executor.py +0 -0
- main.py +272 -0
- makeless-0.1.0.dist-info/METADATA +133 -0
- makeless-0.1.0.dist-info/RECORD +28 -0
- makeless-0.1.0.dist-info/WHEEL +5 -0
- makeless-0.1.0.dist-info/entry_points.txt +2 -0
- makeless-0.1.0.dist-info/licenses/LICENSE +1 -0
- makeless-0.1.0.dist-info/top_level.txt +8 -0
- memory/store.py +40 -0
- observability/logger.py +0 -0
- security/permissions.py +0 -0
- tools/app_finder.py +80 -0
- tools/app_launcher.py +15 -0
- tools/builtin.py +0 -0
- tools/filesystem.py +103 -0
- tools/indexer.py +133 -0
- tools/launcher.py +125 -0
- tools/locations.py +60 -0
- tools/open_router.py +93 -0
- tools/opener.py +10 -0
- tools/reader.py +48 -0
- tools/registry.py +0 -0
- tools/selector.py +56 -0
- tools/watcher.py +178 -0
tools/filesystem.py
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sqlite3
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
|
7
|
+
DATABASE_PATH = PROJECT_ROOT / "data" / "agentcore.db"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def search_files(query, max_results=20):
|
|
11
|
+
query = query.casefold()
|
|
12
|
+
|
|
13
|
+
connection = sqlite3.connect(DATABASE_PATH)
|
|
14
|
+
cursor = connection.cursor()
|
|
15
|
+
|
|
16
|
+
cursor.execute(
|
|
17
|
+
"""
|
|
18
|
+
SELECT path, name, item_type
|
|
19
|
+
FROM file_index
|
|
20
|
+
WHERE LOWER(name) LIKE ?
|
|
21
|
+
LIMIT ?
|
|
22
|
+
""",
|
|
23
|
+
(f"%{query}%", 200)
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
rows = cursor.fetchall()
|
|
27
|
+
connection.close()
|
|
28
|
+
|
|
29
|
+
results = []
|
|
30
|
+
|
|
31
|
+
ignored_paths = [
|
|
32
|
+
"\\$recycle.bin\\",
|
|
33
|
+
"\\windows\\recent\\",
|
|
34
|
+
"\\appdata\\local\\temp\\"
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
for path, name, item_type in rows:
|
|
38
|
+
normalized_path = path.casefold()
|
|
39
|
+
|
|
40
|
+
# Ignore locations that are almost never useful to a user.
|
|
41
|
+
if any(
|
|
42
|
+
ignored in normalized_path
|
|
43
|
+
for ignored in ignored_paths
|
|
44
|
+
):
|
|
45
|
+
continue
|
|
46
|
+
|
|
47
|
+
# Ignore files that no longer exist.
|
|
48
|
+
if not os.path.exists(path):
|
|
49
|
+
continue
|
|
50
|
+
|
|
51
|
+
results.append(path)
|
|
52
|
+
|
|
53
|
+
if len(results) >= max_results:
|
|
54
|
+
break
|
|
55
|
+
|
|
56
|
+
return results
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def find_best_match(query):
|
|
60
|
+
results = search_files(query, max_results=100)
|
|
61
|
+
|
|
62
|
+
if not results:
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
query = query.casefold()
|
|
66
|
+
project_root = str(PROJECT_ROOT).casefold()
|
|
67
|
+
|
|
68
|
+
def score_result(path):
|
|
69
|
+
normalized_path = path.casefold()
|
|
70
|
+
name = os.path.basename(path).casefold()
|
|
71
|
+
|
|
72
|
+
score = 0
|
|
73
|
+
|
|
74
|
+
# Exact filename match.
|
|
75
|
+
if name == query:
|
|
76
|
+
score += 1000
|
|
77
|
+
|
|
78
|
+
# Filename starts with the query.
|
|
79
|
+
elif name.startswith(query):
|
|
80
|
+
score += 500
|
|
81
|
+
|
|
82
|
+
# The AgentCore project gets a strong preference.
|
|
83
|
+
if normalized_path.startswith(project_root):
|
|
84
|
+
score += 2000
|
|
85
|
+
|
|
86
|
+
# Prefer normal user files.
|
|
87
|
+
user_home = str(Path.home()).casefold()
|
|
88
|
+
|
|
89
|
+
if normalized_path.startswith(user_home):
|
|
90
|
+
score += 200
|
|
91
|
+
|
|
92
|
+
# Penalize shortcuts.
|
|
93
|
+
if name.endswith(".lnk"):
|
|
94
|
+
score -= 500
|
|
95
|
+
|
|
96
|
+
return score
|
|
97
|
+
|
|
98
|
+
results.sort(
|
|
99
|
+
key=score_result,
|
|
100
|
+
reverse=True
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
return results[0]
|
tools/indexer.py
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sqlite3
|
|
3
|
+
import string
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
|
8
|
+
DATABASE_PATH = PROJECT_ROOT / "data" / "agentcore.db"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_available_drives():
|
|
12
|
+
drives = []
|
|
13
|
+
|
|
14
|
+
for letter in string.ascii_uppercase:
|
|
15
|
+
drive = f"{letter}:\\"
|
|
16
|
+
|
|
17
|
+
if os.path.exists(drive):
|
|
18
|
+
drives.append(drive)
|
|
19
|
+
|
|
20
|
+
return drives
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def create_database():
|
|
24
|
+
DATABASE_PATH.parent.mkdir(exist_ok=True)
|
|
25
|
+
|
|
26
|
+
connection = sqlite3.connect(DATABASE_PATH)
|
|
27
|
+
cursor = connection.cursor()
|
|
28
|
+
|
|
29
|
+
cursor.execute("""
|
|
30
|
+
CREATE TABLE IF NOT EXISTS file_index (
|
|
31
|
+
path TEXT PRIMARY KEY,
|
|
32
|
+
name TEXT NOT NULL,
|
|
33
|
+
item_type TEXT NOT NULL
|
|
34
|
+
)
|
|
35
|
+
""")
|
|
36
|
+
|
|
37
|
+
cursor.execute("""
|
|
38
|
+
CREATE INDEX IF NOT EXISTS idx_file_name
|
|
39
|
+
ON file_index(name)
|
|
40
|
+
""")
|
|
41
|
+
|
|
42
|
+
connection.commit()
|
|
43
|
+
connection.close()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def index_drive(drive):
|
|
47
|
+
connection = sqlite3.connect(DATABASE_PATH)
|
|
48
|
+
cursor = connection.cursor()
|
|
49
|
+
|
|
50
|
+
indexed_count = 0
|
|
51
|
+
|
|
52
|
+
print(f"Indexing {drive}...")
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
for root, directories, files in os.walk(
|
|
56
|
+
drive,
|
|
57
|
+
topdown=True,
|
|
58
|
+
followlinks=False
|
|
59
|
+
):
|
|
60
|
+
# Don't follow Windows links.
|
|
61
|
+
directories[:] = [
|
|
62
|
+
directory
|
|
63
|
+
for directory in directories
|
|
64
|
+
if not os.path.islink(
|
|
65
|
+
os.path.join(root, directory)
|
|
66
|
+
)
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
for directory in directories:
|
|
70
|
+
full_path = os.path.join(root, directory)
|
|
71
|
+
|
|
72
|
+
try:
|
|
73
|
+
cursor.execute(
|
|
74
|
+
"""
|
|
75
|
+
INSERT OR IGNORE INTO file_index
|
|
76
|
+
(path, name, item_type)
|
|
77
|
+
VALUES (?, ?, ?)
|
|
78
|
+
""",
|
|
79
|
+
(full_path, directory, "folder")
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
indexed_count += 1
|
|
83
|
+
|
|
84
|
+
except sqlite3.Error:
|
|
85
|
+
continue
|
|
86
|
+
|
|
87
|
+
for file in files:
|
|
88
|
+
full_path = os.path.join(root, file)
|
|
89
|
+
|
|
90
|
+
try:
|
|
91
|
+
cursor.execute(
|
|
92
|
+
"""
|
|
93
|
+
INSERT OR IGNORE INTO file_index
|
|
94
|
+
(path, name, item_type)
|
|
95
|
+
VALUES (?, ?, ?)
|
|
96
|
+
""",
|
|
97
|
+
(full_path, file, "file")
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
indexed_count += 1
|
|
101
|
+
|
|
102
|
+
except sqlite3.Error:
|
|
103
|
+
continue
|
|
104
|
+
|
|
105
|
+
# Save progress periodically.
|
|
106
|
+
if indexed_count % 1000 == 0 and indexed_count > 0:
|
|
107
|
+
connection.commit()
|
|
108
|
+
|
|
109
|
+
except PermissionError:
|
|
110
|
+
pass
|
|
111
|
+
|
|
112
|
+
connection.commit()
|
|
113
|
+
connection.close()
|
|
114
|
+
|
|
115
|
+
print(f"Finished {drive}: {indexed_count} items indexed.")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def build_index():
|
|
119
|
+
print("\nAgentCore File Indexer")
|
|
120
|
+
print("----------------------")
|
|
121
|
+
|
|
122
|
+
create_database()
|
|
123
|
+
|
|
124
|
+
drives = get_available_drives()
|
|
125
|
+
|
|
126
|
+
for drive in drives:
|
|
127
|
+
index_drive(drive)
|
|
128
|
+
|
|
129
|
+
print("\nIndexing complete.")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
if __name__ == "__main__":
|
|
133
|
+
build_index()
|
tools/launcher.py
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import subprocess
|
|
3
|
+
import webbrowser
|
|
4
|
+
from urllib.parse import quote_plus
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def open_with_default_app(path):
|
|
8
|
+
"""
|
|
9
|
+
Open a file or folder using Windows' default application.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
os.startfile(path)
|
|
14
|
+
return True, f"Opened {path}"
|
|
15
|
+
|
|
16
|
+
except OSError as error:
|
|
17
|
+
return False, str(error)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def open_in_vscode(path):
|
|
21
|
+
"""
|
|
22
|
+
Open a file or folder in Visual Studio Code.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
subprocess.Popen(
|
|
27
|
+
["code.cmd", path],
|
|
28
|
+
shell=True
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
return True, f"Opened {path} in VS Code"
|
|
32
|
+
|
|
33
|
+
except FileNotFoundError:
|
|
34
|
+
return (
|
|
35
|
+
False,
|
|
36
|
+
"VS Code command was not found. "
|
|
37
|
+
"Make sure VS Code is installed and available."
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
except Exception as error:
|
|
41
|
+
return False, str(error)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def show_in_file_explorer(path):
|
|
45
|
+
"""
|
|
46
|
+
Open a folder in File Explorer or select a file.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
if os.path.isdir(path):
|
|
51
|
+
subprocess.Popen(
|
|
52
|
+
["explorer.exe", path]
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
return True, f"Opened folder in File Explorer: {path}"
|
|
56
|
+
|
|
57
|
+
subprocess.Popen(
|
|
58
|
+
["explorer.exe", "/select,", path]
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
return True, f"Selected file in File Explorer: {path}"
|
|
62
|
+
|
|
63
|
+
except Exception as error:
|
|
64
|
+
return False, str(error)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def open_in_browser(path_or_url, browser=None):
|
|
68
|
+
"""
|
|
69
|
+
Open a URL in the default browser or a specific browser.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
try:
|
|
73
|
+
if browser is None:
|
|
74
|
+
webbrowser.open(path_or_url)
|
|
75
|
+
return True, f"Opened {path_or_url} in your browser"
|
|
76
|
+
|
|
77
|
+
browser = browser.casefold().strip()
|
|
78
|
+
|
|
79
|
+
browser_paths = {
|
|
80
|
+
"brave": (
|
|
81
|
+
r"C:\Program Files\BraveSoftware"
|
|
82
|
+
r"\Brave-Browser\Application\brave.exe"
|
|
83
|
+
),
|
|
84
|
+
"chrome": (
|
|
85
|
+
r"C:\Program Files\Google"
|
|
86
|
+
r"\Chrome\Application\chrome.exe"
|
|
87
|
+
),
|
|
88
|
+
"edge": (
|
|
89
|
+
r"C:\Program Files (x86)\Microsoft"
|
|
90
|
+
r"\Edge\Application\msedge.exe"
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
executable = browser_paths.get(browser)
|
|
95
|
+
|
|
96
|
+
if executable and os.path.exists(executable):
|
|
97
|
+
subprocess.Popen(
|
|
98
|
+
[executable, path_or_url]
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
True,
|
|
103
|
+
f"Opened {path_or_url} in {browser.title()}"
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
return False, f"Could not find {browser.title()}"
|
|
107
|
+
|
|
108
|
+
except Exception as error:
|
|
109
|
+
return False, str(error)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def search_in_browser(query, browser):
|
|
113
|
+
"""
|
|
114
|
+
Search Google using a specific browser.
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
search_url = (
|
|
118
|
+
"https://www.google.com/search?q="
|
|
119
|
+
+ quote_plus(query)
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
return open_in_browser(
|
|
123
|
+
search_url,
|
|
124
|
+
browser
|
|
125
|
+
)
|
tools/locations.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def get_system_location(query):
|
|
6
|
+
"""
|
|
7
|
+
Understand common human names for drives and folders.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
normalized = query.casefold().strip()
|
|
11
|
+
|
|
12
|
+
# Remove natural filler words.
|
|
13
|
+
words_to_remove = [
|
|
14
|
+
"my",
|
|
15
|
+
"the"
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
words = normalized.split()
|
|
19
|
+
|
|
20
|
+
filtered_words = []
|
|
21
|
+
|
|
22
|
+
for word in words:
|
|
23
|
+
if word not in words_to_remove:
|
|
24
|
+
filtered_words.append(word)
|
|
25
|
+
|
|
26
|
+
normalized = " ".join(filtered_words)
|
|
27
|
+
|
|
28
|
+
# Handle drive names.
|
|
29
|
+
if normalized in ["c drive", "c"]:
|
|
30
|
+
return "C:\\"
|
|
31
|
+
|
|
32
|
+
if normalized in ["e drive", "e"]:
|
|
33
|
+
return "E:\\"
|
|
34
|
+
|
|
35
|
+
# Get the user's home directory.
|
|
36
|
+
home = Path.home()
|
|
37
|
+
|
|
38
|
+
locations = {
|
|
39
|
+
"downloads": home / "Downloads",
|
|
40
|
+
"download": home / "Downloads"
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
# Check Windows-configured special folders.
|
|
44
|
+
try:
|
|
45
|
+
from tools.watcher import get_windows_folder
|
|
46
|
+
|
|
47
|
+
desktop = get_windows_folder("Desktop")
|
|
48
|
+
documents = get_windows_folder("Personal")
|
|
49
|
+
|
|
50
|
+
if desktop:
|
|
51
|
+
locations["desktop"] = desktop
|
|
52
|
+
|
|
53
|
+
if documents:
|
|
54
|
+
locations["documents"] = documents
|
|
55
|
+
locations["document"] = documents
|
|
56
|
+
|
|
57
|
+
except Exception:
|
|
58
|
+
pass
|
|
59
|
+
|
|
60
|
+
return locations.get(normalized)
|
tools/open_router.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from tools.launcher import (
|
|
5
|
+
open_in_vscode,
|
|
6
|
+
open_with_default_app,
|
|
7
|
+
show_in_file_explorer
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
IMAGE_EXTENSIONS = {
|
|
12
|
+
".jpg",
|
|
13
|
+
".jpeg",
|
|
14
|
+
".png",
|
|
15
|
+
".gif",
|
|
16
|
+
".webp",
|
|
17
|
+
".bmp"
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
AUDIO_EXTENSIONS = {
|
|
21
|
+
".mp3",
|
|
22
|
+
".wav",
|
|
23
|
+
".m4a",
|
|
24
|
+
".flac",
|
|
25
|
+
".aac",
|
|
26
|
+
".ogg"
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
VIDEO_EXTENSIONS = {
|
|
30
|
+
".mp4",
|
|
31
|
+
".mkv",
|
|
32
|
+
".avi",
|
|
33
|
+
".mov",
|
|
34
|
+
".wmv",
|
|
35
|
+
".webm"
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def get_item_type(path):
|
|
40
|
+
"""
|
|
41
|
+
Determine whether the path is a folder, media file,
|
|
42
|
+
or a normal file.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
item_path = Path(path)
|
|
46
|
+
|
|
47
|
+
if item_path.is_dir():
|
|
48
|
+
return "folder"
|
|
49
|
+
|
|
50
|
+
extension = item_path.suffix.casefold()
|
|
51
|
+
|
|
52
|
+
if extension in IMAGE_EXTENSIONS:
|
|
53
|
+
return "image"
|
|
54
|
+
|
|
55
|
+
if extension in AUDIO_EXTENSIONS:
|
|
56
|
+
return "audio"
|
|
57
|
+
|
|
58
|
+
if extension in VIDEO_EXTENSIONS:
|
|
59
|
+
return "video"
|
|
60
|
+
|
|
61
|
+
return "file"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def open_item(path, app=None):
|
|
65
|
+
"""
|
|
66
|
+
Open an item based on what it is and what the user requested.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
if not os.path.exists(path):
|
|
70
|
+
return False, "That item no longer exists."
|
|
71
|
+
|
|
72
|
+
# If the user explicitly requested VS Code,
|
|
73
|
+
# open the file or folder there.
|
|
74
|
+
if app:
|
|
75
|
+
normalized_app = app.casefold().strip()
|
|
76
|
+
|
|
77
|
+
if normalized_app in ["vs code", "vscode", "visual studio code"]:
|
|
78
|
+
return open_in_vscode(path)
|
|
79
|
+
|
|
80
|
+
return False, f"I don't know how to open items in {app} yet."
|
|
81
|
+
|
|
82
|
+
item_type = get_item_type(path)
|
|
83
|
+
|
|
84
|
+
# Folders open in File Explorer.
|
|
85
|
+
if item_type == "folder":
|
|
86
|
+
return show_in_file_explorer(path)
|
|
87
|
+
|
|
88
|
+
# Media files open in their default Windows application.
|
|
89
|
+
if item_type in ["image", "audio", "video"]:
|
|
90
|
+
return open_with_default_app(path)
|
|
91
|
+
|
|
92
|
+
# Normal files are shown in File Explorer.
|
|
93
|
+
return show_in_file_explorer(path)
|
tools/opener.py
ADDED
tools/reader.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def read_file(path, max_size=1_000_000):
|
|
5
|
+
"""
|
|
6
|
+
Read a text file safely.
|
|
7
|
+
|
|
8
|
+
Returns:
|
|
9
|
+
success, content_or_error
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
file_path = Path(path)
|
|
13
|
+
|
|
14
|
+
if not file_path.exists():
|
|
15
|
+
return False, "The file no longer exists."
|
|
16
|
+
|
|
17
|
+
if not file_path.is_file():
|
|
18
|
+
return False, "That is a folder. I can only read files."
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
file_size = file_path.stat().st_size
|
|
22
|
+
|
|
23
|
+
if file_size > max_size:
|
|
24
|
+
return (
|
|
25
|
+
False,
|
|
26
|
+
"That file is too large to print safely."
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
content = file_path.read_text(
|
|
30
|
+
encoding="utf-8"
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
return True, content
|
|
34
|
+
|
|
35
|
+
except UnicodeDecodeError:
|
|
36
|
+
return (
|
|
37
|
+
False,
|
|
38
|
+
"This doesn't appear to be a readable text file."
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
except PermissionError:
|
|
42
|
+
return (
|
|
43
|
+
False,
|
|
44
|
+
"I don't have permission to read that file."
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
except OSError as error:
|
|
48
|
+
return False, str(error)
|
tools/registry.py
ADDED
|
File without changes
|
tools/selector.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import msvcrt
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def select_result(results):
|
|
5
|
+
"""
|
|
6
|
+
Show an interactive terminal menu.
|
|
7
|
+
|
|
8
|
+
Use Up/Down arrow keys and Enter to select.
|
|
9
|
+
Returns the selected result.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
selected_index = 0
|
|
13
|
+
|
|
14
|
+
while True:
|
|
15
|
+
# Move cursor to the top of the menu when redrawing.
|
|
16
|
+
print("\033[H\033[J", end="")
|
|
17
|
+
|
|
18
|
+
print("AgentCore: Multiple matches found.")
|
|
19
|
+
print("Which one do you mean?\n")
|
|
20
|
+
|
|
21
|
+
for index, result in enumerate(results):
|
|
22
|
+
if index == selected_index:
|
|
23
|
+
print(f"> {result}")
|
|
24
|
+
else:
|
|
25
|
+
print(f" {result}")
|
|
26
|
+
|
|
27
|
+
print("\nUse ↑ ↓ arrows and Enter to select.")
|
|
28
|
+
print("Press Esc to cancel.")
|
|
29
|
+
|
|
30
|
+
key = msvcrt.getch()
|
|
31
|
+
|
|
32
|
+
# Arrow keys on Windows send two characters.
|
|
33
|
+
if key in (b"\x00", b"\xe0"):
|
|
34
|
+
key = msvcrt.getch()
|
|
35
|
+
|
|
36
|
+
# Up arrow.
|
|
37
|
+
if key == b"H":
|
|
38
|
+
selected_index -= 1
|
|
39
|
+
|
|
40
|
+
if selected_index < 0:
|
|
41
|
+
selected_index = len(results) - 1
|
|
42
|
+
|
|
43
|
+
# Down arrow.
|
|
44
|
+
elif key == b"P":
|
|
45
|
+
selected_index += 1
|
|
46
|
+
|
|
47
|
+
if selected_index >= len(results):
|
|
48
|
+
selected_index = 0
|
|
49
|
+
|
|
50
|
+
# Enter key.
|
|
51
|
+
elif key == b"\r":
|
|
52
|
+
return results[selected_index]
|
|
53
|
+
|
|
54
|
+
# Escape key.
|
|
55
|
+
elif key == b"\x1b":
|
|
56
|
+
return None
|