machineconfig 2.8__py3-none-any.whl → 2.94__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.

Files changed (34) hide show
  1. machineconfig/cluster/sessions_managers/zellij_local_manager.py +4 -20
  2. machineconfig/jobs/python/python_ve_symlink.py +1 -1
  3. machineconfig/scripts/python/__pycache__/devops.cpython-313.pyc +0 -0
  4. machineconfig/scripts/python/__pycache__/devops_update_repos.cpython-313.pyc +0 -0
  5. machineconfig/scripts/python/__pycache__/fire_agents.cpython-313.pyc +0 -0
  6. machineconfig/scripts/python/__pycache__/repos_helper_update.cpython-313.pyc +0 -0
  7. machineconfig/scripts/python/ai/mcinit.py +2 -14
  8. machineconfig/scripts/python/devops.py +18 -64
  9. machineconfig/scripts/python/devops_add_identity.py +6 -2
  10. machineconfig/scripts/python/devops_add_ssh_key.py +5 -2
  11. machineconfig/scripts/python/devops_backup_retrieve.py +3 -15
  12. machineconfig/scripts/python/devops_devapps_install.py +8 -6
  13. machineconfig/scripts/python/devops_update_repos.py +122 -226
  14. machineconfig/scripts/python/fire_agents.py +111 -198
  15. machineconfig/scripts/python/fire_agents_help_launch.py +142 -0
  16. machineconfig/scripts/python/fire_agents_help_search.py +82 -0
  17. machineconfig/scripts/python/fire_agents_load_balancer.py +52 -0
  18. machineconfig/scripts/python/fire_jobs.py +2 -1
  19. machineconfig/scripts/python/helpers/cloud_helpers.py +2 -5
  20. machineconfig/scripts/python/repos.py +1 -1
  21. machineconfig/scripts/python/repos_helper_update.py +265 -0
  22. machineconfig/utils/installer_utils/installer_class.py +3 -3
  23. machineconfig/utils/notifications.py +24 -4
  24. machineconfig/utils/path.py +2 -1
  25. machineconfig/utils/procs.py +7 -7
  26. machineconfig/utils/schemas/fire_agents/fire_agents_input.py +70 -0
  27. machineconfig/utils/schemas/layouts/layout_types.py +0 -1
  28. machineconfig/utils/source_of_truth.py +2 -0
  29. machineconfig/utils/ve.py +9 -5
  30. {machineconfig-2.8.dist-info → machineconfig-2.94.dist-info}/METADATA +7 -10
  31. {machineconfig-2.8.dist-info → machineconfig-2.94.dist-info}/RECORD +34 -27
  32. {machineconfig-2.8.dist-info → machineconfig-2.94.dist-info}/WHEEL +0 -0
  33. {machineconfig-2.8.dist-info → machineconfig-2.94.dist-info}/entry_points.txt +0 -0
  34. {machineconfig-2.8.dist-info → machineconfig-2.94.dist-info}/top_level.txt +0 -0
@@ -7,63 +7,19 @@ Improved design notes:
7
7
  * Preserves original core behavior & command generation for each agent type.
8
8
  """
9
9
 
10
- from __future__ import annotations
11
-
12
10
  from pathlib import Path
13
- import shlex
14
- from math import ceil
15
- from typing import Literal, TypeAlias, get_args, Iterable
11
+ from typing import cast, get_args, Iterable, TypeAlias, Literal
12
+ import json
13
+ import sys
16
14
 
15
+ from machineconfig.scripts.python.fire_agents_help_launch import prep_agent_launch, get_agents_launch_layout, AGENTS
16
+ from machineconfig.scripts.python.fire_agents_help_search import search_files_by_pattern, search_python_files
17
+ from machineconfig.scripts.python.fire_agents_load_balancer import chunk_prompts, SPLITTING_STRATEGY, DEFAULT_AGENT_CAP
17
18
  from machineconfig.cluster.sessions_managers.zellij_local_manager import ZellijLocalManager
18
- from machineconfig.utils.schemas.layouts.layout_types import TabConfig, LayoutConfig
19
- from machineconfig.utils.utils2 import randstr
20
- import random
21
- # import time
22
-
23
- AGENTS: TypeAlias = Literal[
24
- "cursor-agent", "gemini", "crush", "q", "onlyPrepPromptFiles"
25
- # warp terminal
26
- ]
27
- DEFAULT_AGENT_CAP = 6
28
-
29
-
30
- def get_gemini_api_keys() -> list[str]:
31
- from machineconfig.utils.utils2 import read_ini
32
-
33
- config = read_ini(Path.home().joinpath("dotfiles/creds/llm/gemini/api_keys.ini"))
34
- res: list[str] = []
35
- for a_section_name in list(config.sections()):
36
- a_section = config[a_section_name]
37
- if "api_key" in a_section:
38
- api_key = a_section["api_key"].strip()
39
- if api_key:
40
- res.append(api_key)
41
- # res = [v for k, v in config.items("api_keys") if k.startswith("key") and v.strip() != ""]
42
- print(f"Found {len(res)} Gemini API keys configured.")
43
- return res
44
-
45
-
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
19
+ from machineconfig.utils.options import choose_one_option
20
+ from machineconfig.utils.ve import get_repo_root
21
+
22
+ SEARCH_STRATEGIES: TypeAlias = Literal["file_path", "keyword_search", "filename_pattern"]
67
23
 
68
24
 
69
25
  def _write_list_file(target: Path, files: Iterable[Path]) -> None:
@@ -71,164 +27,121 @@ def _write_list_file(target: Path, files: Iterable[Path]) -> None:
71
27
  target.write_text("\n".join(str(f) for f in files), encoding="utf-8")
72
28
 
73
29
 
74
- def _chunk_prompts(prompts: list[str], max_agents: int) -> list[str]:
75
- prompts = [p for p in prompts if p.strip() != ""] # drop blank entries
76
- if len(prompts) <= max_agents:
77
- return prompts
78
- print(f"Chunking {len(prompts)} prompts into groups for up to {max_agents} agents because it exceeds the cap.")
79
- chunk_size = ceil(len(prompts) / max_agents)
80
- grouped: list[str] = []
81
- for i in range(0, len(prompts), chunk_size):
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.)
101
- """
102
- if not prompts:
103
- raise ValueError("No prompts provided")
104
-
105
- if len(prompts) > max_agents:
106
- proceed = _confirm(message=(f"You are about to launch {len(prompts)} agents which exceeds the cap ({max_agents}). Proceed?"))
107
- if not proceed:
108
- print("Aborting per user choice.")
109
- return []
110
-
111
- tab_config: list[TabConfig] = []
112
- tmp_dir = repo_root / ".ai" / f"tmp_prompts/{randstr()}"
113
- tmp_dir.mkdir(parents=True, exist_ok=True)
114
-
115
- for idx, a_prompt in enumerate(prompts):
116
- prompt_path = tmp_dir / f"agent{idx}_prompt.txt"
117
- prompt_path.write_text(a_prompt, encoding="utf-8")
118
- cmd_path = tmp_dir / f"agent{idx}_cmd.sh"
119
- match agent:
120
- case "gemini":
121
- # model = "gemini-2.5-pro"
122
- # model = "gemini-2.5-flash-lite"
123
- model = None # auto-select
124
- if model is None:
125
- model_arg = ""
126
- else:
127
- model_arg = f"--model {shlex.quote(model)}"
128
- # Need a real shell for the pipeline; otherwise '| gemini ...' is passed as args to 'cat'
129
- safe_path = shlex.quote(str(prompt_path))
130
- api_keys = get_gemini_api_keys()
131
- api_key = api_keys[idx % len(api_keys)] if api_keys else ""
132
- # Export the environment variable so it's available to subshells
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))
177
-
178
- print(f"Launching a template with #{len(tab_config)} agents")
179
- return tab_config
180
-
181
-
182
- def main(): # noqa: C901 - (complexity acceptable for CLI glue)
183
- repo_root = Path.cwd()
184
- print(f"Operating @ {repo_root}")
185
-
186
- file_path_input = input("Enter path to target file [press Enter to generate it from searching]: ").strip()
187
- if file_path_input == "":
30
+ def get_prompt_material(search_strategy: SEARCH_STRATEGIES, repo_root: Path) -> tuple[Path, str]:
31
+ if search_strategy == "file_path":
32
+ file_path_input = input("Enter path to target file: ").strip()
33
+ if not file_path_input:
34
+ print("No file path provided. Exiting.")
35
+ sys.exit(1)
36
+ target_file_path = Path(file_path_input).expanduser().resolve()
37
+ if not target_file_path.exists() or not target_file_path.is_file():
38
+ print(f"Invalid file path: {target_file_path}")
39
+ sys.exit(1)
40
+ separator = input("Enter separator [\\n]: ").strip() or "\n"
41
+ elif search_strategy == "keyword_search":
188
42
  keyword = input("Enter keyword to search recursively for all .py files containing it: ").strip()
189
43
  if not keyword:
190
44
  print("No keyword supplied. Exiting.")
191
- return
192
- matching_files = _search_python_files(repo_root, keyword)
45
+ sys.exit(1)
46
+ matching_files = search_python_files(repo_root, keyword)
193
47
  if not matching_files:
194
48
  print(f"💥 No .py files found containing keyword: {keyword}")
195
- return
49
+ sys.exit(1)
196
50
  for idx, mf in enumerate(matching_files):
197
51
  print(f"{idx:>3}: {mf}")
198
52
  print(f"\nFound {len(matching_files)} .py files containing keyword: {keyword}")
199
- target_list_file = repo_root / ".ai" / "target_file.txt"
200
- _write_list_file(target_list_file, matching_files)
53
+ target_file_path = repo_root / ".ai" / "target_file.txt"
54
+ _write_list_file(target_file_path, matching_files)
55
+ separator = "\n"
56
+ elif search_strategy == "filename_pattern":
57
+ pattern = input("Enter filename pattern (e.g., '*.py', '*test*', 'config.*'): ").strip()
58
+ if not pattern:
59
+ print("No pattern supplied. Exiting.")
60
+ sys.exit(1)
61
+ matching_files = search_files_by_pattern(repo_root, pattern)
62
+ if not matching_files:
63
+ print(f"💥 No files found matching pattern: {pattern}")
64
+ sys.exit(1)
65
+ for idx, mf in enumerate(matching_files):
66
+ print(f"{idx:>3}: {mf}")
67
+ print(f"\nFound {len(matching_files)} files matching pattern: {pattern}")
68
+ target_file_path = repo_root / ".ai" / "target_file.txt"
69
+ _write_list_file(target_file_path, matching_files)
201
70
  separator = "\n"
202
- source_text = target_list_file.read_text(encoding="utf-8", errors="ignore")
203
71
  else:
204
- target_file_path = Path(file_path_input).expanduser().resolve()
205
- if not target_file_path.exists() or not target_file_path.is_file():
206
- print(f"Invalid file path: {target_file_path}")
207
- return
208
- separator = input("Enter separator [\\n]: ").strip() or "\n"
209
- if not target_file_path.exists():
210
- print(f"File does not exist: {target_file_path}")
211
- return
212
- source_text = target_file_path.read_text(encoding="utf-8", errors="ignore")
213
-
214
- raw_prompts = source_text.split(separator)
215
- print(f"Loaded {len(raw_prompts)} raw prompts from source.")
216
- prefix = input("Enter prefix prompt: ")
217
- combined_prompts = _chunk_prompts(raw_prompts, DEFAULT_AGENT_CAP)
218
- combined_prompts = [prefix + "\n" + p for p in combined_prompts]
72
+ raise ValueError(f"Unknown search strategy: {search_strategy}")
73
+ return target_file_path, separator
219
74
 
220
- from machineconfig.utils.options import choose_one_option
221
75
 
222
- 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)
225
- if not tab_config:
226
- return
76
+ def main(): # noqa: C901 - (complexity acceptable for CLI glue)
77
+ repo_root = get_repo_root(Path.cwd())
78
+ if repo_root is None:
79
+ print("💥 Could not determine the repository root. Please run this script from within a git repository.")
80
+ sys.exit(1)
81
+ print(f"Operating @ {repo_root}")
227
82
 
228
- from machineconfig.utils.utils2 import randstr
83
+ search_strategy = cast(SEARCH_STRATEGIES, choose_one_option(header="Choose search strategy:", options=get_args(SEARCH_STRATEGIES)))
84
+ splitting_strategy = cast(SPLITTING_STRATEGY, choose_one_option(header="Choose prompt splitting strategy:", options=get_args(SPLITTING_STRATEGY)))
85
+ agent_selected = cast(AGENTS, choose_one_option(header="Select agent type", options=get_args(AGENTS)))
86
+ print("Enter prefix prompt (end with Ctrl-D / Ctrl-Z):")
87
+ prompt_prefix = "\n".join(sys.stdin.readlines())
88
+ job_name = input("Enter job name [AI_AGENTS]: ") or "AI_Agents"
89
+ keep_material_in_separate_file_input = input("Keep prompt material in separate file? [y/N]: ").strip().lower() == "y"
90
+
91
+ prompt_material_path, separator = get_prompt_material(search_strategy=search_strategy, repo_root=repo_root)
92
+ match splitting_strategy:
93
+ case "agent_cap":
94
+ agent_cap_input = input(f"Enter maximum number of agents/splits [default: {DEFAULT_AGENT_CAP}]: ").strip()
95
+ agent_cap = int(agent_cap_input) if agent_cap_input else DEFAULT_AGENT_CAP
96
+ task_rows = None
97
+ case "task_rows":
98
+ task_rows_input: str = input("Enter number of rows/tasks per agent [13]: ").strip() or "13"
99
+ task_rows = int(task_rows_input)
100
+ agent_cap = None
101
+ prompt_material_re_splitted = chunk_prompts(prompt_material_path, splitting_strategy, agent_cap=agent_cap, task_rows=task_rows, joiner=separator)
102
+
103
+ agents_dir = prep_agent_launch(repo_root=repo_root, prompts_material=prompt_material_re_splitted, keep_material_in_separate_file=keep_material_in_separate_file_input, prompt_prefix=prompt_prefix, agent=agent_selected, job_name=job_name)
104
+ layoutfile = get_agents_launch_layout(session_root=agents_dir)
105
+
106
+ regenerate_py_code = f"""
107
+ #!/usr/bin/env uv run --python 3.13 --with machineconfig
108
+ #!/usr/bin/env uv run --project $HOME/code/machineconfig
109
+
110
+ from machineconfig.scripts.python.fire_agents import *
111
+
112
+ repo_root = Path("{repo_root}")
113
+ search_strategy = "{search_strategy}"
114
+ splitting_strategy = "{splitting_strategy}"
115
+ agent_selected = "{agent_selected}"
116
+ prompt_prefix = '''{prompt_prefix}'''
117
+ job_name = "{job_name}"
118
+ keep_material_in_separate_file_input = {keep_material_in_separate_file_input}
119
+ separator = "{separator}"
120
+ prompt_material_path = Path("{prompt_material_path}")
121
+ agent_cap = {agent_cap}
122
+ task_rows = {task_rows}
123
+
124
+ prompt_material_re_splitted = chunk_prompts(prompt_material_path, splitting_strategy, agent_cap=agent_cap, task_rows=task_rows, joiner=separator)
125
+ agents_dir = prep_agent_launch(repo_root=repo_root, prompts_material=prompt_material_re_splitted, keep_material_in_separate_file=keep_material_in_separate_file_input, prompt_prefix=prompt_prefix, agent=agent_selected, job_name=job_name)
126
+ layout = get_agents_launch_layout(session_root=agents_dir)
127
+
128
+ (agents_dir / "aa_agents_relaunch.py").write_text(data=regenerate_py_code, encoding="utf-8")
129
+ (agents_dir / "layout.json").write_text(data=json.dumps(layout, indent=2), encoding="utf-8")
130
+
131
+ if len(layout["layoutTabs"]) > 25:
132
+ print("Too many agents (>25) to launch. Skipping launch.")
133
+ sys.exit(0)
134
+ manager = ZellijLocalManager(session_layouts=[layout])
135
+ manager.start_all_sessions()
136
+ manager.run_monitoring_routine()
229
137
 
230
- random_name = randstr(length=3)
231
- manager = ZellijLocalManager(session_layouts=[LayoutConfig(layoutName="Agents", layoutTabs=tab_config)], session_name_prefix=random_name)
138
+ """
139
+ (agents_dir / "aa_agents_relaunch.py").write_text(data=regenerate_py_code, encoding="utf-8")
140
+ (agents_dir / "layout.json").write_text(data=json.dumps(layoutfile, indent=2), encoding="utf-8")
141
+ if len(layoutfile["layouts"][0]["layoutTabs"]) > 25:
142
+ print("Too many agents (>25) to launch. Skipping launch.")
143
+ sys.exit(0)
144
+ manager = ZellijLocalManager(session_layouts=layoutfile["layouts"])
232
145
  manager.start_all_sessions()
233
146
  manager.run_monitoring_routine()
234
147
 
@@ -0,0 +1,142 @@
1
+
2
+ from machineconfig.utils.utils2 import randstr
3
+
4
+ import random
5
+ import shlex
6
+ from pathlib import Path
7
+ from typing import Literal, TypeAlias
8
+
9
+
10
+ AGENTS: TypeAlias = Literal[
11
+ "cursor-agent", "gemini", "crush", "q", "onlyPrepPromptFiles"
12
+ # warp terminal
13
+ ]
14
+ AGENT_NAME_FORMATTER = "agent_{idx}_cmd.sh" # e.g., agent_0_cmd.sh
15
+
16
+
17
+ def get_gemini_api_keys() -> list[str]:
18
+ from machineconfig.utils.utils2 import read_ini
19
+ config = read_ini(Path.home().joinpath("dotfiles/creds/llm/gemini/api_keys.ini"))
20
+ res: list[str] = []
21
+ for a_section_name in list(config.sections()):
22
+ a_section = config[a_section_name]
23
+ if "api_key" in a_section:
24
+ api_key = a_section["api_key"].strip()
25
+ if api_key:
26
+ res.append(api_key)
27
+ print(f"Found {len(res)} Gemini API keys configured.")
28
+ return res
29
+
30
+ def prep_agent_launch(repo_root: Path, prompts_material: list[str], prompt_prefix: str, keep_material_in_separate_file: bool, agent: AGENTS, *, job_name: str) -> Path:
31
+ session_root = repo_root / ".ai" / f"tmp_prompts/{job_name}_{randstr()}"
32
+ session_root.mkdir(parents=True, exist_ok=True)
33
+ prompt_folder = session_root / "prompts"
34
+ prompt_folder.mkdir(parents=True, exist_ok=True)
35
+
36
+ all_materials_scripts: list[Path] = []
37
+ for idx, a_prompt_material in enumerate(prompts_material):
38
+ prompt_root = prompt_folder / f"agent_{idx}"
39
+ prompt_root.mkdir(parents=True, exist_ok=True)
40
+ prompt_path = prompt_root / f"agent_{idx}_prompt.txt"
41
+ if keep_material_in_separate_file:
42
+ prompt_material_path = prompt_root / f"agent_{idx}_material.txt"
43
+ prompt_material_path.write_text(a_prompt_material, encoding="utf-8")
44
+ prompt_path.write_text(prompt_prefix + f"""\nPlease only look @ {prompt_material_path}. You don't need to do any other work beside the content of this material file.""", encoding="utf-8")
45
+ all_materials_scripts.append(prompt_material_path)
46
+ else:
47
+ prompt_material_path = prompt_path
48
+ prompt_path.write_text(prompt_prefix + """\nPlease only look @ the following:\n""" + a_prompt_material, encoding="utf-8")
49
+
50
+ agent_cmd_launch_path = prompt_root / AGENT_NAME_FORMATTER.format(idx=idx) # e.g., agent_0_cmd.sh
51
+ random_sleep_time = random.uniform(0, 5)
52
+ cmd_prefix = f"""
53
+ #!/usr/bin/env bash
54
+
55
+ # AGENT-{idx}-LAUNCH-SCRIPT
56
+ # Auto-generated by fire_agents.py
57
+
58
+ export FIRE_AGENTS_AGENT_NAME="{agent}"
59
+ export FIRE_AGENTS_JOB_NAME="{job_name}"
60
+ export FIRE_AGENTS_PROMPT_FILE="{prompt_path}"
61
+ export FIRE_AGENTS_MATERIAL_FILE="{prompt_material_path}"
62
+ export FIRE_AGENTS_AGENT_LAUNCHER="{agent_cmd_launch_path}"
63
+
64
+ echo "Sleeping for {random_sleep_time:.2f} seconds to stagger agent startups..."
65
+ sleep {random_sleep_time:.2f}
66
+ echo "Launching agent {agent} with prompt from {prompt_path}"
67
+ echo "Launching agent {agent} with command from {agent_cmd_launch_path}"
68
+ echo "--------START OF AGENT OUTPUT--------"
69
+ sleep 0.1
70
+
71
+ """
72
+ match agent:
73
+ case "gemini":
74
+ model = "gemini-2.5-pro"
75
+ # model = "gemini-2.5-flash-lite"
76
+ # model = None # auto-select
77
+ if model is None:
78
+ model_arg = ""
79
+ else:
80
+ model_arg = f"--model {shlex.quote(model)}"
81
+ # Need a real shell for the pipeline; otherwise '| gemini ...' is passed as args to 'cat'
82
+ safe_path = shlex.quote(str(prompt_path))
83
+ api_keys = get_gemini_api_keys()
84
+ api_key = api_keys[idx % len(api_keys)] if api_keys else ""
85
+ # Export the environment variable so it's available to subshells
86
+ cmd = f"""
87
+ export GEMINI_API_KEY={shlex.quote(api_key)}
88
+ echo "Using Gemini API key $GEMINI_API_KEY"
89
+ bash -lc 'cat {safe_path} | gemini {model_arg} --yolo --prompt'
90
+ """
91
+ case "cursor-agent":
92
+ # As originally implemented
93
+ cmd = f"""
94
+
95
+ cursor-agent --print --output-format text < {prompt_path}
96
+
97
+ """
98
+ case "crush":
99
+ cmd = f"""
100
+ crush run {prompt_path}
101
+ """
102
+ case "q":
103
+ cmd = f"""
104
+ q chat --no-interactive --trust-all-tools {prompt_path}
105
+ """
106
+ case "onlyPrepPromptFiles":
107
+ cmd = f"""
108
+ echo "Prepared prompt file at {prompt_path}"
109
+ """
110
+ case _:
111
+ raise ValueError(f"Unsupported agent type: {agent}")
112
+ cmd_postfix = """
113
+ sleep 0.1
114
+ echo "---------END OF AGENT OUTPUT---------"
115
+ """
116
+ agent_cmd_launch_path.write_text(cmd_prefix + cmd + cmd_postfix, encoding="utf-8")
117
+
118
+
119
+ # print(f"Launching a template with #{len(tab_config)} agents")
120
+ if len(all_materials_scripts) > 0:
121
+ all_materials_list_path = session_root / "all_materials_redistributed.txt"
122
+ all_materials_list_path.write_text("\n".join(str(p) for p in all_materials_scripts), encoding="utf-8")
123
+ print(f"All prompt materials listed @ {all_materials_list_path}")
124
+ return session_root
125
+
126
+
127
+ def get_agents_launch_layout(session_root: Path):
128
+ from machineconfig.utils.schemas.layouts.layout_types import TabConfig, LayoutConfig, LayoutsFile
129
+ tab_config: list[TabConfig] = []
130
+ prompt_root = session_root / "prompts"
131
+ all_dirs_under_prompts = [d for d in prompt_root.iterdir() if d.is_dir()]
132
+ launch_agents_squentially = ""
133
+ for a_prompt_dir in all_dirs_under_prompts:
134
+ idx = a_prompt_dir.name.split("_")[-1] # e.g., agent_0 -> 0
135
+ agent_cmd_path = a_prompt_dir / AGENT_NAME_FORMATTER.format(idx=idx)
136
+ fire_cmd = f"bash {shlex.quote(str(agent_cmd_path))}"
137
+ tab_config.append(TabConfig(tabName=f"Agent{idx}", startDir=str(session_root.parent.parent.parent), command=fire_cmd))
138
+ launch_agents_squentially += f". {shlex.quote(str(agent_cmd_path))}\n"
139
+ layout = LayoutConfig(layoutName="Agents", layoutTabs=tab_config)
140
+ (session_root / "launch_all_agents_sequentially.sh").write_text(launch_agents_squentially, encoding="utf-8")
141
+ layouts_file: LayoutsFile = LayoutsFile(version="1.0", layouts=[layout])
142
+ return layouts_file
@@ -0,0 +1,82 @@
1
+
2
+ from machineconfig.utils.source_of_truth import EXCLUDE_DIRS
3
+ import fnmatch
4
+ from pathlib import Path
5
+
6
+
7
+ def search_files_by_pattern(repo_root: Path, pattern: str) -> list[Path]:
8
+ """Return all files under repo_root whose filename matches the given pattern.
9
+
10
+ Notes:
11
+ - Uses glob-style pattern matching (e.g., "*.py", "*test*", "config.*")
12
+ - Skips any paths that reside under directories listed in EXCLUDE_DIRS at any depth.
13
+ """
14
+ matches: list[Path] = []
15
+
16
+ def _should_skip_dir(dir_path: Path) -> bool:
17
+ """Check if directory should be skipped based on EXCLUDE_DIRS."""
18
+ return any(part in EXCLUDE_DIRS for part in dir_path.parts)
19
+
20
+ def _walk_and_filter(current_path: Path) -> None:
21
+ """Recursively walk directories, filtering out excluded ones early."""
22
+ try:
23
+ if not current_path.is_dir():
24
+ return
25
+
26
+ # Skip if current directory is in exclude list
27
+ if _should_skip_dir(current_path):
28
+ return
29
+
30
+ for item in current_path.iterdir():
31
+ if item.is_dir():
32
+ _walk_and_filter(item)
33
+ elif item.is_file() and fnmatch.fnmatch(item.name, pattern):
34
+ matches.append(item)
35
+ except (OSError, PermissionError):
36
+ # Skip directories we can't read
37
+ return
38
+
39
+ _walk_and_filter(repo_root)
40
+ return matches
41
+
42
+
43
+ def search_python_files(repo_root: Path, keyword: str) -> list[Path]:
44
+ """Return all Python files under repo_root whose text contains keyword.
45
+
46
+ Notes:
47
+ - Skips any paths that reside under directories listed in EXCLUDE_DIRS at any depth.
48
+ - Errors reading individual files are ignored (decoded with 'ignore').
49
+ """
50
+ keyword_lower = keyword.lower()
51
+ matches: list[Path] = []
52
+
53
+ def _should_skip_dir(dir_path: Path) -> bool:
54
+ """Check if directory should be skipped based on EXCLUDE_DIRS."""
55
+ return any(part in EXCLUDE_DIRS for part in dir_path.parts)
56
+
57
+ def _walk_and_filter(current_path: Path) -> None:
58
+ """Recursively walk directories, filtering out excluded ones early."""
59
+ try:
60
+ if not current_path.is_dir():
61
+ return
62
+
63
+ # Skip if current directory is in exclude list
64
+ if _should_skip_dir(current_path):
65
+ return
66
+
67
+ for item in current_path.iterdir():
68
+ if item.is_dir():
69
+ _walk_and_filter(item)
70
+ elif item.is_file() and item.suffix == ".py":
71
+ try:
72
+ if keyword_lower in item.read_text(encoding="utf-8", errors="ignore").lower():
73
+ matches.append(item)
74
+ except OSError:
75
+ # Skip unreadable file
76
+ continue
77
+ except (OSError, PermissionError):
78
+ # Skip directories we can't read
79
+ return
80
+
81
+ _walk_and_filter(repo_root)
82
+ return matches
@@ -0,0 +1,52 @@
1
+
2
+
3
+ from typing import Literal, TypeAlias
4
+ from math import ceil
5
+ from pathlib import Path
6
+
7
+
8
+ SPLITTING_STRATEGY: TypeAlias = Literal[
9
+ "agent_cap", # User decides number of agents, rows/tasks determined automatically
10
+ "task_rows" # User decides number of rows/tasks, number of agents determined automatically
11
+ ]
12
+ DEFAULT_AGENT_CAP = 6
13
+
14
+
15
+ def chunk_prompts(prompt_material_path: Path, strategy: SPLITTING_STRATEGY, joiner: str, *, agent_cap: int | None, task_rows: int | None) -> list[str]:
16
+ """Chunk prompts based on splitting strategy.
17
+
18
+ Args:
19
+ prompts: List of prompts to chunk
20
+ strategy: Either 'agent_cap' or 'task_rows'
21
+ agent_cap: Maximum number of agents (used with 'agent_cap' strategy)
22
+ task_rows: Number of rows/tasks per agent (used with 'task_rows' strategy)
23
+ """
24
+ prompts = [p for p in prompt_material_path.read_text(encoding="utf-8", errors="ignore").split(joiner) if p.strip() != ""] # drop blank entries
25
+
26
+ if strategy == "agent_cap":
27
+ if agent_cap is None:
28
+ raise ValueError("agent_cap must be provided when using 'agent_cap' strategy")
29
+
30
+ if len(prompts) <= agent_cap:
31
+ return prompts
32
+
33
+ print(f"Chunking {len(prompts)} prompts into groups for up to {agent_cap} agents because it exceeds the cap.")
34
+ chunk_size = ceil(len(prompts) / agent_cap)
35
+ grouped: list[str] = []
36
+ for i in range(0, len(prompts), chunk_size):
37
+ grouped.append(joiner.join(prompts[i : i + chunk_size]))
38
+ return grouped
39
+
40
+ elif strategy == "task_rows":
41
+ if task_rows is None:
42
+ raise ValueError("task_rows must be provided when using 'task_rows' strategy")
43
+ if task_rows >= len(prompts):
44
+ return prompts
45
+ print(f"Chunking {len(prompts)} prompts into groups of {task_rows} rows/tasks each.")
46
+ grouped: list[str] = []
47
+ for i in range(0, len(prompts), task_rows):
48
+ grouped.append(joiner.join(prompts[i : i + task_rows]))
49
+ return grouped
50
+
51
+ else:
52
+ raise ValueError(f"Unknown splitting strategy: {strategy}")
@@ -21,6 +21,7 @@ from machineconfig.utils.utils2 import randstr, read_toml
21
21
  from machineconfig.scripts.python.fire_jobs_args_helper import get_args, FireJobArgs, extract_kwargs
22
22
  import platform
23
23
  from typing import Optional
24
+ from pathlib import Path
24
25
  # import os
25
26
 
26
27
 
@@ -42,7 +43,7 @@ def route(args: FireJobArgs) -> None:
42
43
  choice_file = PathExtended(choice_file)
43
44
  else:
44
45
  choice_file = path_obj
45
- repo_root = get_repo_root(str(choice_file))
46
+ repo_root = get_repo_root(Path(choice_file))
46
47
  print(f"💾 Selected file: {choice_file}.\nRepo root: {repo_root}")
47
48
  ve_root_from_file, ipy_profile = get_ve_path_and_ipython_profile(choice_file)
48
49
  if ipy_profile is None:
@@ -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.devops_update_repos import update_repository
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