@softspark/ai-toolkit 2.5.0 → 2.6.2

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.
@@ -92,6 +92,14 @@ def install_ai_tools(target_dir: Path, rules_dir: Path,
92
92
  _install_codex_global(target_dir, rules_dir)
93
93
  installed.append("codex")
94
94
 
95
+ if "opencode" in eds:
96
+ if dry_run:
97
+ print(" Would inject: ~/.config/opencode/{AGENTS.md, agents/, "
98
+ "commands/, plugins/ai-toolkit-hooks.js, opencode.json}")
99
+ else:
100
+ _install_opencode_global(target_dir, rules_dir)
101
+ installed.append("opencode")
102
+
95
103
  print()
96
104
  print(f" Available: {', '.join(GLOBAL_CAPABLE_EDITORS)}")
97
105
  print(" Note: Copilot, Cline, Roo Code, Aider, Antigravity have no global config -- use 'ai-toolkit install --local' per project")
@@ -128,6 +136,51 @@ def _install_codex_global(target_dir: Path, rules_dir: Path) -> None:
128
136
  _install_codex_skills(target_dir)
129
137
 
130
138
 
139
+ def _install_opencode_global(target_dir: Path, rules_dir: Path) -> None:
140
+ """Install opencode at the global level (~/.config/opencode/).
141
+
142
+ Creates:
143
+ - ~/.config/opencode/AGENTS.md (marker injection with rules)
144
+ - ~/.config/opencode/agents/ai-toolkit-*.md (subagents)
145
+ - ~/.config/opencode/commands/ai-toolkit-*.md (slash commands)
146
+ - ~/.config/opencode/plugins/ai-toolkit-hooks.js (hook bridge)
147
+ - ~/.config/opencode/opencode.json (MCP merge, preserves user keys)
148
+ """
149
+ opencode_home = target_dir / ".config" / "opencode"
150
+ inject_with_rules(
151
+ "generate_opencode.py",
152
+ opencode_home / "AGENTS.md",
153
+ rules_dir,
154
+ )
155
+
156
+ from generate_opencode_agents import generate as gen_opencode_agents
157
+ written, removed = gen_opencode_agents(target_dir, config_root=opencode_home)
158
+ msg = f" Created: ~/.config/opencode/agents/ ({written} agents"
159
+ if removed:
160
+ msg += f", {removed} stale removed"
161
+ msg += ")"
162
+ print(msg)
163
+
164
+ from generate_opencode_commands import generate as gen_opencode_commands
165
+ written, removed = gen_opencode_commands(target_dir, config_root=opencode_home)
166
+ msg = f" Created: ~/.config/opencode/commands/ ({written} commands"
167
+ if removed:
168
+ msg += f", {removed} stale removed"
169
+ msg += ")"
170
+ print(msg)
171
+
172
+ from generate_opencode_plugin import generate as gen_opencode_plugin
173
+ gen_opencode_plugin(target_dir, config_root=opencode_home)
174
+ print(" Created: ~/.config/opencode/plugins/ai-toolkit-hooks.js")
175
+
176
+ from generate_opencode_json import merge_into_opencode_json
177
+ _, count = merge_into_opencode_json(
178
+ target_dir, output_path=opencode_home / "opencode.json"
179
+ )
180
+ suffix = f" ({count} MCP server(s) merged)" if count else " (no MCP servers)"
181
+ print(f" Created: ~/.config/opencode/opencode.json{suffix}")
182
+
183
+
131
184
  def inject_with_rules(
132
185
  generator_script: str,
133
186
  target_file: Path,
@@ -194,7 +247,7 @@ def run_script(script_name: str, *args: str, capture: bool = False) -> str:
194
247
  # All known editor identifiers for --editors flag
195
248
  ALL_EDITORS = [
196
249
  "copilot", "cursor", "windsurf", "cline", "roo",
197
- "aider", "augment", "antigravity", "codex", "gemini",
250
+ "aider", "augment", "antigravity", "codex", "gemini", "opencode",
198
251
  ]
199
252
 
200
253
  # Map of project files/dirs → editor names for auto-detection
@@ -213,7 +266,15 @@ _EDITOR_MARKERS: dict[str, str] = {
213
266
  ".agent/rules": "antigravity",
214
267
  ".agents/skills": "codex",
215
268
  ".codex": "codex",
269
+ # NOTE: AGENTS.md alone is ambiguous (Codex + opencode both read it);
270
+ # prefer the dedicated .opencode/ and opencode.json markers when
271
+ # disambiguating. If only AGENTS.md is present, Codex takes precedence
272
+ # to preserve v2.4.x behavior.
216
273
  "AGENTS.md": "codex",
274
+ "opencode.json": "opencode",
275
+ ".opencode": "opencode",
276
+ ".opencode/agents": "opencode",
277
+ ".opencode/commands": "opencode",
217
278
  }
218
279
 
219
280
 
@@ -485,6 +546,7 @@ def _install_local_dry_run(reset: bool, editors: list[str] | None = None) -> Non
485
546
  "aider": " Would generate: .aider.conf.yml + CONVENTIONS.md",
486
547
  "augment": " Would generate: .augment/rules/ai-toolkit-*.md",
487
548
  "antigravity": " Would generate: .agent/rules/ + .agent/workflows/",
549
+ "opencode": " Would generate: AGENTS.md + .opencode/{agents,commands,plugins}/ + opencode.json",
488
550
  }
489
551
  for ed, msg in _EDITOR_DRY_RUN.items():
490
552
  if ed in eds:
@@ -511,6 +573,24 @@ def _reset_local_configs(cwd: Path) -> None:
511
573
  p.unlink()
512
574
  print(f" Removed: {rel}")
513
575
 
576
+ # opencode: remove only ai-toolkit-prefixed generated files so the
577
+ # user's own .opencode/agents/ or commands/ entries are preserved.
578
+ # opencode.json is left alone — it may contain user MCP servers and other
579
+ # project settings; reinstall re-merges our MCP entries idempotently.
580
+ opencode_plugin = cwd / ".opencode" / "plugins" / "ai-toolkit-hooks.js"
581
+ if opencode_plugin.is_file():
582
+ opencode_plugin.unlink()
583
+ print(" Removed: .opencode/plugins/ai-toolkit-hooks.js")
584
+ for sub in ("agents", "commands"):
585
+ sub_dir = cwd / ".opencode" / sub
586
+ if sub_dir.is_dir():
587
+ removed_any = False
588
+ for f in sorted(sub_dir.glob("ai-toolkit-*.md")):
589
+ f.unlink()
590
+ removed_any = True
591
+ if removed_any:
592
+ print(f" Removed: .opencode/{sub}/ai-toolkit-*.md")
593
+
514
594
 
515
595
  def _create_local_claude_md(cwd: Path, reset: bool) -> None:
516
596
  claude_local = cwd / "CLAUDE.md"
@@ -699,6 +779,42 @@ def _create_local_ai_tool_configs(cwd: Path, rules_dir: Path,
699
779
  # .agents/skills/ — filtered symlinks (Codex-compatible skills only)
700
780
  _install_codex_skills(cwd)
701
781
 
782
+ if "opencode" in eds:
783
+ # AGENTS.md — shared with Codex via marker injection (opencode reads same file)
784
+ # Use a dedicated section tag so Codex and opencode don't clobber each other.
785
+ inject_with_rules(
786
+ "generate_opencode.py",
787
+ cwd / "AGENTS.md",
788
+ rules_dir,
789
+ )
790
+ # .opencode/agents/ — native subagents
791
+ from generate_opencode_agents import generate as gen_opencode_agents
792
+ written, removed = gen_opencode_agents(cwd)
793
+ msg = f" Created: .opencode/agents/ ({written} agents"
794
+ if removed:
795
+ msg += f", {removed} stale removed"
796
+ msg += ")"
797
+ print(msg)
798
+ # .opencode/commands/ — native slash commands
799
+ from generate_opencode_commands import generate as gen_opencode_commands
800
+ written, removed = gen_opencode_commands(cwd)
801
+ msg = f" Created: .opencode/commands/ ({written} commands"
802
+ if removed:
803
+ msg += f", {removed} stale removed"
804
+ msg += ")"
805
+ print(msg)
806
+ # .opencode/plugins/ai-toolkit-hooks.js — lifecycle hook bridge
807
+ from generate_opencode_plugin import generate as gen_opencode_plugin
808
+ gen_opencode_plugin(cwd)
809
+ print(" Created: .opencode/plugins/ai-toolkit-hooks.js")
810
+ # opencode.json — merge MCP servers from .mcp.json (preserves user keys)
811
+ from generate_opencode_json import merge_into_opencode_json
812
+ _, mcp_count = merge_into_opencode_json(cwd)
813
+ if mcp_count:
814
+ print(f" Updated: opencode.json ({mcp_count} MCP server(s) from .mcp.json)")
815
+ else:
816
+ print(" Updated: opencode.json ($schema set)")
817
+
702
818
  synced_paths = sync_project_mcp_to_editors(cwd, sorted(eds))
703
819
  for path in synced_paths:
704
820
  rel = path.relative_to(cwd)
@@ -89,7 +89,7 @@ def remove_mcp_template(name: str) -> None:
89
89
  DEFAULT_GLOBAL_EDITORS: list[str] = []
90
90
 
91
91
  # All editors that support global install (opt-in via --editors)
92
- GLOBAL_CAPABLE_EDITORS = ["augment", "codex", "cursor", "gemini", "windsurf"]
92
+ GLOBAL_CAPABLE_EDITORS = ["augment", "codex", "cursor", "gemini", "opencode", "windsurf"]
93
93
 
94
94
 
95
95
  def get_global_editors() -> list[str]: