llm-ide-rules 0.7.0__py3-none-any.whl → 0.8.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.
@@ -7,7 +7,58 @@ import typer
7
7
  from typing_extensions import Annotated
8
8
 
9
9
  from llm_ide_rules.commands.download import INSTRUCTION_TYPES, DEFAULT_TYPES
10
+ from llm_ide_rules.constants import header_to_filename
10
11
  from llm_ide_rules.log import log
12
+ from llm_ide_rules.markdown_parser import parse_sections
13
+
14
+
15
+ def get_generated_files(target_dir: Path) -> set[Path]:
16
+ """Identify files that would be generated from local instruction files."""
17
+ generated = set()
18
+
19
+ # Check instructions.md
20
+ instructions_path = target_dir / "instructions.md"
21
+ if instructions_path.exists():
22
+ try:
23
+ general, sections = parse_sections(instructions_path.read_text())
24
+
25
+ # If general instructions exist, these files are generated
26
+ if any(line.strip() for line in general):
27
+ generated.add(target_dir / ".cursor/rules/general.mdc")
28
+ generated.add(target_dir / ".github/copilot-instructions.md")
29
+ generated.add(target_dir / "CLAUDE.md")
30
+
31
+ # If any sections exist, root docs are definitely generated
32
+ if sections:
33
+ generated.add(target_dir / "CLAUDE.md")
34
+
35
+ # Section specific files
36
+ for header in sections:
37
+ filename = header_to_filename(header)
38
+ generated.add(target_dir / f".cursor/rules/{filename}.mdc")
39
+ generated.add(
40
+ target_dir / f".github/instructions/{filename}.instructions.md"
41
+ )
42
+
43
+ except Exception as e:
44
+ log.warning("failed to parse instructions.md", error=str(e))
45
+
46
+ # Check commands.md
47
+ commands_path = target_dir / "commands.md"
48
+ if commands_path.exists():
49
+ try:
50
+ _, sections = parse_sections(commands_path.read_text())
51
+ for header in sections:
52
+ filename = header_to_filename(header)
53
+ generated.add(target_dir / f".cursor/commands/{filename}.md")
54
+ generated.add(target_dir / f".github/prompts/{filename}.prompt.md")
55
+ generated.add(target_dir / f".gemini/commands/{filename}.toml")
56
+ generated.add(target_dir / f".claude/commands/{filename}.md")
57
+ generated.add(target_dir / f".opencode/commands/{filename}.md")
58
+ except Exception as e:
59
+ log.warning("failed to parse commands.md", error=str(e))
60
+
61
+ return {p.resolve() for p in generated}
11
62
 
12
63
 
13
64
  def find_files_to_delete(
@@ -38,6 +89,11 @@ def find_files_to_delete(
38
89
  if file_path.exists() and file_path.is_file():
39
90
  files_to_delete.append(file_path)
40
91
 
92
+ for file_name in config.get("generated_files", []):
93
+ file_path = target_dir / file_name
94
+ if file_path.exists() and file_path.is_file():
95
+ files_to_delete.append(file_path)
96
+
41
97
  for file_pattern in config.get("recursive_files", []):
42
98
  matching_files = list(target_dir.rglob(file_pattern))
43
99
  files_to_delete.extend([f for f in matching_files if f.is_file()])
@@ -49,12 +105,19 @@ def delete_main(
49
105
  instruction_types: Annotated[
50
106
  list[str] | None,
51
107
  typer.Argument(
52
- help="Types of instructions to delete (cursor, github, gemini, claude, agent, agents). Deletes everything by default."
108
+ help="Types of instructions to delete (cursor, github, gemini, claude, opencode, agents). Deletes everything by default."
53
109
  ),
54
110
  ] = None,
55
111
  target_dir: Annotated[
56
112
  str, typer.Option("--target", "-t", help="Target directory to delete from")
57
113
  ] = ".",
114
+ everything: Annotated[
115
+ bool,
116
+ typer.Option(
117
+ "--everything",
118
+ help="Delete all instruction files, not just those generated from local sources.",
119
+ ),
120
+ ] = False,
58
121
  yes: Annotated[
59
122
  bool,
60
123
  typer.Option(
@@ -64,17 +127,25 @@ def delete_main(
64
127
  ):
65
128
  """Remove downloaded LLM instruction files.
66
129
 
67
- This command removes files and directories that were downloaded by the 'download' command.
68
- It will show you what will be deleted and ask for confirmation before proceeding.
130
+ This command removes files and directories that were downloaded by the 'download' command
131
+ or generated by the 'explode' command.
132
+
133
+ By default, it ONLY deletes files that correspond to your local 'instructions.md' and
134
+ 'commands.md' files. This prevents accidental deletion of manually created files.
135
+ Use --everything to delete all standard instruction files and directories.
69
136
 
70
137
  Examples:
71
138
 
72
139
  \b
73
- # Delete everything (with confirmation)
140
+ # Delete only generated files (safest, default)
74
141
  llm_ide_rules delete
75
142
 
76
143
  \b
77
- # Delete only Cursor and Gemini files
144
+ # Delete ALL instruction files (including manual ones)
145
+ llm_ide_rules delete --everything
146
+
147
+ \b
148
+ # Delete only Cursor and Gemini files (but only if generated)
78
149
  llm_ide_rules delete cursor gemini
79
150
 
80
151
  \b
@@ -115,9 +186,39 @@ def delete_main(
115
186
  instruction_types, target_path
116
187
  )
117
188
 
189
+ skipped_files = []
190
+
191
+ if not everything:
192
+ log.info("filtering files to delete based on local sources")
193
+ generated_files = get_generated_files(target_path)
194
+
195
+ # Expand directories to files for granular filtering
196
+ expanded_files = []
197
+ for d in dirs_to_delete:
198
+ expanded_files.extend([f for f in d.rglob("*") if f.is_file()])
199
+
200
+ all_candidates = files_to_delete + expanded_files
201
+
202
+ # Filter: keep only files that are in the generated set
203
+ # We compare resolved paths to be safe
204
+ files_to_delete = [f for f in all_candidates if f.resolve() in generated_files]
205
+
206
+ # Identify skipped files (candidates that were NOT in generated set)
207
+ skipped_files = [
208
+ f for f in all_candidates if f.resolve() not in generated_files
209
+ ]
210
+
211
+ # We are no longer deleting whole directories in safe mode
212
+ dirs_to_delete = []
213
+
118
214
  if not dirs_to_delete and not files_to_delete:
119
215
  log.info("no files found to delete")
120
216
  typer.echo("No matching instruction files found to delete.")
217
+ if skipped_files:
218
+ typer.echo(
219
+ f"\n{len(skipped_files)} files were skipped because they don't match local instructions/commands."
220
+ )
221
+ typer.echo("Use --everything to delete them.")
121
222
  return
122
223
 
123
224
  typer.echo("\nThe following files and directories will be deleted:\n")
@@ -137,6 +238,11 @@ def delete_main(
137
238
  total_items = len(dirs_to_delete) + len(files_to_delete)
138
239
  typer.echo(f"\nTotal: {total_items} items")
139
240
 
241
+ if skipped_files:
242
+ typer.echo(
243
+ f"\n(Note: {len(skipped_files)} other files will be preserved. Use --everything to delete them)"
244
+ )
245
+
140
246
  if not yes:
141
247
  typer.echo()
142
248
  confirm = typer.confirm("Are you sure you want to delete these files?")
@@ -46,7 +46,7 @@ def normalize_repo(repo: str) -> str:
46
46
  # The directories listed here are what gets created by explode and what delete removes.
47
47
  INSTRUCTION_TYPES = {
48
48
  "cursor": {
49
- "directories": [".cursor"],
49
+ "directories": [".cursor/rules", ".cursor/commands"],
50
50
  "files": [],
51
51
  "include_patterns": [],
52
52
  },
@@ -56,22 +56,23 @@ INSTRUCTION_TYPES = {
56
56
  "include_patterns": [],
57
57
  },
58
58
  "gemini": {
59
- "directories": [".gemini"],
60
- "files": ["GEMINI.md"],
59
+ "directories": [".gemini/commands"],
60
+ "files": [],
61
+ "generated_files": [],
61
62
  "include_patterns": [],
62
63
  },
63
64
  "claude": {
64
- "directories": [".claude"],
65
- "files": ["CLAUDE.md"],
65
+ "directories": [".claude/commands"],
66
+ "files": [],
67
+ "generated_files": ["CLAUDE.md"],
66
68
  "include_patterns": [],
67
69
  },
68
70
  "opencode": {
69
- "directories": [".opencode"],
71
+ "directories": [".opencode/commands"],
70
72
  "files": [],
71
73
  "include_patterns": [],
72
74
  },
73
- "agent": {"directories": [], "files": ["AGENT.md"]},
74
- "agents": {"directories": [], "files": [], "recursive_files": ["AGENTS.md"]},
75
+ "agents": {"directories": [], "files": [], "generated_files": ["AGENTS.md"]},
75
76
  }
76
77
 
77
78
  # Default types to download when no specific types are specified
@@ -288,7 +289,7 @@ def download_main(
288
289
  instruction_types: Annotated[
289
290
  list[str] | None,
290
291
  typer.Argument(
291
- help="Types of instructions to download (cursor, github, gemini, claude, agent, agents). Downloads everything by default."
292
+ help="Types of instructions to download (cursor, github, gemini, claude, opencode, agents). Downloads everything by default."
292
293
  ),
293
294
  ] = None,
294
295
  repo: Annotated[
@@ -355,7 +356,10 @@ def download_main(
355
356
 
356
357
  try:
357
358
  # Copy instruction files
358
- copied_items = copy_instruction_files(repo_dir, instruction_types, target_path)
359
+ copied_items = [
360
+ f"Downloaded: {item}"
361
+ for item in copy_instruction_files(repo_dir, instruction_types, target_path)
362
+ ]
359
363
 
360
364
  # Check for source files (instructions.md, commands.md) and copy them if available
361
365
  # These are needed for 'explode' logic
@@ -373,7 +377,7 @@ def download_main(
373
377
  log.info("copying source file", source=str(src), target=str(dst))
374
378
  dst.parent.mkdir(parents=True, exist_ok=True)
375
379
  dst.write_bytes(src.read_bytes())
376
- copied_items.append(source_file)
380
+ copied_items.append(f"Downloaded: {source_file}")
377
381
  sources_copied = True
378
382
 
379
383
  # Generate rule files locally for supported agents
@@ -395,7 +399,7 @@ def download_main(
395
399
  agent=agent,
396
400
  working_dir=target_path,
397
401
  )
398
- copied_items.append(f"(generated) {agent} rules")
402
+ copied_items.append(f"Generated: {agent} rules")
399
403
  except Exception as e:
400
404
  log.error("failed to generate rules", agent=agent, error=str(e))
401
405
  typer.echo(
@@ -408,8 +412,23 @@ def download_main(
408
412
  for item in copied_items:
409
413
  typer.echo(f" - {item}")
410
414
  else:
411
- log.warning("no files were copied or generated")
412
- typer.echo("No matching instruction files found in the repository.")
415
+ log.info("no files were copied or generated")
416
+
417
+ # Build list of expected files
418
+ expected_files = []
419
+ for inst_type in instruction_types:
420
+ config = INSTRUCTION_TYPES[inst_type]
421
+ expected_files.extend(config.get("directories", []))
422
+ expected_files.extend(config.get("files", []))
423
+ expected_files.extend(config.get("recursive_files", []))
424
+
425
+ error_msg = "No matching instruction files found in the repository."
426
+ typer.echo(typer.style(error_msg, fg=typer.colors.RED), err=True)
427
+
428
+ if expected_files:
429
+ typer.echo("\nExpected files/directories:", err=True)
430
+ for expected in expected_files:
431
+ typer.echo(f" - {expected}", err=True)
413
432
 
414
433
  finally:
415
434
  # Clean up temporary directory
@@ -54,9 +54,19 @@ def process_unmapped_as_always_apply(
54
54
  section_content = replace_header_with_proper_casing(section_content, section_name)
55
55
 
56
56
  cursor_agent.write_rule(
57
- section_content, filename, cursor_rules_dir, glob_pattern=None
57
+ section_content,
58
+ filename,
59
+ cursor_rules_dir,
60
+ glob_pattern=None,
61
+ description=section_name,
62
+ )
63
+ github_agent.write_rule(
64
+ section_content,
65
+ filename,
66
+ copilot_dir,
67
+ glob_pattern=None,
68
+ description=section_name,
58
69
  )
59
- github_agent.write_rule(section_content, filename, copilot_dir, glob_pattern=None)
60
70
 
61
71
  return True
62
72
 
@@ -88,7 +98,14 @@ def explode_implementation(
88
98
  # Initialize only the agents we need
89
99
  agents_to_process = []
90
100
  if agent == "all":
91
- agents_to_process = ["cursor", "github", "claude", "gemini", "opencode"]
101
+ agents_to_process = [
102
+ "cursor",
103
+ "github",
104
+ "claude",
105
+ "gemini",
106
+ "opencode",
107
+ "agents",
108
+ ]
92
109
  else:
93
110
  agents_to_process = [agent]
94
111
 
@@ -103,14 +120,14 @@ def explode_implementation(
103
120
  # These agents have both rules and commands
104
121
  rules_dir = working_dir / agent_instances[agent_name].rules_dir
105
122
  commands_dir = working_dir / agent_instances[agent_name].commands_dir
106
- rules_dir.mkdir(parents=True, exist_ok=True)
107
- commands_dir.mkdir(parents=True, exist_ok=True)
108
123
  agent_dirs[agent_name] = {"rules": rules_dir, "commands": commands_dir}
109
- else:
124
+ elif agent_instances[agent_name].commands_dir:
110
125
  # claude, gemini, and opencode only have commands
111
126
  commands_dir = working_dir / agent_instances[agent_name].commands_dir
112
- commands_dir.mkdir(parents=True, exist_ok=True)
113
127
  agent_dirs[agent_name] = {"commands": commands_dir}
128
+ else:
129
+ # agents has neither rules nor commands dirs (only generates root doc)
130
+ agent_dirs[agent_name] = {}
114
131
 
115
132
  input_path = working_dir / input_file
116
133
 
@@ -135,7 +152,8 @@ def explode_implementation(
135
152
  if any(line.strip() for line in general):
136
153
  general_header = """
137
154
  ---
138
- description:
155
+ description: General Instructions
156
+ globs:
139
157
  alwaysApply: true
140
158
  ---
141
159
  """
@@ -180,6 +198,7 @@ alwaysApply: true
180
198
  filename,
181
199
  agent_dirs["cursor"]["rules"],
182
200
  glob_pattern=None,
201
+ description=section_name,
183
202
  )
184
203
  elif "github" in agent_instances:
185
204
  agent_instances["github"].write_rule(
@@ -187,6 +206,7 @@ alwaysApply: true
187
206
  filename,
188
207
  agent_dirs["github"]["rules"],
189
208
  glob_pattern=None,
209
+ description=section_name,
190
210
  )
191
211
  elif glob_pattern != "manual":
192
212
  # Has glob pattern = file-specific rule
@@ -196,6 +216,7 @@ alwaysApply: true
196
216
  filename,
197
217
  agent_dirs["cursor"]["rules"],
198
218
  glob_pattern,
219
+ description=section_name,
199
220
  )
200
221
  if "github" in agent_instances:
201
222
  agent_instances["github"].write_rule(
@@ -203,6 +224,7 @@ alwaysApply: true
203
224
  filename,
204
225
  agent_dirs["github"]["rules"],
205
226
  glob_pattern,
227
+ description=section_name,
206
228
  )
207
229
 
208
230
  # Process commands for all agents
@@ -210,15 +232,21 @@ alwaysApply: true
210
232
  command_sections = {}
211
233
  if commands_text:
212
234
  _, command_sections_data = parse_sections(commands_text)
213
- agents = [agent_instances[name] for name in agents_to_process]
235
+ agents_with_commands = [
236
+ agent_instances[name]
237
+ for name in agents_to_process
238
+ if agent_instances[name].commands_dir
239
+ ]
214
240
  command_dirs = {
215
- name: agent_dirs[name]["commands"] for name in agents_to_process
241
+ name: agent_dirs[name]["commands"]
242
+ for name in agents_to_process
243
+ if "commands" in agent_dirs[name]
216
244
  }
217
245
 
218
246
  for section_name, section_data in command_sections_data.items():
219
247
  command_sections[section_name] = section_data.content
220
248
  process_command_section(
221
- section_name, section_data.content, agents, command_dirs
249
+ section_name, section_data.content, agents_with_commands, command_dirs
222
250
  )
223
251
 
224
252
  # Generate root documentation (CLAUDE.md, GEMINI.md, etc.)
@@ -228,6 +256,7 @@ alwaysApply: true
228
256
  rules_sections,
229
257
  command_sections,
230
258
  working_dir,
259
+ section_globs=section_globs,
231
260
  )
232
261
 
233
262
  # Build log message and user output based on processed agents
@@ -239,15 +268,37 @@ alwaysApply: true
239
268
  log_data[f"{agent_name}_rules"] = str(agent_dirs[agent_name]["rules"])
240
269
  log_data[f"{agent_name}_commands"] = str(agent_dirs[agent_name]["commands"])
241
270
  created_dirs.append(f".{agent_name}/")
242
- else:
271
+ elif agent_dirs[agent_name]:
272
+ # Has commands directory
243
273
  log_data[f"{agent_name}_commands"] = str(agent_dirs[agent_name]["commands"])
244
274
  created_dirs.append(f".{agent_name}/")
275
+ # else: agent has no directories (e.g., agents which only generates root doc)
245
276
 
246
- if len(created_dirs) == 1:
247
- success_msg = f"Created files in {created_dirs[0]} directory"
248
- typer.echo(typer.style(success_msg, fg=typer.colors.GREEN))
277
+ if "gemini" in agent_instances:
278
+ if not agent_instances["gemini"].check_agents_md_config(working_dir):
279
+ typer.secho(
280
+ "Warning: Gemini CLI configuration missing for AGENTS.md.",
281
+ fg=typer.colors.YELLOW,
282
+ )
283
+ typer.secho(
284
+ "Run this command to configure it:",
285
+ fg=typer.colors.YELLOW,
286
+ )
287
+ typer.secho(
288
+ " gemini config set agent.instructionFile AGENTS.md",
289
+ fg=typer.colors.YELLOW,
290
+ )
291
+
292
+ if created_dirs:
293
+ if len(created_dirs) == 1:
294
+ success_msg = f"Created files in {created_dirs[0]} directory"
295
+ typer.echo(typer.style(success_msg, fg=typer.colors.GREEN))
296
+ else:
297
+ success_msg = f"Created files in {', '.join(created_dirs)} directories"
298
+ typer.echo(typer.style(success_msg, fg=typer.colors.GREEN))
249
299
  else:
250
- success_msg = f"Created files in {', '.join(created_dirs)} directories"
300
+ # No directories created (e.g., agents that only generate root docs)
301
+ success_msg = "Created root documentation files"
251
302
  typer.echo(typer.style(success_msg, fg=typer.colors.GREEN))
252
303
 
253
304
 
@@ -1,12 +1,12 @@
1
1
  """Implode command: Bundle rule files into a single instruction file."""
2
2
 
3
- from pathlib import Path
4
3
  from typing_extensions import Annotated
5
4
 
6
5
  import typer
7
6
 
8
7
  from llm_ide_rules.agents import get_agent
9
8
  from llm_ide_rules.log import log
9
+ from llm_ide_rules.utils import find_project_root
10
10
 
11
11
 
12
12
  def cursor(
@@ -17,7 +17,7 @@ def cursor(
17
17
  """Bundle Cursor rules into instructions.md and commands into commands.md."""
18
18
 
19
19
  agent = get_agent("cursor")
20
- cwd = Path.cwd()
20
+ base_dir = find_project_root()
21
21
 
22
22
  rules_dir = agent.rules_dir
23
23
  if not rules_dir:
@@ -30,14 +30,14 @@ def cursor(
30
30
  commands_dir=agent.commands_dir,
31
31
  )
32
32
 
33
- rules_path = cwd / rules_dir
33
+ rules_path = base_dir / rules_dir
34
34
  if not rules_path.exists():
35
35
  log.error("cursor rules directory not found", rules_dir=str(rules_path))
36
36
  error_msg = f"Cursor rules directory not found: {rules_path}"
37
37
  typer.echo(typer.style(error_msg, fg=typer.colors.RED), err=True)
38
38
  raise typer.Exit(1)
39
39
 
40
- output_path = cwd / output
40
+ output_path = base_dir / output
41
41
  rules_written = agent.bundle_rules(output_path)
42
42
  if rules_written:
43
43
  success_msg = f"Bundled cursor rules into {output}"
@@ -46,7 +46,7 @@ def cursor(
46
46
  output_path.unlink(missing_ok=True)
47
47
  log.info("no cursor rules to bundle")
48
48
 
49
- commands_output_path = cwd / "commands.md"
49
+ commands_output_path = base_dir / "commands.md"
50
50
  commands_written = agent.bundle_commands(commands_output_path)
51
51
  if commands_written:
52
52
  success_msg = "Bundled cursor commands into commands.md"
@@ -63,7 +63,7 @@ def github(
63
63
  """Bundle GitHub instructions into instructions.md and prompts into commands.md."""
64
64
 
65
65
  agent = get_agent("github")
66
- cwd = Path.cwd()
66
+ base_dir = find_project_root()
67
67
 
68
68
  rules_dir = agent.rules_dir
69
69
  if not rules_dir:
@@ -76,7 +76,7 @@ def github(
76
76
  prompts_dir=agent.commands_dir,
77
77
  )
78
78
 
79
- rules_path = cwd / rules_dir
79
+ rules_path = base_dir / rules_dir
80
80
  if not rules_path.exists():
81
81
  log.error(
82
82
  "github instructions directory not found", instructions_dir=str(rules_path)
@@ -85,7 +85,7 @@ def github(
85
85
  typer.echo(typer.style(error_msg, fg=typer.colors.RED), err=True)
86
86
  raise typer.Exit(1)
87
87
 
88
- output_path = cwd / output
88
+ output_path = base_dir / output
89
89
  instructions_written = agent.bundle_rules(output_path)
90
90
  if instructions_written:
91
91
  success_msg = f"Bundled github instructions into {output}"
@@ -94,7 +94,7 @@ def github(
94
94
  output_path.unlink(missing_ok=True)
95
95
  log.info("no github instructions to bundle")
96
96
 
97
- commands_output_path = cwd / "commands.md"
97
+ commands_output_path = base_dir / "commands.md"
98
98
  prompts_written = agent.bundle_commands(commands_output_path)
99
99
  if prompts_written:
100
100
  success_msg = "Bundled github prompts into commands.md"
@@ -109,7 +109,7 @@ def claude(
109
109
  """Bundle Claude Code commands into commands.md."""
110
110
 
111
111
  agent = get_agent("claude")
112
- cwd = Path.cwd()
112
+ base_dir = find_project_root()
113
113
 
114
114
  commands_dir = agent.commands_dir
115
115
  if not commands_dir:
@@ -121,7 +121,7 @@ def claude(
121
121
  commands_dir=commands_dir,
122
122
  )
123
123
 
124
- commands_path = cwd / commands_dir
124
+ commands_path = base_dir / commands_dir
125
125
  if not commands_path.exists():
126
126
  log.error(
127
127
  "claude code commands directory not found", commands_dir=str(commands_path)
@@ -130,7 +130,7 @@ def claude(
130
130
  typer.echo(typer.style(error_msg, fg=typer.colors.RED), err=True)
131
131
  raise typer.Exit(1)
132
132
 
133
- output_path = cwd / output
133
+ output_path = base_dir / output
134
134
  commands_written = agent.bundle_commands(output_path)
135
135
  if commands_written:
136
136
  success_msg = f"Bundled claude commands into {output}"
@@ -146,7 +146,7 @@ def gemini(
146
146
  """Bundle Gemini CLI commands into commands.md."""
147
147
 
148
148
  agent = get_agent("gemini")
149
- cwd = Path.cwd()
149
+ base_dir = find_project_root()
150
150
 
151
151
  commands_dir = agent.commands_dir
152
152
  if not commands_dir:
@@ -158,7 +158,7 @@ def gemini(
158
158
  commands_dir=commands_dir,
159
159
  )
160
160
 
161
- commands_path = cwd / commands_dir
161
+ commands_path = base_dir / commands_dir
162
162
  if not commands_path.exists():
163
163
  log.error(
164
164
  "gemini cli commands directory not found", commands_dir=str(commands_path)
@@ -167,7 +167,7 @@ def gemini(
167
167
  typer.echo(typer.style(error_msg, fg=typer.colors.RED), err=True)
168
168
  raise typer.Exit(1)
169
169
 
170
- output_path = cwd / output
170
+ output_path = base_dir / output
171
171
  commands_written = agent.bundle_commands(output_path)
172
172
  if commands_written:
173
173
  success_msg = f"Bundled gemini commands into {output}"
@@ -183,7 +183,7 @@ def opencode(
183
183
  """Bundle OpenCode commands into commands.md."""
184
184
 
185
185
  agent = get_agent("opencode")
186
- cwd = Path.cwd()
186
+ base_dir = find_project_root()
187
187
 
188
188
  commands_dir = agent.commands_dir
189
189
  if not commands_dir:
@@ -195,7 +195,7 @@ def opencode(
195
195
  commands_dir=commands_dir,
196
196
  )
197
197
 
198
- commands_path = cwd / commands_dir
198
+ commands_path = base_dir / commands_dir
199
199
  if not commands_path.exists():
200
200
  log.error(
201
201
  "opencode commands directory not found", commands_dir=str(commands_path)
@@ -204,7 +204,7 @@ def opencode(
204
204
  typer.echo(typer.style(error_msg, fg=typer.colors.RED), err=True)
205
205
  raise typer.Exit(1)
206
206
 
207
- output_path = cwd / output
207
+ output_path = base_dir / output
208
208
  commands_written = agent.bundle_commands(output_path)
209
209
  if commands_written:
210
210
  success_msg = f"Bundled opencode commands into {output}"
@@ -23,7 +23,7 @@ def explode(
23
23
  "all",
24
24
  "--agent",
25
25
  "-a",
26
- help="Agent: claude, cursor, gemini, opencode, copilot, or all",
26
+ help="Agent: claude, cursor, gemini, opencode, copilot, vscode, or all",
27
27
  ),
28
28
  ) -> None:
29
29
  """Convert unified mcp.json to platform-specific configs."""
@@ -1,6 +1,6 @@
1
1
  """Shared constants for explode and implode functionality."""
2
2
 
3
- VALID_AGENTS = ["cursor", "github", "claude", "gemini", "opencode", "all"]
3
+ VALID_AGENTS = ["cursor", "github", "claude", "gemini", "opencode", "agents", "all"]
4
4
 
5
5
 
6
6
  def header_to_filename(header: str) -> str: