spawn-cli 0.1.21__tar.gz → 0.1.22__tar.gz

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.
Files changed (56) hide show
  1. {spawn_cli-0.1.21/src/spawn_cli.egg-info → spawn_cli-0.1.22}/PKG-INFO +30 -4
  2. {spawn_cli-0.1.21 → spawn_cli-0.1.22}/README.md +31 -5
  3. {spawn_cli-0.1.21 → spawn_cli-0.1.22}/pyproject.toml +1 -1
  4. {spawn_cli-0.1.21 → spawn_cli-0.1.22}/src/spawn_cli/core/low_level.py +3 -0
  5. {spawn_cli-0.1.21 → spawn_cli-0.1.22}/src/spawn_cli/ide/__init__.py +9 -0
  6. {spawn_cli-0.1.21 → spawn_cli-0.1.22}/src/spawn_cli/ide/_vacancy.py +60 -0
  7. spawn_cli-0.1.22/src/spawn_cli/ide/hermes.py +107 -0
  8. spawn_cli-0.1.22/src/spawn_cli/ide/junie.py +141 -0
  9. spawn_cli-0.1.22/src/spawn_cli/ide/mimo_code.py +177 -0
  10. {spawn_cli-0.1.21 → spawn_cli-0.1.22}/src/spawn_cli/ide/registry.py +1 -0
  11. {spawn_cli-0.1.21 → spawn_cli-0.1.22/src/spawn_cli.egg-info}/PKG-INFO +30 -4
  12. {spawn_cli-0.1.21 → spawn_cli-0.1.22}/src/spawn_cli.egg-info/SOURCES.txt +3 -0
  13. {spawn_cli-0.1.21 → spawn_cli-0.1.22}/LICENSE +0 -0
  14. {spawn_cli-0.1.21 → spawn_cli-0.1.22}/setup.cfg +0 -0
  15. {spawn_cli-0.1.21 → spawn_cli-0.1.22}/src/spawn_cli/__init__.py +0 -0
  16. {spawn_cli-0.1.21 → spawn_cli-0.1.22}/src/spawn_cli/cli.py +0 -0
  17. {spawn_cli-0.1.21 → spawn_cli-0.1.22}/src/spawn_cli/core/__init__.py +0 -0
  18. {spawn_cli-0.1.21 → spawn_cli-0.1.22}/src/spawn_cli/core/download.py +0 -0
  19. {spawn_cli-0.1.21 → spawn_cli-0.1.22}/src/spawn_cli/core/errors.py +0 -0
  20. {spawn_cli-0.1.21 → spawn_cli-0.1.22}/src/spawn_cli/core/high_level.py +0 -0
  21. {spawn_cli-0.1.21 → spawn_cli-0.1.22}/src/spawn_cli/core/mcp_stdio.py +0 -0
  22. {spawn_cli-0.1.21 → spawn_cli-0.1.22}/src/spawn_cli/core/scripts.py +0 -0
  23. {spawn_cli-0.1.21 → spawn_cli-0.1.22}/src/spawn_cli/errors.py +0 -0
  24. {spawn_cli-0.1.21 → spawn_cli-0.1.22}/src/spawn_cli/ide/_helpers.py +0 -0
  25. {spawn_cli-0.1.21 → spawn_cli-0.1.22}/src/spawn_cli/ide/_stub.py +0 -0
  26. {spawn_cli-0.1.21 → spawn_cli-0.1.22}/src/spawn_cli/ide/claude_code.py +0 -0
  27. {spawn_cli-0.1.21 → spawn_cli-0.1.22}/src/spawn_cli/ide/cline.py +0 -0
  28. {spawn_cli-0.1.21 → spawn_cli-0.1.22}/src/spawn_cli/ide/codex.py +0 -0
  29. {spawn_cli-0.1.21 → spawn_cli-0.1.22}/src/spawn_cli/ide/cursor.py +0 -0
  30. {spawn_cli-0.1.21 → spawn_cli-0.1.22}/src/spawn_cli/ide/gemini_cli.py +0 -0
  31. {spawn_cli-0.1.21 → spawn_cli-0.1.22}/src/spawn_cli/ide/github_copilot.py +0 -0
  32. {spawn_cli-0.1.21 → spawn_cli-0.1.22}/src/spawn_cli/ide/mcp_stdio_argv.py +0 -0
  33. {spawn_cli-0.1.21 → spawn_cli-0.1.22}/src/spawn_cli/ide/opencode.py +0 -0
  34. {spawn_cli-0.1.21 → spawn_cli-0.1.22}/src/spawn_cli/ide/windsurf.py +0 -0
  35. {spawn_cli-0.1.21 → spawn_cli-0.1.22}/src/spawn_cli/io/__init__.py +0 -0
  36. {spawn_cli-0.1.21 → spawn_cli-0.1.22}/src/spawn_cli/io/json_io.py +0 -0
  37. {spawn_cli-0.1.21 → spawn_cli-0.1.22}/src/spawn_cli/io/lock.py +0 -0
  38. {spawn_cli-0.1.21 → spawn_cli-0.1.22}/src/spawn_cli/io/paths.py +0 -0
  39. {spawn_cli-0.1.21 → spawn_cli-0.1.22}/src/spawn_cli/io/text_io.py +0 -0
  40. {spawn_cli-0.1.21 → spawn_cli-0.1.22}/src/spawn_cli/io/toml_io.py +0 -0
  41. {spawn_cli-0.1.21 → spawn_cli-0.1.22}/src/spawn_cli/io/yaml_io.py +0 -0
  42. {spawn_cli-0.1.21 → spawn_cli-0.1.22}/src/spawn_cli/models/__init__.py +0 -0
  43. {spawn_cli-0.1.21 → spawn_cli-0.1.22}/src/spawn_cli/models/config.py +0 -0
  44. {spawn_cli-0.1.21 → spawn_cli-0.1.22}/src/spawn_cli/models/mcp.py +0 -0
  45. {spawn_cli-0.1.21 → spawn_cli-0.1.22}/src/spawn_cli/models/metadata.py +0 -0
  46. {spawn_cli-0.1.21 → spawn_cli-0.1.22}/src/spawn_cli/models/navigation.py +0 -0
  47. {spawn_cli-0.1.21 → spawn_cli-0.1.22}/src/spawn_cli/models/skill.py +0 -0
  48. {spawn_cli-0.1.21 → spawn_cli-0.1.22}/src/spawn_cli/resources/__init__.py +0 -0
  49. {spawn_cli-0.1.21 → spawn_cli-0.1.22}/src/spawn_cli/resources/default_core_config.yaml +0 -0
  50. {spawn_cli-0.1.21 → spawn_cli-0.1.22}/src/spawn_cli/warnings_display.py +0 -0
  51. {spawn_cli-0.1.21 → spawn_cli-0.1.22}/src/spawn_cli.egg-info/dependency_links.txt +0 -0
  52. {spawn_cli-0.1.21 → spawn_cli-0.1.22}/src/spawn_cli.egg-info/entry_points.txt +0 -0
  53. {spawn_cli-0.1.21 → spawn_cli-0.1.22}/src/spawn_cli.egg-info/requires.txt +0 -0
  54. {spawn_cli-0.1.21 → spawn_cli-0.1.22}/src/spawn_cli.egg-info/top_level.txt +0 -0
  55. {spawn_cli-0.1.21 → spawn_cli-0.1.22}/tests/test_cli.py +0 -0
  56. {spawn_cli-0.1.21 → spawn_cli-0.1.22}/tests/test_warnings_display.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: spawn-cli
3
- Version: 0.1.21
3
+ Version: 0.1.22
4
4
  Summary: A minimal installable Python CLI package.
5
5
  License: MIT
6
6
  Classifier: License :: OSI Approved :: MIT License
@@ -240,14 +240,40 @@ read-required:
240
240
 
241
241
  Then run **`spawn refresh`** so skills and entry points pick up the hint.
242
242
 
243
- ## License
243
+ ### Supported IDEs
244
244
 
245
- MIT — see [LICENSE](LICENSE).
245
+ | IDE (key) | Skills | MCP | Agent ignore | Entry point | Detection markers |
246
+ | --- | --- | --- | --- | --- | --- |
247
+ | Cursor (`cursor`) | `.cursor/skills/` | `.cursor/mcp.json` | `.cursorignore` | `AGENTS.md` | `.cursor/` |
248
+ | Codex (`codex`) | `.agents/skills/` | `.codex/config.toml` | `config.toml` permissions | `AGENTS.md` | `.codex/`, `.agents/` |
249
+ | Claude Code (`claude-code`) | `.claude/skills/` | `.mcp.json` | `.claude/settings.json` | `CLAUDE.md` | `.claude/`, `CLAUDE.md` |
250
+ | Windsurf (`windsurf`) | `.windsurf/skills/` | — | `.codeiumignore` | `AGENTS.md` | `.windsurf/`, `.codeiumignore` |
251
+ | GitHub Copilot (`github-copilot`) | `.github/skills/` | `.vscode/mcp.json` | — | `.github/copilot-instructions.md`, `AGENTS.md` | `.github/`, `.vscode/` |
252
+ | Gemini CLI (`gemini-cli`) | `.gemini/skills/` | `.gemini/settings.json` | `.geminiignore` | `GEMINI.md` | `.gemini/`, `GEMINI.md` |
253
+ | Cline (`cline`) | `.cline/skills/` | — | `.clineignore` | `AGENTS.md` | `.cline/`, `.clinerules`, `.clineignore` |
254
+ | OpenCode (`opencode`) | `.opencode/skills/` | `opencode.json` | `opencode.json` permissions | `AGENTS.md` | `.opencode/`, `opencode.json`, `opencode.jsonc` |
255
+ | Mimo Code (`mimo-code`) | `.mimocode/skills/` | `.mimocode/mimocode.json` | `mimocode.json` permissions | `AGENTS.md` | `.mimocode/` |
256
+ | Hermes (`hermes`) | `.hermes/skills/` | — | — | `.hermes.md` | `.hermes/`, `.hermes.md`, `HERMES.md` |
257
+ | JetBrains Junie (`junie`) | `.junie/skills/` | `.junie/mcp/mcp.json` | `.aiignore` | `AGENTS.md` | `.junie/` |
246
258
 
247
- ---
259
+ **Legend:**
260
+
261
+ - **Skills** — project directory where spawn renders `SKILL.md` per skill (`native` capability; frontmatter `name` matches the directory name).
262
+ - **MCP** — project-scope config file where spawn merges extension MCP servers. "—" means the tool keeps MCP global-only (e.g. `~/.hermes/config.yaml`); spawn warns (`SpawnWarning`) and writes nothing.
263
+ - **Agent ignore** — where spawn merges agent-ignore globs: a dedicated ignore file, or permissions entries in the project config (managed block / spawn-owned entries only; removal never touches user rules). "—" means no upstream ignore mechanism; spawn warns and skips.
264
+ - **Entry point** — file(s) receiving the Spawn-managed instruction block (`<!-- spawn:start -->` ... `<!-- spawn:end -->`); user content around the block is preserved.
265
+ - **Detection markers** — files/directories that make `spawn ide list-supported-ides` (and auto-detection) mark the IDE as used in the repo.
266
+
267
+ Full per-IDE contracts (config shapes, ignore semantics, caveats): [spec/design/ide-adapters.md](spec/design/ide-adapters.md).
248
268
 
249
269
  P.S. Below is a diagram that might help you understand how Spawn works.
250
270
 
251
271
  <p align="center">
252
272
  <img src="logo/Scheme.png" alt="How Spawn works" />
253
273
  </p>
274
+
275
+ ---
276
+
277
+ ## License
278
+
279
+ MIT — see [LICENSE](LICENSE).
@@ -223,14 +223,40 @@ read-required:
223
223
 
224
224
  Then run **`spawn refresh`** so skills and entry points pick up the hint.
225
225
 
226
- ## License
227
-
228
- MIT — see [LICENSE](LICENSE).
229
-
230
- ---
226
+ ### Supported IDEs
227
+
228
+ | IDE (key) | Skills | MCP | Agent ignore | Entry point | Detection markers |
229
+ | --- | --- | --- | --- | --- | --- |
230
+ | Cursor (`cursor`) | `.cursor/skills/` | `.cursor/mcp.json` | `.cursorignore` | `AGENTS.md` | `.cursor/` |
231
+ | Codex (`codex`) | `.agents/skills/` | `.codex/config.toml` | `config.toml` permissions | `AGENTS.md` | `.codex/`, `.agents/` |
232
+ | Claude Code (`claude-code`) | `.claude/skills/` | `.mcp.json` | `.claude/settings.json` | `CLAUDE.md` | `.claude/`, `CLAUDE.md` |
233
+ | Windsurf (`windsurf`) | `.windsurf/skills/` | — | `.codeiumignore` | `AGENTS.md` | `.windsurf/`, `.codeiumignore` |
234
+ | GitHub Copilot (`github-copilot`) | `.github/skills/` | `.vscode/mcp.json` | — | `.github/copilot-instructions.md`, `AGENTS.md` | `.github/`, `.vscode/` |
235
+ | Gemini CLI (`gemini-cli`) | `.gemini/skills/` | `.gemini/settings.json` | `.geminiignore` | `GEMINI.md` | `.gemini/`, `GEMINI.md` |
236
+ | Cline (`cline`) | `.cline/skills/` | — | `.clineignore` | `AGENTS.md` | `.cline/`, `.clinerules`, `.clineignore` |
237
+ | OpenCode (`opencode`) | `.opencode/skills/` | `opencode.json` | `opencode.json` permissions | `AGENTS.md` | `.opencode/`, `opencode.json`, `opencode.jsonc` |
238
+ | Mimo Code (`mimo-code`) | `.mimocode/skills/` | `.mimocode/mimocode.json` | `mimocode.json` permissions | `AGENTS.md` | `.mimocode/` |
239
+ | Hermes (`hermes`) | `.hermes/skills/` | — | — | `.hermes.md` | `.hermes/`, `.hermes.md`, `HERMES.md` |
240
+ | JetBrains Junie (`junie`) | `.junie/skills/` | `.junie/mcp/mcp.json` | `.aiignore` | `AGENTS.md` | `.junie/` |
241
+
242
+ **Legend:**
243
+
244
+ - **Skills** — project directory where spawn renders `SKILL.md` per skill (`native` capability; frontmatter `name` matches the directory name).
245
+ - **MCP** — project-scope config file where spawn merges extension MCP servers. "—" means the tool keeps MCP global-only (e.g. `~/.hermes/config.yaml`); spawn warns (`SpawnWarning`) and writes nothing.
246
+ - **Agent ignore** — where spawn merges agent-ignore globs: a dedicated ignore file, or permissions entries in the project config (managed block / spawn-owned entries only; removal never touches user rules). "—" means no upstream ignore mechanism; spawn warns and skips.
247
+ - **Entry point** — file(s) receiving the Spawn-managed instruction block (`<!-- spawn:start -->` ... `<!-- spawn:end -->`); user content around the block is preserved.
248
+ - **Detection markers** — files/directories that make `spawn ide list-supported-ides` (and auto-detection) mark the IDE as used in the repo.
249
+
250
+ Full per-IDE contracts (config shapes, ignore semantics, caveats): [spec/design/ide-adapters.md](spec/design/ide-adapters.md).
231
251
 
232
252
  P.S. Below is a diagram that might help you understand how Spawn works.
233
253
 
234
254
  <p align="center">
235
255
  <img src="logo/Scheme.png" alt="How Spawn works" />
236
256
  </p>
257
+
258
+ ---
259
+
260
+ ## License
261
+
262
+ MIT — see [LICENSE](LICENSE).
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "spawn-cli"
7
- version = "0.1.21"
7
+ version = "0.1.22"
8
8
  description = "A minimal installable Python CLI package."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -34,6 +34,9 @@ CANONICAL_IDE_KEYS: tuple[str, ...] = (
34
34
  "gemini-cli",
35
35
  "cline",
36
36
  "opencode",
37
+ "mimo-code",
38
+ "hermes",
39
+ "junie",
37
40
  )
38
41
 
39
42
  SPAWN_GITIGNORE_START = "# spawn:start"
@@ -10,9 +10,15 @@ from . import github_copilot # noqa: F401
10
10
  from . import windsurf # noqa: F401
11
11
  from . import cline # noqa: F401
12
12
  from . import opencode # noqa: F401
13
+ from . import mimo_code # noqa: F401
14
+ from . import hermes # noqa: F401
15
+ from . import junie # noqa: F401
13
16
  from ._stub import StubAdapter
14
17
  from .cline import ClineAdapter
15
18
  from .opencode import OpenCodeAdapter
19
+ from .mimo_code import MimoCodeAdapter
20
+ from .hermes import HermesAdapter
21
+ from .junie import JunieAdapter
16
22
  from .claude_code import ClaudeCodeAdapter
17
23
  from .codex import CodexAdapter
18
24
  from .cursor import CursorAdapter
@@ -48,6 +54,9 @@ __all__ = [
48
54
  "ALIASES",
49
55
  "ClineAdapter",
50
56
  "OpenCodeAdapter",
57
+ "MimoCodeAdapter",
58
+ "HermesAdapter",
59
+ "JunieAdapter",
51
60
  "ClaudeCodeAdapter",
52
61
  "CodexAdapter",
53
62
  "CursorAdapter",
@@ -261,6 +261,31 @@ def finalize_opencode_repo(target_root: Path) -> None:
261
261
  (target_root / ".opencode").rmdir()
262
262
 
263
263
 
264
+ def finalize_hermes_repo(target_root: Path) -> None:
265
+ """Hermes adapter: `.hermes/skills` dotdir; drop when vacant."""
266
+ finalize_standard_dotdir_skills_and_mcp(
267
+ target_root,
268
+ ".hermes",
269
+ allow_delete_entire=True,
270
+ )
271
+
272
+
273
+ def finalize_junie_repo(target_root: Path) -> None:
274
+ """Junie adapter: `.junie/{skills,mcp/mcp.json}`; drop parts when vacant, keep user content."""
275
+ junie_root = target_root / ".junie"
276
+ if not junie_root.is_dir():
277
+ return
278
+ try_unlink_empty_mcp_mcp_servers(junie_root / "mcp" / "mcp.json")
279
+ prune_empty_directories_under(junie_root)
280
+ if ide_dotdir_is_entirely_removable(junie_root, allow_delete_entire=True):
281
+ shutil.rmtree(junie_root, ignore_errors=True)
282
+ ignore = target_root / ".aiignore"
283
+ if ignore.is_file():
284
+ clear_split_agent_ignore_file(ignore)
285
+ if not ignore.read_text(encoding="utf-8").strip():
286
+ ignore.unlink(missing_ok=True)
287
+
288
+
264
289
  def finalize_cline_repo(target_root: Path) -> None:
265
290
  """Cline adapter: `.cline/skills` dotdir and root `.clineignore`; drop both when vacant."""
266
291
  finalize_standard_dotdir_skills_and_mcp(
@@ -275,10 +300,45 @@ def finalize_cline_repo(target_root: Path) -> None:
275
300
  ignore.unlink(missing_ok=True)
276
301
 
277
302
 
303
+ def _mimo_config_vacant(path: Path) -> bool:
304
+ data = json.loads(path.read_text(encoding="utf-8"))
305
+ if not isinstance(data, dict):
306
+ return False # user config in an unexpected shape: keep untouched
307
+ if data == {}:
308
+ return True
309
+ for key, value in data.items():
310
+ if key in ("mcp", "permission"):
311
+ if value: # non-empty -> user content or live servers
312
+ return False
313
+ elif json_value_nonempty(value): # unknown user key with substance
314
+ return False
315
+ return True
316
+
317
+
318
+ def finalize_mimo_code_repo(target_root: Path) -> None:
319
+ """Mimo Code adapter: `.mimocode/mimocode.json` (keys mcp, permission) and `.mimocode/skills`; drop when vacant."""
320
+ config = target_root / ".mimocode" / "mimocode.json"
321
+ if config.is_file():
322
+ try:
323
+ vacant = _mimo_config_vacant(config)
324
+ except json.JSONDecodeError:
325
+ vacant = False # keep unparseable user config untouched
326
+ if vacant:
327
+ config.unlink()
328
+ skills_dir = target_root / ".mimocode" / "skills"
329
+ if skills_dir.is_dir() and not dir_has_any_file(skills_dir):
330
+ prune_empty_directories_under(target_root / ".mimocode")
331
+ if (target_root / ".mimocode").is_dir() and not dir_has_any_file(target_root / ".mimocode"):
332
+ (target_root / ".mimocode").rmdir()
333
+
334
+
278
335
  __all__ = [
279
336
  "dir_has_any_file",
280
337
  "finalize_claude_repo",
281
338
  "finalize_cline_repo",
339
+ "finalize_hermes_repo",
340
+ "finalize_junie_repo",
341
+ "finalize_mimo_code_repo",
282
342
  "finalize_opencode_repo",
283
343
  "finalize_codex_repo",
284
344
  "finalize_github_copilot_repo",
@@ -0,0 +1,107 @@
1
+ """Hermes Agent (Nous Research) IDE adapter.
2
+
3
+ Project scope: skills at <repo>/.hermes/skills/<name>/SKILL.md and a
4
+ spawn-managed block in root `.hermes.md` (Hermes highest-priority project
5
+ context file; priority: .hermes.md > AGENTS.override.md > AGENTS.md).
6
+ MCP config and agent ignore are global-only in Hermes
7
+ (~/.hermes/config.yaml; no ignore file) -> unsupported here.
8
+ Project skills require `hermes skills trust` before Hermes loads them.
9
+ `.agents/skills` is intentionally NOT used (owned by the Codex adapter).
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import warnings
15
+ from pathlib import Path
16
+
17
+ from spawn_cli.errors import SpawnWarning
18
+ from spawn_cli.ide import _vacancy as _vac
19
+ from spawn_cli.ide._helpers import rewrite_managed_block
20
+ from spawn_cli.ide.registry import (
21
+ DetectResult,
22
+ IdeAdapter,
23
+ IdeCapabilities,
24
+ normalize_skill_name,
25
+ register,
26
+ render_skill_md,
27
+ )
28
+ from spawn_cli.models.mcp import NormalizedMcp
29
+ from spawn_cli.models.skill import SkillMetadata
30
+
31
+ CAPS = IdeCapabilities(
32
+ skills="native",
33
+ mcp="unsupported",
34
+ agent_ignore="unsupported",
35
+ entry_point="hermes-md",
36
+ )
37
+
38
+
39
+ class HermesAdapter(IdeAdapter):
40
+ key = "hermes"
41
+
42
+ def detect(self, target_root: Path) -> DetectResult:
43
+ used = (
44
+ (target_root / ".hermes").is_dir()
45
+ or (target_root / ".hermes.md").is_file()
46
+ or (target_root / "HERMES.md").is_file()
47
+ )
48
+ return DetectResult(used_in_repo=used, capabilities=CAPS)
49
+
50
+ def add_skills(self, target_root: Path, skill_metadata: list[SkillMetadata]) -> list[dict]:
51
+ results = []
52
+ for skill in skill_metadata:
53
+ name = normalize_skill_name(skill.name)
54
+ skill_dir = target_root / ".hermes" / "skills" / name
55
+ skill_dir.mkdir(parents=True, exist_ok=True)
56
+ skill_path = skill_dir / "SKILL.md"
57
+ if skill_path.exists():
58
+ warnings.warn(f"Overwriting existing rendered skill: {skill_path}")
59
+ # Hermes requires frontmatter name == directory name.
60
+ rendered = render_skill_md(skill.model_copy(update={"name": name}))
61
+ skill_path.write_text(rendered, encoding="utf-8")
62
+ rel = skill_path.relative_to(target_root).as_posix()
63
+ results.append({"skill": skill.name, "path": rel})
64
+ return results
65
+
66
+ def remove_skills(self, target_root: Path, rendered_paths: list[dict]) -> None:
67
+ for entry in rendered_paths:
68
+ p = target_root / entry["path"]
69
+ if p.exists():
70
+ p.unlink()
71
+ if p.parent.exists() and not any(p.parent.iterdir()):
72
+ p.parent.rmdir()
73
+
74
+ def add_mcp(self, target_root: Path, normalized_mcp: NormalizedMcp) -> list[str]:
75
+ del normalized_mcp
76
+ del target_root
77
+ warnings.warn(
78
+ "Hermes has no project-scope MCP settings; "
79
+ "servers stay configured globally in ~/.hermes/config.yaml",
80
+ SpawnWarning,
81
+ )
82
+ return []
83
+
84
+ def remove_mcp(self, target_root: Path, rendered_mcp_names: list[str]) -> None:
85
+ del target_root, rendered_mcp_names
86
+ warnings.warn("MCP is not managed by spawn for hermes; nothing to remove", SpawnWarning)
87
+
88
+ def add_agent_ignore(self, target_root: Path, globs: list[str]) -> None:
89
+ del target_root, globs
90
+ warnings.warn("hermes: agent ignore is unsupported", SpawnWarning)
91
+
92
+ def remove_agent_ignore(self, target_root: Path, globs: list[str]) -> None:
93
+ del target_root, globs
94
+ warnings.warn("hermes: agent ignore is unsupported", SpawnWarning)
95
+
96
+ def rewrite_entry_point(self, target_root: Path, prompt: str) -> str:
97
+ ep = target_root / ".hermes.md"
98
+ rewrite_managed_block(ep, prompt)
99
+ return ep.relative_to(target_root).as_posix()
100
+
101
+ def finalize_repo_after_ide_removed(self, target_root: Path) -> None:
102
+ _vac.finalize_hermes_repo(target_root)
103
+
104
+
105
+ register(HermesAdapter())
106
+
107
+ __all__ = ["HermesAdapter"]
@@ -0,0 +1,141 @@
1
+ """JetBrains Junie IDE adapter: .junie/skills, .junie/mcp/mcp.json, .aiignore, AGENTS.md."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import warnings
7
+ from pathlib import Path
8
+
9
+ from spawn_cli.errors import SpawnWarning
10
+ from spawn_cli.ide import _vacancy as _vac
11
+ from spawn_cli.ide.mcp_stdio_argv import mcp_stdio_argv
12
+ from spawn_cli.ide._helpers import (
13
+ clear_split_agent_ignore_file,
14
+ remove_ignore_block,
15
+ rewrite_core_agent_ignore_region,
16
+ rewrite_extension_agent_ignore_region,
17
+ rewrite_ignore_block,
18
+ )
19
+ from spawn_cli.ide.registry import (
20
+ DetectResult,
21
+ IdeAdapter,
22
+ IdeCapabilities,
23
+ register,
24
+ normalize_skill_name,
25
+ render_skill_md,
26
+ rewrite_managed_block,
27
+ )
28
+ from spawn_cli.models.mcp import McpServer, NormalizedMcp
29
+ from spawn_cli.models.skill import SkillMetadata
30
+
31
+
32
+ def _build_mcp_server_entry(server: McpServer) -> dict:
33
+ """Convert McpServer to .junie/mcp/mcp.json entry (mcpServers format)."""
34
+ transport = server.transport
35
+ if transport.type == "stdio":
36
+ if server.spawn_stdio_proxy:
37
+ entry = {"command": "spawn", "args": mcp_stdio_argv(server.extension, server.name)}
38
+ else:
39
+ entry = {"command": transport.command, "args": transport.args}
40
+ elif transport.type in ("streamable-http", "sse"):
41
+ entry = {"type": transport.type, "url": transport.url}
42
+ if transport.headers:
43
+ entry["headers"] = {k: f"${{{v}}}" for k, v in transport.headers.items()}
44
+ else:
45
+ entry = {"command": transport.command, "args": transport.args}
46
+ if server.env:
47
+ entry["env"] = {
48
+ k: f"${{{k}}}" if v.secret else v.value or f"${{{k}}}"
49
+ for k, v in server.env.items()
50
+ }
51
+ return entry
52
+
53
+
54
+ class JunieAdapter(IdeAdapter):
55
+ key = "junie"
56
+
57
+ def detect(self, target_root: Path) -> DetectResult:
58
+ used = (target_root / ".junie").exists()
59
+ return DetectResult(
60
+ used_in_repo=used,
61
+ capabilities=IdeCapabilities(
62
+ skills="native",
63
+ mcp="project",
64
+ agent_ignore="native",
65
+ entry_point="agents-md",
66
+ ),
67
+ )
68
+
69
+ def add_skills(self, target_root: Path, skill_metadata: list[SkillMetadata]) -> list[dict]:
70
+ results = []
71
+ for skill in skill_metadata:
72
+ name = normalize_skill_name(skill.name)
73
+ skill_dir = target_root / ".junie" / "skills" / name
74
+ skill_dir.mkdir(parents=True, exist_ok=True)
75
+ skill_path = skill_dir / "SKILL.md"
76
+ if skill_path.exists():
77
+ warnings.warn(f"Overwriting existing rendered skill: {skill_path}", SpawnWarning)
78
+ skill_path.write_text(render_skill_md(skill.model_copy(update={"name": name})), encoding="utf-8")
79
+ rel = skill_path.relative_to(target_root).as_posix()
80
+ results.append({"skill": skill.name, "path": rel})
81
+ return results
82
+
83
+ def remove_skills(self, target_root: Path, rendered_paths: list[dict]) -> None:
84
+ for entry in rendered_paths:
85
+ p = target_root / entry["path"]
86
+ if p.exists():
87
+ p.unlink()
88
+ if p.parent.exists() and not any(p.parent.iterdir()):
89
+ p.parent.rmdir()
90
+
91
+ def add_mcp(self, target_root: Path, normalized_mcp: NormalizedMcp) -> list[str]:
92
+ """Merge Spawn-owned servers into .junie/mcp/mcp.json (mcpServers format)."""
93
+ mcp_path = target_root / ".junie" / "mcp" / "mcp.json"
94
+ mcp_path.parent.mkdir(parents=True, exist_ok=True)
95
+ data = json.loads(mcp_path.read_text(encoding="utf-8")) if mcp_path.exists() else {}
96
+ data.setdefault("mcpServers", {})
97
+ rendered_names = []
98
+ for server in normalized_mcp.servers:
99
+ entry = _build_mcp_server_entry(server)
100
+ data["mcpServers"][server.name] = entry
101
+ rendered_names.append(server.name)
102
+ mcp_path.write_text(json.dumps(data, indent=2), encoding="utf-8")
103
+ return rendered_names
104
+
105
+ def remove_mcp(self, target_root: Path, rendered_mcp_names: list[str]) -> None:
106
+ """Remove named entries from .junie/mcp/mcp.json."""
107
+ mcp_path = target_root / ".junie" / "mcp" / "mcp.json"
108
+ if not mcp_path.exists():
109
+ return
110
+ data = json.loads(mcp_path.read_text(encoding="utf-8"))
111
+ for name in rendered_mcp_names:
112
+ data.get("mcpServers", {}).pop(name, None)
113
+ mcp_path.write_text(json.dumps(data, indent=2), encoding="utf-8")
114
+
115
+ def add_agent_ignore(self, target_root: Path, globs: list[str]) -> None:
116
+ rewrite_ignore_block(target_root / ".aiignore", globs)
117
+
118
+ def remove_agent_ignore(self, target_root: Path, globs: list[str]) -> None:
119
+ remove_ignore_block(target_root / ".aiignore", globs)
120
+
121
+ def rewrite_core_agent_ignore(self, target_root: Path, globs: list[str]) -> None:
122
+ rewrite_core_agent_ignore_region(target_root / ".aiignore", globs)
123
+
124
+ def rewrite_extension_agent_ignore(self, target_root: Path, globs: list[str]) -> None:
125
+ rewrite_extension_agent_ignore_region(target_root / ".aiignore", globs)
126
+
127
+ def clear_spawn_agent_ignore(self, target_root: Path) -> None:
128
+ clear_split_agent_ignore_file(target_root / ".aiignore")
129
+
130
+ def rewrite_entry_point(self, target_root: Path, prompt: str) -> str:
131
+ ep = target_root / "AGENTS.md"
132
+ rewrite_managed_block(ep, prompt)
133
+ return str(ep.relative_to(target_root))
134
+
135
+ def finalize_repo_after_ide_removed(self, target_root: Path) -> None:
136
+ _vac.finalize_junie_repo(target_root)
137
+
138
+
139
+ register(JunieAdapter())
140
+
141
+ __all__ = ["JunieAdapter"]
@@ -0,0 +1,177 @@
1
+ """Mimo Code adapter: .mimocode/skills, .mimocode/mimocode.json (mcp + permission), AGENTS.md."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import warnings
7
+ from pathlib import Path
8
+
9
+ from spawn_cli.core import low_level as ll
10
+ from spawn_cli.errors import SpawnError, SpawnWarning
11
+ from spawn_cli.ide import _vacancy as _vac
12
+ from spawn_cli.ide.mcp_stdio_argv import mcp_stdio_argv
13
+ from spawn_cli.ide.registry import (
14
+ DetectResult,
15
+ IdeAdapter,
16
+ IdeCapabilities,
17
+ normalize_skill_name,
18
+ register,
19
+ render_skill_md,
20
+ rewrite_managed_block,
21
+ )
22
+ from spawn_cli.models.mcp import McpServer, NormalizedMcp
23
+ from spawn_cli.models.skill import SkillMetadata
24
+
25
+ _CONFIG = Path(".mimocode") / "mimocode.json"
26
+
27
+
28
+ def _load_config(target_root: Path) -> dict:
29
+ path = target_root / _CONFIG
30
+ if not path.exists():
31
+ if (target_root / ".mimocode" / "mimocode.jsonc").exists():
32
+ warnings.warn(
33
+ "Mimo Code: Spawn manages .mimocode/mimocode.json; existing mimocode.jsonc is not merged",
34
+ SpawnWarning,
35
+ )
36
+ return {}
37
+ try:
38
+ return json.loads(path.read_text(encoding="utf-8"))
39
+ except json.JSONDecodeError as exc:
40
+ raise SpawnError(f"Mimo Code config is not valid JSON: {path} ({exc})") from exc
41
+
42
+
43
+ def _save_config(target_root: Path, data: dict) -> None:
44
+ path = target_root / _CONFIG
45
+ path.parent.mkdir(parents=True, exist_ok=True)
46
+ path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
47
+
48
+
49
+ def _build_mcp_entry(server: McpServer) -> dict:
50
+ transport = server.transport
51
+ if transport.type == "stdio":
52
+ if server.spawn_stdio_proxy:
53
+ entry = {
54
+ "type": "local",
55
+ "command": ["spawn"] + mcp_stdio_argv(server.extension, server.name),
56
+ }
57
+ else:
58
+ entry = {"type": "local", "command": [transport.command, *list(transport.args)]}
59
+ else: # streamable-http and sse both render as remote
60
+ entry = {"type": "remote", "url": transport.url}
61
+ if transport.headers:
62
+ entry["headers"] = {k: f"{{env:{v}}}" for k, v in transport.headers.items()}
63
+ entry["enabled"] = True
64
+ if server.env:
65
+ entry["environment"] = {
66
+ k: f"{{env:{k}}}" if v.secret else (v.value or f"{{env:{k}}}")
67
+ for k, v in server.env.items()
68
+ }
69
+ return entry
70
+
71
+
72
+ class MimoCodeAdapter(IdeAdapter):
73
+ key = "mimo-code"
74
+
75
+ def detect(self, target_root: Path) -> DetectResult:
76
+ used = (target_root / ".mimocode").exists()
77
+ return DetectResult(
78
+ used_in_repo=used,
79
+ capabilities=IdeCapabilities(
80
+ skills="native",
81
+ mcp="project",
82
+ agent_ignore="project",
83
+ entry_point="agents-md",
84
+ ),
85
+ )
86
+
87
+ def add_skills(self, target_root: Path, skill_metadata: list[SkillMetadata]) -> list[dict]:
88
+ results = []
89
+ for skill in skill_metadata:
90
+ name = normalize_skill_name(skill.name)
91
+ skill_dir = target_root / ".mimocode" / "skills" / name
92
+ skill_dir.mkdir(parents=True, exist_ok=True)
93
+ skill_path = skill_dir / "SKILL.md"
94
+ if skill_path.exists():
95
+ warnings.warn(f"Overwriting existing rendered skill: {skill_path}", SpawnWarning)
96
+ # Mimo Code requires frontmatter name == directory name.
97
+ rendered = render_skill_md(skill.model_copy(update={"name": name}))
98
+ skill_path.write_text(rendered, encoding="utf-8")
99
+ results.append({"skill": skill.name, "path": skill_path.relative_to(target_root).as_posix()})
100
+ return results
101
+
102
+ def remove_skills(self, target_root: Path, rendered_paths: list[dict]) -> None:
103
+ for entry in rendered_paths:
104
+ p = target_root / entry["path"]
105
+ if p.exists():
106
+ p.unlink()
107
+ if p.parent.exists() and not any(p.parent.iterdir()):
108
+ p.parent.rmdir()
109
+
110
+ def add_mcp(self, target_root: Path, normalized_mcp: NormalizedMcp) -> list[str]:
111
+ data = _load_config(target_root)
112
+ mcp = data.setdefault("mcp", {})
113
+ for server in normalized_mcp.servers:
114
+ mcp[server.name] = _build_mcp_entry(server)
115
+ _save_config(target_root, data)
116
+ return [s.name for s in normalized_mcp.servers]
117
+
118
+ def remove_mcp(self, target_root: Path, rendered_mcp_names: list[str]) -> None:
119
+ if not (target_root / _CONFIG).exists():
120
+ return
121
+ data = _load_config(target_root)
122
+ mcp = data.get("mcp")
123
+ if isinstance(mcp, dict):
124
+ for name in rendered_mcp_names:
125
+ mcp.pop(name, None)
126
+ if not mcp:
127
+ data.pop("mcp", None)
128
+ _save_config(target_root, data)
129
+
130
+ def add_agent_ignore(self, target_root: Path, globs: list[str]) -> None:
131
+ data = _load_config(target_root)
132
+ perms = data.setdefault("permission", {})
133
+ for section in ("read", "edit"):
134
+ rules = perms.setdefault(section, {})
135
+ if not isinstance(rules, dict):
136
+ rules = {}
137
+ perms[section] = rules
138
+ for glob in globs:
139
+ rules[glob] = "deny"
140
+ _save_config(target_root, data)
141
+
142
+ def remove_agent_ignore(self, target_root: Path, globs: list[str]) -> None:
143
+ if not (target_root / _CONFIG).exists():
144
+ return
145
+ data = _load_config(target_root)
146
+ perms = data.get("permission")
147
+ if not isinstance(perms, dict):
148
+ return
149
+ drop = set(globs)
150
+ for section in ("read", "edit"):
151
+ rules = perms.get(section)
152
+ if isinstance(rules, dict):
153
+ for glob in drop:
154
+ rules.pop(glob, None)
155
+ if not rules:
156
+ perms.pop(section, None)
157
+ if not perms:
158
+ data.pop("permission", None)
159
+ _save_config(target_root, data)
160
+
161
+ def clear_spawn_agent_ignore(self, target_root: Path) -> None:
162
+ recorded = ll.get_agent_ignore_list(target_root, self.key)
163
+ if recorded:
164
+ self.remove_agent_ignore(target_root, recorded)
165
+
166
+ def rewrite_entry_point(self, target_root: Path, prompt: str) -> str:
167
+ ep = target_root / "AGENTS.md"
168
+ rewrite_managed_block(ep, prompt)
169
+ return ep.relative_to(target_root).as_posix()
170
+
171
+ def finalize_repo_after_ide_removed(self, target_root: Path) -> None:
172
+ _vac.finalize_mimo_code_repo(target_root)
173
+
174
+
175
+ register(MimoCodeAdapter())
176
+
177
+ __all__ = ["MimoCodeAdapter"]
@@ -102,6 +102,7 @@ ALIASES: dict[str, str] = {
102
102
  "gemini": "gemini-cli",
103
103
  "copilot": "github-copilot",
104
104
  "github": "github-copilot",
105
+ "mimo": "mimo-code",
105
106
  }
106
107
 
107
108
  _registry: dict[str, IdeAdapter] = {} # populated at module load by adapter modules
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: spawn-cli
3
- Version: 0.1.21
3
+ Version: 0.1.22
4
4
  Summary: A minimal installable Python CLI package.
5
5
  License: MIT
6
6
  Classifier: License :: OSI Approved :: MIT License
@@ -240,14 +240,40 @@ read-required:
240
240
 
241
241
  Then run **`spawn refresh`** so skills and entry points pick up the hint.
242
242
 
243
- ## License
243
+ ### Supported IDEs
244
244
 
245
- MIT — see [LICENSE](LICENSE).
245
+ | IDE (key) | Skills | MCP | Agent ignore | Entry point | Detection markers |
246
+ | --- | --- | --- | --- | --- | --- |
247
+ | Cursor (`cursor`) | `.cursor/skills/` | `.cursor/mcp.json` | `.cursorignore` | `AGENTS.md` | `.cursor/` |
248
+ | Codex (`codex`) | `.agents/skills/` | `.codex/config.toml` | `config.toml` permissions | `AGENTS.md` | `.codex/`, `.agents/` |
249
+ | Claude Code (`claude-code`) | `.claude/skills/` | `.mcp.json` | `.claude/settings.json` | `CLAUDE.md` | `.claude/`, `CLAUDE.md` |
250
+ | Windsurf (`windsurf`) | `.windsurf/skills/` | — | `.codeiumignore` | `AGENTS.md` | `.windsurf/`, `.codeiumignore` |
251
+ | GitHub Copilot (`github-copilot`) | `.github/skills/` | `.vscode/mcp.json` | — | `.github/copilot-instructions.md`, `AGENTS.md` | `.github/`, `.vscode/` |
252
+ | Gemini CLI (`gemini-cli`) | `.gemini/skills/` | `.gemini/settings.json` | `.geminiignore` | `GEMINI.md` | `.gemini/`, `GEMINI.md` |
253
+ | Cline (`cline`) | `.cline/skills/` | — | `.clineignore` | `AGENTS.md` | `.cline/`, `.clinerules`, `.clineignore` |
254
+ | OpenCode (`opencode`) | `.opencode/skills/` | `opencode.json` | `opencode.json` permissions | `AGENTS.md` | `.opencode/`, `opencode.json`, `opencode.jsonc` |
255
+ | Mimo Code (`mimo-code`) | `.mimocode/skills/` | `.mimocode/mimocode.json` | `mimocode.json` permissions | `AGENTS.md` | `.mimocode/` |
256
+ | Hermes (`hermes`) | `.hermes/skills/` | — | — | `.hermes.md` | `.hermes/`, `.hermes.md`, `HERMES.md` |
257
+ | JetBrains Junie (`junie`) | `.junie/skills/` | `.junie/mcp/mcp.json` | `.aiignore` | `AGENTS.md` | `.junie/` |
246
258
 
247
- ---
259
+ **Legend:**
260
+
261
+ - **Skills** — project directory where spawn renders `SKILL.md` per skill (`native` capability; frontmatter `name` matches the directory name).
262
+ - **MCP** — project-scope config file where spawn merges extension MCP servers. "—" means the tool keeps MCP global-only (e.g. `~/.hermes/config.yaml`); spawn warns (`SpawnWarning`) and writes nothing.
263
+ - **Agent ignore** — where spawn merges agent-ignore globs: a dedicated ignore file, or permissions entries in the project config (managed block / spawn-owned entries only; removal never touches user rules). "—" means no upstream ignore mechanism; spawn warns and skips.
264
+ - **Entry point** — file(s) receiving the Spawn-managed instruction block (`<!-- spawn:start -->` ... `<!-- spawn:end -->`); user content around the block is preserved.
265
+ - **Detection markers** — files/directories that make `spawn ide list-supported-ides` (and auto-detection) mark the IDE as used in the repo.
266
+
267
+ Full per-IDE contracts (config shapes, ignore semantics, caveats): [spec/design/ide-adapters.md](spec/design/ide-adapters.md).
248
268
 
249
269
  P.S. Below is a diagram that might help you understand how Spawn works.
250
270
 
251
271
  <p align="center">
252
272
  <img src="logo/Scheme.png" alt="How Spawn works" />
253
273
  </p>
274
+
275
+ ---
276
+
277
+ ## License
278
+
279
+ MIT — see [LICENSE](LICENSE).
@@ -28,7 +28,10 @@ src/spawn_cli/ide/codex.py
28
28
  src/spawn_cli/ide/cursor.py
29
29
  src/spawn_cli/ide/gemini_cli.py
30
30
  src/spawn_cli/ide/github_copilot.py
31
+ src/spawn_cli/ide/hermes.py
32
+ src/spawn_cli/ide/junie.py
31
33
  src/spawn_cli/ide/mcp_stdio_argv.py
34
+ src/spawn_cli/ide/mimo_code.py
32
35
  src/spawn_cli/ide/opencode.py
33
36
  src/spawn_cli/ide/registry.py
34
37
  src/spawn_cli/ide/windsurf.py
File without changes
File without changes
File without changes