machineconfig 2.8__py3-none-any.whl → 2.9__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.
Potentially problematic release.
This version of machineconfig might be problematic. Click here for more details.
- machineconfig/jobs/python/python_ve_symlink.py +1 -1
- machineconfig/scripts/python/__pycache__/devops.cpython-313.pyc +0 -0
- machineconfig/scripts/python/__pycache__/devops_update_repos.cpython-313.pyc +0 -0
- machineconfig/scripts/python/__pycache__/fire_agents.cpython-313.pyc +0 -0
- machineconfig/scripts/python/devops.py +18 -64
- machineconfig/scripts/python/devops_add_identity.py +6 -2
- machineconfig/scripts/python/devops_add_ssh_key.py +5 -2
- machineconfig/scripts/python/devops_backup_retrieve.py +3 -15
- machineconfig/scripts/python/devops_devapps_install.py +8 -6
- machineconfig/scripts/python/devops_update_repos.py +125 -226
- machineconfig/scripts/python/fire_agents.py +108 -151
- machineconfig/scripts/python/fire_agents_help_launch.py +97 -0
- machineconfig/scripts/python/fire_agents_help_search.py +83 -0
- machineconfig/scripts/python/helpers/cloud_helpers.py +2 -5
- machineconfig/scripts/python/repos.py +1 -1
- machineconfig/scripts/python/repos_helper_update.py +288 -0
- machineconfig/utils/installer_utils/installer_class.py +3 -3
- machineconfig/utils/notifications.py +24 -4
- machineconfig/utils/path.py +2 -1
- machineconfig/utils/procs.py +7 -7
- machineconfig/utils/source_of_truth.py +2 -0
- {machineconfig-2.8.dist-info → machineconfig-2.9.dist-info}/METADATA +7 -10
- {machineconfig-2.8.dist-info → machineconfig-2.9.dist-info}/RECORD +26 -22
- {machineconfig-2.8.dist-info → machineconfig-2.9.dist-info}/WHEEL +0 -0
- {machineconfig-2.8.dist-info → machineconfig-2.9.dist-info}/entry_points.txt +0 -0
- {machineconfig-2.8.dist-info → machineconfig-2.9.dist-info}/top_level.txt +0 -0
|
@@ -10,20 +10,25 @@ Improved design notes:
|
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
|
|
12
12
|
from pathlib import Path
|
|
13
|
-
import shlex
|
|
14
13
|
from math import ceil
|
|
15
14
|
from typing import Literal, TypeAlias, get_args, Iterable
|
|
16
15
|
|
|
17
16
|
from machineconfig.cluster.sessions_managers.zellij_local_manager import ZellijLocalManager
|
|
18
|
-
from machineconfig.
|
|
19
|
-
from machineconfig.
|
|
20
|
-
import
|
|
17
|
+
from machineconfig.scripts.python.fire_agents_help_launch import launch_agents
|
|
18
|
+
from machineconfig.scripts.python.fire_agents_help_search import search_files_by_pattern, search_python_files
|
|
19
|
+
from machineconfig.utils.schemas.layouts.layout_types import LayoutConfig
|
|
21
20
|
# import time
|
|
22
21
|
|
|
23
22
|
AGENTS: TypeAlias = Literal[
|
|
24
23
|
"cursor-agent", "gemini", "crush", "q", "onlyPrepPromptFiles"
|
|
25
24
|
# warp terminal
|
|
26
25
|
]
|
|
26
|
+
|
|
27
|
+
SPLITTING_STRATEGY: TypeAlias = Literal[
|
|
28
|
+
"agent_cap", # User decides number of agents, rows/tasks determined automatically
|
|
29
|
+
"task_rows" # User decides number of rows/tasks, number of agents determined automatically
|
|
30
|
+
]
|
|
31
|
+
|
|
27
32
|
DEFAULT_AGENT_CAP = 6
|
|
28
33
|
|
|
29
34
|
|
|
@@ -43,153 +48,86 @@ def get_gemini_api_keys() -> list[str]:
|
|
|
43
48
|
return res
|
|
44
49
|
|
|
45
50
|
|
|
46
|
-
def _search_python_files(repo_root: Path, keyword: str) -> list[Path]:
|
|
47
|
-
"""Return all Python files under repo_root whose text contains keyword.
|
|
48
|
-
|
|
49
|
-
Notes:
|
|
50
|
-
- Skips any paths that reside under a directory named ".venv" at any depth.
|
|
51
|
-
- Errors reading individual files are ignored (decoded with 'ignore').
|
|
52
|
-
"""
|
|
53
|
-
py_files = list(repo_root.rglob("*.py"))
|
|
54
|
-
keyword_lower = keyword.lower()
|
|
55
|
-
matches: list[Path] = []
|
|
56
|
-
for f in py_files:
|
|
57
|
-
# Skip anything under a .venv directory anywhere in the path
|
|
58
|
-
if any(part == ".venv" for part in f.parts):
|
|
59
|
-
continue
|
|
60
|
-
try:
|
|
61
|
-
if keyword_lower in f.read_text(encoding="utf-8", errors="ignore").lower():
|
|
62
|
-
matches.append(f)
|
|
63
|
-
except OSError:
|
|
64
|
-
# Skip unreadable file
|
|
65
|
-
continue
|
|
66
|
-
return matches
|
|
67
|
-
|
|
68
|
-
|
|
69
51
|
def _write_list_file(target: Path, files: Iterable[Path]) -> None:
|
|
70
52
|
target.parent.mkdir(parents=True, exist_ok=True)
|
|
71
53
|
target.write_text("\n".join(str(f) for f in files), encoding="utf-8")
|
|
72
54
|
|
|
73
55
|
|
|
74
|
-
def _chunk_prompts(prompts: list[str],
|
|
75
|
-
prompts
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
grouped.append("\nTargeted Locations:\n".join(prompts[i : i + chunk_size]))
|
|
83
|
-
return grouped
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
def _confirm(message: str, default_no: bool = True) -> bool:
|
|
87
|
-
suffix = "[y/N]" if default_no else "[Y/n]"
|
|
88
|
-
answer = input(f"{message} {suffix} ").strip().lower()
|
|
89
|
-
if answer in {"y", "yes"}:
|
|
90
|
-
return True
|
|
91
|
-
if not default_no and answer == "":
|
|
92
|
-
return True
|
|
93
|
-
return False
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
def launch_agents(repo_root: Path, prompts: list[str], agent: AGENTS, *, max_agents: int = DEFAULT_AGENT_CAP) -> list[TabConfig]:
|
|
97
|
-
"""Create tab configuration for a set of agent prompts.
|
|
98
|
-
|
|
99
|
-
If number of prompts exceeds max_agents, ask user for confirmation.
|
|
100
|
-
(Original behavior raised an error; now interactive override.)
|
|
56
|
+
def _chunk_prompts(prompts: list[str], strategy: SPLITTING_STRATEGY, *, agent_cap: int | None, task_rows: int | None) -> list[str]:
|
|
57
|
+
"""Chunk prompts based on splitting strategy.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
prompts: List of prompts to chunk
|
|
61
|
+
strategy: Either 'agent_cap' or 'task_rows'
|
|
62
|
+
agent_cap: Maximum number of agents (used with 'agent_cap' strategy)
|
|
63
|
+
task_rows: Number of rows/tasks per agent (used with 'task_rows' strategy)
|
|
101
64
|
"""
|
|
102
|
-
if
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
return
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
cmd = f"""
|
|
134
|
-
export GEMINI_API_KEY={shlex.quote(api_key)}
|
|
135
|
-
echo "Using Gemini API key $GEMINI_API_KEY"
|
|
136
|
-
cat {prompt_path}
|
|
137
|
-
GEMINI_API_KEY={shlex.quote(api_key)} bash -lc 'cat {safe_path} | gemini {model_arg} --yolo --prompt'
|
|
138
|
-
"""
|
|
139
|
-
case "cursor-agent":
|
|
140
|
-
# As originally implemented
|
|
141
|
-
cmd = f"""
|
|
142
|
-
|
|
143
|
-
cursor-agent --print --output-format text < {prompt_path}
|
|
144
|
-
|
|
145
|
-
"""
|
|
146
|
-
case "crush":
|
|
147
|
-
cmd = f"""
|
|
148
|
-
# cat {prompt_path} | crush run
|
|
149
|
-
crush run {prompt_path}
|
|
150
|
-
"""
|
|
151
|
-
case "q":
|
|
152
|
-
cmd = f"""
|
|
153
|
-
q chat --no-interactive --trust-all-tools {prompt_path}
|
|
154
|
-
"""
|
|
155
|
-
case "onlyPrepPromptFiles":
|
|
156
|
-
cmd = f"""
|
|
157
|
-
echo "Prepared prompt file at {prompt_path}"
|
|
158
|
-
"""
|
|
159
|
-
case _:
|
|
160
|
-
raise ValueError(f"Unsupported agent type: {agent}")
|
|
161
|
-
random_sleep_time = random.uniform(0, 5)
|
|
162
|
-
cmd_prefix = f"""
|
|
163
|
-
echo "Sleeping for {random_sleep_time:.2f} seconds to stagger agent startups..."
|
|
164
|
-
sleep {random_sleep_time:.2f}
|
|
165
|
-
echo "Launching `{agent}` with prompt from {prompt_path}"
|
|
166
|
-
echo "Launching `{agent}` with command from {cmd_path}"
|
|
167
|
-
echo "--------START OF AGENT OUTPUT--------"
|
|
168
|
-
sleep 0.1
|
|
169
|
-
"""
|
|
170
|
-
cmd_postfix = """
|
|
171
|
-
sleep 0.1
|
|
172
|
-
echo "---------END OF AGENT OUTPUT---------"
|
|
173
|
-
"""
|
|
174
|
-
cmd_path.write_text(cmd_prefix + cmd + cmd_postfix, encoding="utf-8")
|
|
175
|
-
fire_cmd = f"bash {shlex.quote(str(cmd_path))}"
|
|
176
|
-
tab_config.append(TabConfig(tabName=f"Agent{idx}", startDir=str(repo_root), command=fire_cmd))
|
|
65
|
+
prompts = [p for p in prompts if p.strip() != ""] # drop blank entries
|
|
66
|
+
|
|
67
|
+
if strategy == "agent_cap":
|
|
68
|
+
if agent_cap is None:
|
|
69
|
+
raise ValueError("agent_cap must be provided when using 'agent_cap' strategy")
|
|
70
|
+
|
|
71
|
+
if len(prompts) <= agent_cap:
|
|
72
|
+
return prompts
|
|
73
|
+
|
|
74
|
+
print(f"Chunking {len(prompts)} prompts into groups for up to {agent_cap} agents because it exceeds the cap.")
|
|
75
|
+
chunk_size = ceil(len(prompts) / agent_cap)
|
|
76
|
+
grouped: list[str] = []
|
|
77
|
+
for i in range(0, len(prompts), chunk_size):
|
|
78
|
+
grouped.append("\nTargeted Locations:\n".join(prompts[i : i + chunk_size]))
|
|
79
|
+
return grouped
|
|
80
|
+
|
|
81
|
+
elif strategy == "task_rows":
|
|
82
|
+
if task_rows is None:
|
|
83
|
+
raise ValueError("task_rows must be provided when using 'task_rows' strategy")
|
|
84
|
+
|
|
85
|
+
if task_rows >= len(prompts):
|
|
86
|
+
return prompts
|
|
87
|
+
|
|
88
|
+
print(f"Chunking {len(prompts)} prompts into groups of {task_rows} rows/tasks each.")
|
|
89
|
+
grouped: list[str] = []
|
|
90
|
+
for i in range(0, len(prompts), task_rows):
|
|
91
|
+
grouped.append("\nTargeted Locations:\n".join(prompts[i : i + task_rows]))
|
|
92
|
+
return grouped
|
|
93
|
+
|
|
94
|
+
else:
|
|
95
|
+
raise ValueError(f"Unknown splitting strategy: {strategy}")
|
|
177
96
|
|
|
178
|
-
print(f"Launching a template with #{len(tab_config)} agents")
|
|
179
|
-
return tab_config
|
|
180
97
|
|
|
181
98
|
|
|
182
99
|
def main(): # noqa: C901 - (complexity acceptable for CLI glue)
|
|
183
100
|
repo_root = Path.cwd()
|
|
184
101
|
print(f"Operating @ {repo_root}")
|
|
185
102
|
|
|
186
|
-
|
|
187
|
-
|
|
103
|
+
from machineconfig.utils.options import choose_one_option
|
|
104
|
+
|
|
105
|
+
# Prompt user to choose search strategy
|
|
106
|
+
search_strategies = ["file_path", "keyword_search", "filename_pattern"]
|
|
107
|
+
search_strategy = choose_one_option(
|
|
108
|
+
header="Choose search strategy:",
|
|
109
|
+
options=search_strategies
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
# Execute chosen search strategy
|
|
113
|
+
if search_strategy == "file_path":
|
|
114
|
+
file_path_input = input("Enter path to target file: ").strip()
|
|
115
|
+
if not file_path_input:
|
|
116
|
+
print("No file path provided. Exiting.")
|
|
117
|
+
return
|
|
118
|
+
target_file_path = Path(file_path_input).expanduser().resolve()
|
|
119
|
+
if not target_file_path.exists() or not target_file_path.is_file():
|
|
120
|
+
print(f"Invalid file path: {target_file_path}")
|
|
121
|
+
return
|
|
122
|
+
separator = input("Enter separator [\\n]: ").strip() or "\n"
|
|
123
|
+
source_text = target_file_path.read_text(encoding="utf-8", errors="ignore")
|
|
124
|
+
|
|
125
|
+
elif search_strategy == "keyword_search":
|
|
188
126
|
keyword = input("Enter keyword to search recursively for all .py files containing it: ").strip()
|
|
189
127
|
if not keyword:
|
|
190
128
|
print("No keyword supplied. Exiting.")
|
|
191
129
|
return
|
|
192
|
-
matching_files =
|
|
130
|
+
matching_files = search_python_files(repo_root, keyword)
|
|
193
131
|
if not matching_files:
|
|
194
132
|
print(f"💥 No .py files found containing keyword: {keyword}")
|
|
195
133
|
return
|
|
@@ -200,33 +138,52 @@ def main(): # noqa: C901 - (complexity acceptable for CLI glue)
|
|
|
200
138
|
_write_list_file(target_list_file, matching_files)
|
|
201
139
|
separator = "\n"
|
|
202
140
|
source_text = target_list_file.read_text(encoding="utf-8", errors="ignore")
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
141
|
+
|
|
142
|
+
elif search_strategy == "filename_pattern":
|
|
143
|
+
pattern = input("Enter filename pattern (e.g., '*.py', '*test*', 'config.*'): ").strip()
|
|
144
|
+
if not pattern:
|
|
145
|
+
print("No pattern supplied. Exiting.")
|
|
207
146
|
return
|
|
208
|
-
|
|
209
|
-
if not
|
|
210
|
-
print(f"
|
|
147
|
+
matching_files = search_files_by_pattern(repo_root, pattern)
|
|
148
|
+
if not matching_files:
|
|
149
|
+
print(f"💥 No files found matching pattern: {pattern}")
|
|
211
150
|
return
|
|
212
|
-
|
|
213
|
-
|
|
151
|
+
for idx, mf in enumerate(matching_files):
|
|
152
|
+
print(f"{idx:>3}: {mf}")
|
|
153
|
+
print(f"\nFound {len(matching_files)} files matching pattern: {pattern}")
|
|
154
|
+
target_list_file = repo_root / ".ai" / "target_file.txt"
|
|
155
|
+
_write_list_file(target_list_file, matching_files)
|
|
156
|
+
separator = "\n"
|
|
157
|
+
source_text = target_list_file.read_text(encoding="utf-8", errors="ignore")
|
|
158
|
+
else:
|
|
159
|
+
raise ValueError(f"Unknown search strategy: {search_strategy}")
|
|
214
160
|
raw_prompts = source_text.split(separator)
|
|
215
161
|
print(f"Loaded {len(raw_prompts)} raw prompts from source.")
|
|
216
162
|
prefix = input("Enter prefix prompt: ")
|
|
217
|
-
|
|
163
|
+
# Prompt user for splitting strategy
|
|
164
|
+
splitting_strategy = choose_one_option(header="Select splitting strategy", options=get_args(SPLITTING_STRATEGY))
|
|
165
|
+
# Get parameters based on strategy
|
|
166
|
+
if splitting_strategy == "agent_cap":
|
|
167
|
+
agent_cap_input = input(f"Enter maximum number of agents/splits [default: {DEFAULT_AGENT_CAP}]: ").strip()
|
|
168
|
+
agent_cap = int(agent_cap_input) if agent_cap_input else DEFAULT_AGENT_CAP
|
|
169
|
+
combined_prompts = _chunk_prompts(raw_prompts, splitting_strategy, agent_cap=agent_cap, task_rows=None)
|
|
170
|
+
max_agents_for_launch = agent_cap
|
|
171
|
+
elif splitting_strategy == "task_rows":
|
|
172
|
+
task_rows_input = input("Enter number of rows/tasks per agent: ").strip()
|
|
173
|
+
if not task_rows_input:
|
|
174
|
+
print("Number of rows/tasks is required for this strategy.")
|
|
175
|
+
return
|
|
176
|
+
task_rows = int(task_rows_input)
|
|
177
|
+
combined_prompts = _chunk_prompts(raw_prompts, splitting_strategy, agent_cap=None, task_rows=task_rows)
|
|
178
|
+
max_agents_for_launch = len(combined_prompts) # Number of agents determined by chunking
|
|
179
|
+
else:
|
|
180
|
+
raise ValueError(f"Unknown splitting strategy: {splitting_strategy}")
|
|
218
181
|
combined_prompts = [prefix + "\n" + p for p in combined_prompts]
|
|
219
|
-
|
|
220
|
-
from machineconfig.utils.options import choose_one_option
|
|
221
|
-
|
|
222
182
|
agent_selected = choose_one_option(header="Select agent type", options=get_args(AGENTS))
|
|
223
|
-
|
|
224
|
-
tab_config = launch_agents(repo_root=repo_root, prompts=combined_prompts, agent=agent_selected, max_agents=DEFAULT_AGENT_CAP)
|
|
183
|
+
tab_config = launch_agents(repo_root=repo_root, prompts=combined_prompts, agent=agent_selected, max_agents=max_agents_for_launch)
|
|
225
184
|
if not tab_config:
|
|
226
185
|
return
|
|
227
|
-
|
|
228
186
|
from machineconfig.utils.utils2 import randstr
|
|
229
|
-
|
|
230
187
|
random_name = randstr(length=3)
|
|
231
188
|
manager = ZellijLocalManager(session_layouts=[LayoutConfig(layoutName="Agents", layoutTabs=tab_config)], session_name_prefix=random_name)
|
|
232
189
|
manager.start_all_sessions()
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
from machineconfig.scripts.python.fire_agents import AGENTS, get_gemini_api_keys
|
|
2
|
+
from machineconfig.utils.schemas.layouts.layout_types import TabConfig
|
|
3
|
+
from machineconfig.utils.utils2 import randstr
|
|
4
|
+
|
|
5
|
+
import random
|
|
6
|
+
import shlex
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
def _confirm(message: str, default_no: bool = False) -> bool:
|
|
10
|
+
from rich.prompt import Confirm
|
|
11
|
+
return Confirm.ask(message, default=not default_no)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def launch_agents(repo_root: Path, prompts: list[str], agent: AGENTS, *, max_agents: int) -> list[TabConfig]:
|
|
15
|
+
"""Create tab configuration for a set of agent prompts.
|
|
16
|
+
|
|
17
|
+
If number of prompts exceeds max_agents, ask user for confirmation.
|
|
18
|
+
(Original behavior raised an error; now interactive override.)
|
|
19
|
+
"""
|
|
20
|
+
if not prompts:
|
|
21
|
+
raise ValueError("No prompts provided")
|
|
22
|
+
|
|
23
|
+
if len(prompts) > max_agents:
|
|
24
|
+
proceed = _confirm(message=(f"You are about to launch {len(prompts)} agents which exceeds the cap ({max_agents}). Proceed?"), default_no=True)
|
|
25
|
+
if not proceed:
|
|
26
|
+
print("Aborting per user choice.")
|
|
27
|
+
return []
|
|
28
|
+
|
|
29
|
+
tab_config: list[TabConfig] = []
|
|
30
|
+
tmp_dir = repo_root / ".ai" / f"tmp_prompts/{randstr()}"
|
|
31
|
+
tmp_dir.mkdir(parents=True, exist_ok=True)
|
|
32
|
+
|
|
33
|
+
for idx, a_prompt in enumerate(prompts):
|
|
34
|
+
prompt_path = tmp_dir / f"agent{idx}_prompt.txt"
|
|
35
|
+
prompt_path.write_text(a_prompt, encoding="utf-8")
|
|
36
|
+
cmd_path = tmp_dir / f"agent{idx}_cmd.sh"
|
|
37
|
+
match agent:
|
|
38
|
+
case "gemini":
|
|
39
|
+
# model = "gemini-2.5-pro"
|
|
40
|
+
# model = "gemini-2.5-flash-lite"
|
|
41
|
+
model = None # auto-select
|
|
42
|
+
if model is None:
|
|
43
|
+
model_arg = ""
|
|
44
|
+
else:
|
|
45
|
+
model_arg = f"--model {shlex.quote(model)}"
|
|
46
|
+
# Need a real shell for the pipeline; otherwise '| gemini ...' is passed as args to 'cat'
|
|
47
|
+
safe_path = shlex.quote(str(prompt_path))
|
|
48
|
+
api_keys = get_gemini_api_keys()
|
|
49
|
+
api_key = api_keys[idx % len(api_keys)] if api_keys else ""
|
|
50
|
+
# Export the environment variable so it's available to subshells
|
|
51
|
+
cmd = f"""
|
|
52
|
+
export GEMINI_API_KEY={shlex.quote(api_key)}
|
|
53
|
+
echo "Using Gemini API key $GEMINI_API_KEY"
|
|
54
|
+
cat {prompt_path}
|
|
55
|
+
GEMINI_API_KEY={shlex.quote(api_key)} bash -lc 'cat {safe_path} | gemini {model_arg} --yolo --prompt'
|
|
56
|
+
"""
|
|
57
|
+
case "cursor-agent":
|
|
58
|
+
# As originally implemented
|
|
59
|
+
cmd = f"""
|
|
60
|
+
|
|
61
|
+
cursor-agent --print --output-format text < {prompt_path}
|
|
62
|
+
|
|
63
|
+
"""
|
|
64
|
+
case "crush":
|
|
65
|
+
cmd = f"""
|
|
66
|
+
# cat {prompt_path} | crush run
|
|
67
|
+
crush run {prompt_path}
|
|
68
|
+
"""
|
|
69
|
+
case "q":
|
|
70
|
+
cmd = f"""
|
|
71
|
+
q chat --no-interactive --trust-all-tools {prompt_path}
|
|
72
|
+
"""
|
|
73
|
+
case "onlyPrepPromptFiles":
|
|
74
|
+
cmd = f"""
|
|
75
|
+
echo "Prepared prompt file at {prompt_path}"
|
|
76
|
+
"""
|
|
77
|
+
case _:
|
|
78
|
+
raise ValueError(f"Unsupported agent type: {agent}")
|
|
79
|
+
random_sleep_time = random.uniform(0, 5)
|
|
80
|
+
cmd_prefix = f"""
|
|
81
|
+
echo "Sleeping for {random_sleep_time:.2f} seconds to stagger agent startups..."
|
|
82
|
+
sleep {random_sleep_time:.2f}
|
|
83
|
+
echo "Launching `{agent}` with prompt from {prompt_path}"
|
|
84
|
+
echo "Launching `{agent}` with command from {cmd_path}"
|
|
85
|
+
echo "--------START OF AGENT OUTPUT--------"
|
|
86
|
+
sleep 0.1
|
|
87
|
+
"""
|
|
88
|
+
cmd_postfix = """
|
|
89
|
+
sleep 0.1
|
|
90
|
+
echo "---------END OF AGENT OUTPUT---------"
|
|
91
|
+
"""
|
|
92
|
+
cmd_path.write_text(cmd_prefix + cmd + cmd_postfix, encoding="utf-8")
|
|
93
|
+
fire_cmd = f"bash {shlex.quote(str(cmd_path))}"
|
|
94
|
+
tab_config.append(TabConfig(tabName=f"Agent{idx}", startDir=str(repo_root), command=fire_cmd))
|
|
95
|
+
|
|
96
|
+
print(f"Launching a template with #{len(tab_config)} agents")
|
|
97
|
+
return tab_config
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
from machineconfig.utils.source_of_truth import EXCLUDE_DIRS
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
import fnmatch
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def search_files_by_pattern(repo_root: Path, pattern: str) -> list[Path]:
|
|
9
|
+
"""Return all files under repo_root whose filename matches the given pattern.
|
|
10
|
+
|
|
11
|
+
Notes:
|
|
12
|
+
- Uses glob-style pattern matching (e.g., "*.py", "*test*", "config.*")
|
|
13
|
+
- Skips any paths that reside under directories listed in EXCLUDE_DIRS at any depth.
|
|
14
|
+
"""
|
|
15
|
+
matches: list[Path] = []
|
|
16
|
+
|
|
17
|
+
def _should_skip_dir(dir_path: Path) -> bool:
|
|
18
|
+
"""Check if directory should be skipped based on EXCLUDE_DIRS."""
|
|
19
|
+
return any(part in EXCLUDE_DIRS for part in dir_path.parts)
|
|
20
|
+
|
|
21
|
+
def _walk_and_filter(current_path: Path) -> None:
|
|
22
|
+
"""Recursively walk directories, filtering out excluded ones early."""
|
|
23
|
+
try:
|
|
24
|
+
if not current_path.is_dir():
|
|
25
|
+
return
|
|
26
|
+
|
|
27
|
+
# Skip if current directory is in exclude list
|
|
28
|
+
if _should_skip_dir(current_path):
|
|
29
|
+
return
|
|
30
|
+
|
|
31
|
+
for item in current_path.iterdir():
|
|
32
|
+
if item.is_dir():
|
|
33
|
+
_walk_and_filter(item)
|
|
34
|
+
elif item.is_file() and fnmatch.fnmatch(item.name, pattern):
|
|
35
|
+
matches.append(item)
|
|
36
|
+
except (OSError, PermissionError):
|
|
37
|
+
# Skip directories we can't read
|
|
38
|
+
return
|
|
39
|
+
|
|
40
|
+
_walk_and_filter(repo_root)
|
|
41
|
+
return matches
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def search_python_files(repo_root: Path, keyword: str) -> list[Path]:
|
|
45
|
+
"""Return all Python files under repo_root whose text contains keyword.
|
|
46
|
+
|
|
47
|
+
Notes:
|
|
48
|
+
- Skips any paths that reside under directories listed in EXCLUDE_DIRS at any depth.
|
|
49
|
+
- Errors reading individual files are ignored (decoded with 'ignore').
|
|
50
|
+
"""
|
|
51
|
+
keyword_lower = keyword.lower()
|
|
52
|
+
matches: list[Path] = []
|
|
53
|
+
|
|
54
|
+
def _should_skip_dir(dir_path: Path) -> bool:
|
|
55
|
+
"""Check if directory should be skipped based on EXCLUDE_DIRS."""
|
|
56
|
+
return any(part in EXCLUDE_DIRS for part in dir_path.parts)
|
|
57
|
+
|
|
58
|
+
def _walk_and_filter(current_path: Path) -> None:
|
|
59
|
+
"""Recursively walk directories, filtering out excluded ones early."""
|
|
60
|
+
try:
|
|
61
|
+
if not current_path.is_dir():
|
|
62
|
+
return
|
|
63
|
+
|
|
64
|
+
# Skip if current directory is in exclude list
|
|
65
|
+
if _should_skip_dir(current_path):
|
|
66
|
+
return
|
|
67
|
+
|
|
68
|
+
for item in current_path.iterdir():
|
|
69
|
+
if item.is_dir():
|
|
70
|
+
_walk_and_filter(item)
|
|
71
|
+
elif item.is_file() and item.suffix == ".py":
|
|
72
|
+
try:
|
|
73
|
+
if keyword_lower in item.read_text(encoding="utf-8", errors="ignore").lower():
|
|
74
|
+
matches.append(item)
|
|
75
|
+
except OSError:
|
|
76
|
+
# Skip unreadable file
|
|
77
|
+
continue
|
|
78
|
+
except (OSError, PermissionError):
|
|
79
|
+
# Skip directories we can't read
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
_walk_and_filter(repo_root)
|
|
83
|
+
return matches
|
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
from pathlib import Path
|
|
2
2
|
from machineconfig.utils.utils2 import pprint, read_json, read_ini
|
|
3
|
-
from pydantic import ConfigDict
|
|
4
|
-
from pydantic.dataclasses import dataclass
|
|
5
3
|
from typing import Optional
|
|
6
4
|
import os
|
|
7
5
|
from machineconfig.utils.source_of_truth import DEFAULTS_PATH
|
|
8
6
|
from rich.console import Console
|
|
9
7
|
from rich.panel import Panel
|
|
10
8
|
from rich import box # Import box
|
|
11
|
-
|
|
9
|
+
from dataclasses import dataclass
|
|
12
10
|
|
|
13
11
|
console = Console()
|
|
14
12
|
|
|
@@ -26,8 +24,7 @@ class ArgsDefaults:
|
|
|
26
24
|
key = None
|
|
27
25
|
pwd = None
|
|
28
26
|
|
|
29
|
-
|
|
30
|
-
@dataclass(config=ConfigDict(extra="forbid", frozen=False))
|
|
27
|
+
@dataclass
|
|
31
28
|
class Args:
|
|
32
29
|
cloud: Optional[str] = None
|
|
33
30
|
|
|
@@ -8,7 +8,7 @@ in the event that username@github.com is not mentioned in the remote url.
|
|
|
8
8
|
from machineconfig.utils.source_of_truth import CONFIG_PATH, DEFAULTS_PATH
|
|
9
9
|
from machineconfig.utils.path_reduced import PathExtended as PathExtended
|
|
10
10
|
from machineconfig.utils.utils2 import randstr, read_ini
|
|
11
|
-
from machineconfig.scripts.python.
|
|
11
|
+
from machineconfig.scripts.python.repos_helper_update import update_repository
|
|
12
12
|
from machineconfig.scripts.python.repos_helper_record import main as record_repos
|
|
13
13
|
|
|
14
14
|
import argparse
|