agent-brain-cli 9.0.0__tar.gz → 9.3.0__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 (37) hide show
  1. {agent_brain_cli-9.0.0 → agent_brain_cli-9.3.0}/PKG-INFO +2 -2
  2. {agent_brain_cli-9.0.0 → agent_brain_cli-9.3.0}/agent_brain_cli/__init__.py +1 -1
  3. {agent_brain_cli-9.0.0 → agent_brain_cli-9.3.0}/agent_brain_cli/commands/install_agent.py +147 -57
  4. {agent_brain_cli-9.0.0 → agent_brain_cli-9.3.0}/agent_brain_cli/runtime/__init__.py +8 -0
  5. agent_brain_cli-9.3.0/agent_brain_cli/runtime/codex_converter.py +215 -0
  6. {agent_brain_cli-9.0.0 → agent_brain_cli-9.3.0}/agent_brain_cli/runtime/parser.py +66 -0
  7. agent_brain_cli-9.3.0/agent_brain_cli/runtime/skill_runtime_converter.py +234 -0
  8. {agent_brain_cli-9.0.0 → agent_brain_cli-9.3.0}/agent_brain_cli/runtime/types.py +22 -0
  9. {agent_brain_cli-9.0.0 → agent_brain_cli-9.3.0}/pyproject.toml +2 -2
  10. {agent_brain_cli-9.0.0 → agent_brain_cli-9.3.0}/README.md +0 -0
  11. {agent_brain_cli-9.0.0 → agent_brain_cli-9.3.0}/agent_brain_cli/cli.py +0 -0
  12. {agent_brain_cli-9.0.0 → agent_brain_cli-9.3.0}/agent_brain_cli/client/__init__.py +0 -0
  13. {agent_brain_cli-9.0.0 → agent_brain_cli-9.3.0}/agent_brain_cli/client/api_client.py +0 -0
  14. {agent_brain_cli-9.0.0 → agent_brain_cli-9.3.0}/agent_brain_cli/commands/__init__.py +0 -0
  15. {agent_brain_cli-9.0.0 → agent_brain_cli-9.3.0}/agent_brain_cli/commands/cache.py +0 -0
  16. {agent_brain_cli-9.0.0 → agent_brain_cli-9.3.0}/agent_brain_cli/commands/config.py +0 -0
  17. {agent_brain_cli-9.0.0 → agent_brain_cli-9.3.0}/agent_brain_cli/commands/folders.py +0 -0
  18. {agent_brain_cli-9.0.0 → agent_brain_cli-9.3.0}/agent_brain_cli/commands/index.py +0 -0
  19. {agent_brain_cli-9.0.0 → agent_brain_cli-9.3.0}/agent_brain_cli/commands/init.py +0 -0
  20. {agent_brain_cli-9.0.0 → agent_brain_cli-9.3.0}/agent_brain_cli/commands/inject.py +0 -0
  21. {agent_brain_cli-9.0.0 → agent_brain_cli-9.3.0}/agent_brain_cli/commands/jobs.py +0 -0
  22. {agent_brain_cli-9.0.0 → agent_brain_cli-9.3.0}/agent_brain_cli/commands/list_cmd.py +0 -0
  23. {agent_brain_cli-9.0.0 → agent_brain_cli-9.3.0}/agent_brain_cli/commands/query.py +0 -0
  24. {agent_brain_cli-9.0.0 → agent_brain_cli-9.3.0}/agent_brain_cli/commands/reset.py +0 -0
  25. {agent_brain_cli-9.0.0 → agent_brain_cli-9.3.0}/agent_brain_cli/commands/start.py +0 -0
  26. {agent_brain_cli-9.0.0 → agent_brain_cli-9.3.0}/agent_brain_cli/commands/status.py +0 -0
  27. {agent_brain_cli-9.0.0 → agent_brain_cli-9.3.0}/agent_brain_cli/commands/stop.py +0 -0
  28. {agent_brain_cli-9.0.0 → agent_brain_cli-9.3.0}/agent_brain_cli/commands/types.py +0 -0
  29. {agent_brain_cli-9.0.0 → agent_brain_cli-9.3.0}/agent_brain_cli/commands/uninstall.py +0 -0
  30. {agent_brain_cli-9.0.0 → agent_brain_cli-9.3.0}/agent_brain_cli/config.py +0 -0
  31. {agent_brain_cli-9.0.0 → agent_brain_cli-9.3.0}/agent_brain_cli/migration.py +0 -0
  32. {agent_brain_cli-9.0.0 → agent_brain_cli-9.3.0}/agent_brain_cli/runtime/claude_converter.py +0 -0
  33. {agent_brain_cli-9.0.0 → agent_brain_cli-9.3.0}/agent_brain_cli/runtime/converter_base.py +0 -0
  34. {agent_brain_cli-9.0.0 → agent_brain_cli-9.3.0}/agent_brain_cli/runtime/gemini_converter.py +0 -0
  35. {agent_brain_cli-9.0.0 → agent_brain_cli-9.3.0}/agent_brain_cli/runtime/opencode_converter.py +0 -0
  36. {agent_brain_cli-9.0.0 → agent_brain_cli-9.3.0}/agent_brain_cli/runtime/tool_maps.py +0 -0
  37. {agent_brain_cli-9.0.0 → agent_brain_cli-9.3.0}/agent_brain_cli/xdg_paths.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: agent-brain-cli
3
- Version: 9.0.0
3
+ Version: 9.3.0
4
4
  Summary: Agent Brain CLI - Command-line interface for managing AI agent memory and knowledge retrieval
5
5
  Home-page: https://github.com/SpillwaveSolutions/agent-brain
6
6
  License: MIT
@@ -15,7 +15,7 @@ Classifier: Programming Language :: Python :: 3
15
15
  Classifier: Programming Language :: Python :: 3.10
16
16
  Classifier: Programming Language :: Python :: 3.11
17
17
  Classifier: Programming Language :: Python :: 3.12
18
- Requires-Dist: agent-brain-rag (>=9.0.0,<10.0.0)
18
+ Requires-Dist: agent-brain-rag (>=9.3.0,<10.0.0)
19
19
  Requires-Dist: click (>=8.1.0,<9.0.0)
20
20
  Requires-Dist: httpx (>=0.28.0,<0.29.0)
21
21
  Requires-Dist: pydantic (>=2.10.0,<3.0.0)
@@ -1,3 +1,3 @@
1
1
  """Doc-Serve CLI - Command-line interface for managing Doc-Serve server."""
2
2
 
3
- __version__ = "9.0.0"
3
+ __version__ = "9.3.0"
@@ -2,15 +2,18 @@
2
2
 
3
3
  import json
4
4
  from pathlib import Path
5
+ from typing import Any
5
6
 
6
7
  import click
7
8
  from rich.console import Console
8
9
  from rich.panel import Panel
9
10
 
10
11
  from agent_brain_cli.runtime.claude_converter import ClaudeConverter
12
+ from agent_brain_cli.runtime.codex_converter import CodexConverter
11
13
  from agent_brain_cli.runtime.gemini_converter import GeminiConverter
12
14
  from agent_brain_cli.runtime.opencode_converter import OpenCodeConverter
13
15
  from agent_brain_cli.runtime.parser import parse_plugin_dir
16
+ from agent_brain_cli.runtime.skill_runtime_converter import SkillRuntimeConverter
14
17
  from agent_brain_cli.runtime.types import Scope
15
18
 
16
19
  console = Console()
@@ -29,12 +32,29 @@ INSTALL_DIRS: dict[str, dict[str, str]] = {
29
32
  "project": ".gemini/plugins/agent-brain",
30
33
  "global": "~/.config/gemini/plugins/agent-brain",
31
34
  },
35
+ "codex": {
36
+ "project": ".codex/skills/agent-brain",
37
+ "global": "~/.codex/skills/agent-brain",
38
+ },
32
39
  }
33
40
 
34
- CONVERTERS: dict[str, type[ClaudeConverter | OpenCodeConverter | GeminiConverter]] = {
41
+ # Runtimes that require --dir (no default directory)
42
+ DIR_REQUIRED_RUNTIMES = {"skill-runtime"}
43
+
44
+ ConverterType = type[
45
+ ClaudeConverter
46
+ | OpenCodeConverter
47
+ | GeminiConverter
48
+ | SkillRuntimeConverter
49
+ | CodexConverter
50
+ ]
51
+
52
+ CONVERTERS: dict[str, ConverterType] = {
35
53
  "claude": ClaudeConverter,
36
54
  "opencode": OpenCodeConverter,
37
55
  "gemini": GeminiConverter,
56
+ "skill-runtime": SkillRuntimeConverter,
57
+ "codex": CodexConverter,
38
58
  }
39
59
 
40
60
 
@@ -58,9 +78,14 @@ def _find_plugin_dir() -> Path | None:
58
78
 
59
79
 
60
80
  def _resolve_target_dir(
61
- runtime: str, scope: str, project_root: Path | None = None
81
+ runtime: str,
82
+ scope: str,
83
+ project_root: Path | None = None,
84
+ custom_dir: str | None = None,
62
85
  ) -> Path:
63
86
  """Resolve the target installation directory."""
87
+ if custom_dir:
88
+ return Path(custom_dir).expanduser().resolve()
64
89
  dir_template = INSTALL_DIRS[runtime][scope]
65
90
  if scope == "global":
66
91
  return Path(dir_template).expanduser()
@@ -69,12 +94,15 @@ def _resolve_target_dir(
69
94
  return project_root / dir_template
70
95
 
71
96
 
97
+ RUNTIME_CHOICES = ["claude", "opencode", "gemini", "skill-runtime", "codex"]
98
+
99
+
72
100
  @click.command("install-agent")
73
101
  @click.option(
74
102
  "--agent",
75
103
  "-a",
76
104
  required=True,
77
- type=click.Choice(["claude", "opencode", "gemini"]),
105
+ type=click.Choice(RUNTIME_CHOICES),
78
106
  help="Target runtime to install for",
79
107
  )
80
108
  @click.option(
@@ -95,6 +123,12 @@ def _resolve_target_dir(
95
123
  type=click.Path(exists=True, file_okay=False, resolve_path=True),
96
124
  help="Custom canonical plugin source directory",
97
125
  )
126
+ @click.option(
127
+ "--dir",
128
+ "target_dir_option",
129
+ type=click.Path(resolve_path=True),
130
+ help="Target skill directory (required for skill-runtime)",
131
+ )
98
132
  @click.option(
99
133
  "--dry-run",
100
134
  is_flag=True,
@@ -116,6 +150,7 @@ def install_agent_command(
116
150
  agent: str,
117
151
  scope: str,
118
152
  plugin_dir: str | None,
153
+ target_dir_option: str | None,
119
154
  dry_run: bool,
120
155
  json_output: bool,
121
156
  path: str | None,
@@ -130,9 +165,22 @@ def install_agent_command(
130
165
  agent-brain install-agent --agent claude --project
131
166
  agent-brain install-agent --agent opencode --global
132
167
  agent-brain install-agent --agent gemini --dry-run
133
- agent-brain install-agent --agent claude --plugin-dir ./my-plugin
168
+ agent-brain install-agent --agent skill-runtime --dir ./my-skills
169
+ agent-brain install-agent --agent codex
134
170
  """
135
171
  try:
172
+ # Validate --dir requirement for skill-runtime
173
+ if agent in DIR_REQUIRED_RUNTIMES and not target_dir_option:
174
+ msg = (
175
+ f"--dir is required for --agent {agent}. "
176
+ "Specify the target skill directory."
177
+ )
178
+ if json_output:
179
+ click.echo(json.dumps({"error": msg}))
180
+ else:
181
+ console.print(f"[red]Error:[/] {msg}")
182
+ raise SystemExit(1)
183
+
136
184
  # Resolve plugin source directory
137
185
  source: Path
138
186
  if plugin_dir:
@@ -158,12 +206,14 @@ def install_agent_command(
158
206
  console.print(
159
207
  f"[dim]Parsed {len(bundle.commands)} commands, "
160
208
  f"{len(bundle.agents)} agents, "
161
- f"{len(bundle.skills)} skills[/]"
209
+ f"{len(bundle.skills)} skills, "
210
+ f"{len(bundle.templates)} templates, "
211
+ f"{len(bundle.scripts)} scripts[/]"
162
212
  )
163
213
 
164
214
  # Resolve target directory
165
215
  project_root = Path(path) if path else None
166
- target = _resolve_target_dir(agent, scope, project_root)
216
+ target = _resolve_target_dir(agent, scope, project_root, target_dir_option)
167
217
 
168
218
  # Create converter
169
219
  converter_cls = CONVERTERS[agent]
@@ -171,62 +221,36 @@ def install_agent_command(
171
221
  scope_enum = Scope.GLOBAL if scope == "global" else Scope.PROJECT
172
222
 
173
223
  if dry_run:
174
- # Simulate install without writing
175
- import tempfile
176
-
177
- with tempfile.TemporaryDirectory() as tmp:
178
- tmp_target = Path(tmp)
179
- files = converter.install(bundle, tmp_target, scope_enum)
180
- # Remap paths to real target
181
- planned = [target / f.relative_to(tmp_target) for f in files]
182
-
183
- if json_output:
184
- click.echo(
185
- json.dumps(
186
- {
187
- "dry_run": True,
188
- "agent": agent,
189
- "scope": scope,
190
- "target_dir": str(target),
191
- "files": [str(f) for f in planned],
192
- "file_count": len(planned),
193
- },
194
- indent=2,
195
- )
196
- )
197
- else:
198
- console.print(
199
- Panel(
200
- f"[yellow]Dry run[/] — no files written\n\n"
201
- f"[bold]Runtime:[/] {agent}\n"
202
- f"[bold]Scope:[/] {scope}\n"
203
- f"[bold]Target:[/] {target}\n"
204
- f"[bold]Files:[/] {len(planned)}",
205
- title="Install Preview",
206
- border_style="yellow",
207
- )
208
- )
209
- for f in planned:
210
- console.print(f" [dim]{f}[/]")
224
+ _handle_dry_run(
225
+ converter,
226
+ bundle,
227
+ target,
228
+ scope_enum,
229
+ agent,
230
+ scope,
231
+ json_output,
232
+ )
211
233
  return
212
234
 
213
235
  # Actually install
214
- files = converter.install(bundle, target, scope_enum)
236
+ if isinstance(converter, CodexConverter):
237
+ codex_root = Path(path) if path else Path.cwd()
238
+ files = converter.install(
239
+ bundle, target, scope_enum, project_root=codex_root
240
+ )
241
+ else:
242
+ files = converter.install(bundle, target, scope_enum)
215
243
 
216
244
  if json_output:
217
- click.echo(
218
- json.dumps(
219
- {
220
- "status": "installed",
221
- "agent": agent,
222
- "scope": scope,
223
- "target_dir": str(target),
224
- "files_created": len(files),
225
- "source_dir": str(source),
226
- },
227
- indent=2,
228
- )
229
- )
245
+ result: dict[str, Any] = {
246
+ "status": "installed",
247
+ "agent": agent,
248
+ "scope": scope,
249
+ "target_dir": str(target),
250
+ "files_created": len(files),
251
+ "source_dir": str(source),
252
+ }
253
+ click.echo(json.dumps(result, indent=2))
230
254
  else:
231
255
  console.print(
232
256
  Panel(
@@ -248,3 +272,69 @@ def install_agent_command(
248
272
  else:
249
273
  console.print(f"[red]Error:[/] {exc}")
250
274
  raise SystemExit(1) from exc
275
+
276
+
277
+ def _handle_dry_run(
278
+ converter: (
279
+ ClaudeConverter
280
+ | OpenCodeConverter
281
+ | GeminiConverter
282
+ | SkillRuntimeConverter
283
+ | CodexConverter
284
+ ),
285
+ bundle: Any,
286
+ target: Path,
287
+ scope_enum: Scope,
288
+ agent: str,
289
+ scope: str,
290
+ json_output: bool,
291
+ ) -> None:
292
+ """Handle dry-run mode: simulate install in temp dir."""
293
+ import tempfile
294
+
295
+ with tempfile.TemporaryDirectory() as tmp:
296
+ tmp_target = Path(tmp)
297
+ # For Codex, pass tmp as project_root so AGENTS.md lands in tmpdir
298
+ if isinstance(converter, CodexConverter):
299
+ files = converter.install(
300
+ bundle, tmp_target, scope_enum, project_root=Path(tmp)
301
+ )
302
+ else:
303
+ files = converter.install(bundle, tmp_target, scope_enum)
304
+ # Remap paths to real target
305
+ planned: list[Path] = []
306
+ for f in files:
307
+ try:
308
+ planned.append(target / f.relative_to(tmp_target))
309
+ except ValueError:
310
+ # AGENTS.md may be at project_root, not under target
311
+ planned.append(f)
312
+
313
+ if json_output:
314
+ click.echo(
315
+ json.dumps(
316
+ {
317
+ "dry_run": True,
318
+ "agent": agent,
319
+ "scope": scope,
320
+ "target_dir": str(target),
321
+ "files": [str(f) for f in planned],
322
+ "file_count": len(planned),
323
+ },
324
+ indent=2,
325
+ )
326
+ )
327
+ else:
328
+ console.print(
329
+ Panel(
330
+ f"[yellow]Dry run[/] — no files written\n\n"
331
+ f"[bold]Runtime:[/] {agent}\n"
332
+ f"[bold]Scope:[/] {scope}\n"
333
+ f"[bold]Target:[/] {target}\n"
334
+ f"[bold]Files:[/] {len(planned)}",
335
+ title="Install Preview",
336
+ border_style="yellow",
337
+ )
338
+ )
339
+ for f in planned:
340
+ console.print(f" [dim]{f}[/]")
@@ -6,7 +6,9 @@ from agent_brain_cli.runtime.parser import (
6
6
  parse_command,
7
7
  parse_frontmatter,
8
8
  parse_plugin_dir,
9
+ parse_scripts,
9
10
  parse_skill,
11
+ parse_templates,
10
12
  )
11
13
  from agent_brain_cli.runtime.tool_maps import (
12
14
  CLAUDE_TOOLS,
@@ -21,7 +23,9 @@ from agent_brain_cli.runtime.types import (
21
23
  PluginCommand,
22
24
  PluginManifest,
23
25
  PluginParameter,
26
+ PluginScript,
24
27
  PluginSkill,
28
+ PluginTemplate,
25
29
  RuntimeType,
26
30
  Scope,
27
31
  TriggerPattern,
@@ -36,7 +40,9 @@ __all__ = [
36
40
  "PluginCommand",
37
41
  "PluginManifest",
38
42
  "PluginParameter",
43
+ "PluginScript",
39
44
  "PluginSkill",
45
+ "PluginTemplate",
40
46
  "RuntimeConverter",
41
47
  "RuntimeType",
42
48
  "Scope",
@@ -47,5 +53,7 @@ __all__ = [
47
53
  "parse_command",
48
54
  "parse_frontmatter",
49
55
  "parse_plugin_dir",
56
+ "parse_scripts",
50
57
  "parse_skill",
58
+ "parse_templates",
51
59
  ]
@@ -0,0 +1,215 @@
1
+ """Codex runtime converter.
2
+
3
+ Codex is a named preset built on the generic skill-runtime converter.
4
+ It installs to `.codex/skills/agent-brain/` and generates an AGENTS.md
5
+ file at the project root with Agent Brain guidance.
6
+
7
+ Key differences from base SkillRuntimeConverter:
8
+ - Default install dir: .codex/skills/agent-brain/
9
+ - AGENTS.md generated at project root (idempotent via HTML comment markers)
10
+ - Skills include invocation guidance headers
11
+ """
12
+
13
+ import logging
14
+ from pathlib import Path
15
+
16
+ from agent_brain_cli.runtime.skill_runtime_converter import (
17
+ SkillRuntimeConverter,
18
+ )
19
+ from agent_brain_cli.runtime.types import (
20
+ PluginAgent,
21
+ PluginBundle,
22
+ PluginCommand,
23
+ PluginSkill,
24
+ RuntimeType,
25
+ Scope,
26
+ )
27
+
28
+ logger = logging.getLogger(__name__)
29
+
30
+ # HTML comment markers for idempotent AGENTS.md updates
31
+ AGENTS_MD_START = "<!-- agent-brain:start -->"
32
+ AGENTS_MD_END = "<!-- agent-brain:end -->"
33
+
34
+ AGENTS_MD_SECTION = """\
35
+ {start_marker}
36
+
37
+ ## Agent Brain
38
+
39
+ Agent Brain provides semantic search over your codebase and documentation.
40
+
41
+ ### Available Skills
42
+
43
+ | Skill | Description |
44
+ |-------|-------------|
45
+ {skill_table}
46
+
47
+ ### Usage
48
+
49
+ Ask your AI assistant to search documentation or code:
50
+
51
+ - "Search for authentication patterns in my codebase"
52
+ - "Find documentation about the API endpoints"
53
+ - "Look up how error handling works"
54
+
55
+ ### Setup
56
+
57
+ Run `agent-brain start` to start the Agent Brain server, then use
58
+ `agent-brain index ./src` to index your source code.
59
+
60
+ {end_marker}"""
61
+
62
+
63
+ class CodexConverter:
64
+ """Converter for Codex runtime (skill-runtime preset).
65
+
66
+ Delegates skill-directory creation to SkillRuntimeConverter and
67
+ adds Codex-specific AGENTS.md generation.
68
+ """
69
+
70
+ def __init__(self) -> None:
71
+ self._base = SkillRuntimeConverter()
72
+
73
+ @property
74
+ def runtime_type(self) -> RuntimeType:
75
+ return RuntimeType.CODEX
76
+
77
+ def convert_command(self, command: PluginCommand) -> str:
78
+ """Convert command with Codex invocation guidance header."""
79
+ base_content = self._base.convert_command(command)
80
+ return _add_codex_header(base_content, command.name)
81
+
82
+ def convert_agent(self, agent: PluginAgent) -> str:
83
+ """Convert agent with Codex invocation guidance header."""
84
+ base_content = self._base.convert_agent(agent)
85
+ return _add_codex_header(base_content, agent.name)
86
+
87
+ def convert_skill(self, skill: PluginSkill) -> str:
88
+ """Convert skill with Codex invocation guidance header."""
89
+ return self._base.convert_skill(skill)
90
+
91
+ def install(
92
+ self,
93
+ bundle: PluginBundle,
94
+ target_dir: Path,
95
+ scope: Scope,
96
+ project_root: Path | None = None,
97
+ ) -> list[Path]:
98
+ """Install Codex skills and generate AGENTS.md.
99
+
100
+ Delegates skill-directory creation to the base converter,
101
+ then generates/updates AGENTS.md at the project root.
102
+
103
+ Args:
104
+ bundle: Parsed plugin bundle.
105
+ target_dir: Where to write skill directories.
106
+ scope: Project-level or global installation.
107
+ project_root: Project root for AGENTS.md. If None,
108
+ derived from target_dir for project scope.
109
+ """
110
+ # Install skill directories via base converter
111
+ created = self._base.install(bundle, target_dir, scope)
112
+
113
+ # Re-write command and agent SKILL.md files with Codex headers
114
+ for cmd in bundle.commands:
115
+ from agent_brain_cli.runtime.skill_runtime_converter import (
116
+ _skill_dir_name,
117
+ )
118
+
119
+ skill_name = _skill_dir_name(cmd.name)
120
+ skill_file = target_dir / skill_name / "SKILL.md"
121
+ if skill_file.exists():
122
+ skill_file.write_text(self.convert_command(cmd), encoding="utf-8")
123
+
124
+ for agent in bundle.agents:
125
+ from agent_brain_cli.runtime.skill_runtime_converter import (
126
+ _skill_dir_name,
127
+ )
128
+
129
+ skill_name = _skill_dir_name(agent.name)
130
+ skill_file = target_dir / skill_name / "SKILL.md"
131
+ if skill_file.exists():
132
+ skill_file.write_text(self.convert_agent(agent), encoding="utf-8")
133
+
134
+ # Generate AGENTS.md at project root
135
+ if project_root is None:
136
+ if scope == Scope.PROJECT:
137
+ # .codex/skills/agent-brain → project root (3 levels up)
138
+ project_root = target_dir.parent.parent.parent
139
+ else:
140
+ project_root = Path.cwd()
141
+
142
+ agents_md_path = project_root / "AGENTS.md"
143
+ try:
144
+ agents_md_files = _update_agents_md(agents_md_path, bundle)
145
+ created.extend(agents_md_files)
146
+ except OSError as exc:
147
+ logger.warning("Could not write AGENTS.md: %s", exc)
148
+
149
+ return created
150
+
151
+
152
+ def _add_codex_header(content: str, name: str) -> str:
153
+ """Add a Codex invocation guidance header after the frontmatter.
154
+
155
+ Inserts a brief note about how to invoke this skill in Codex.
156
+ """
157
+ # Split on the closing --- of frontmatter
158
+ parts = content.split("---\n", 2)
159
+ if len(parts) < 3:
160
+ return content
161
+
162
+ header = (
163
+ f"> **Codex Skill:** `{name}`\n"
164
+ f"> Invoke by asking about {name} or referencing it directly.\n\n"
165
+ )
166
+ return f"---\n{parts[1]}---\n{header}{parts[2]}"
167
+
168
+
169
+ def _update_agents_md(agents_md_path: Path, bundle: PluginBundle) -> list[Path]:
170
+ """Generate or update AGENTS.md with Agent Brain section.
171
+
172
+ Uses HTML comment markers for idempotent updates — running this
173
+ multiple times will replace the existing section rather than
174
+ duplicating it.
175
+
176
+ Returns list of created/updated paths.
177
+ """
178
+ # Build skill table
179
+ skill_rows: list[str] = []
180
+ for cmd in bundle.commands:
181
+ skill_rows.append(f"| {cmd.name} | {cmd.description} |")
182
+ for agent in bundle.agents:
183
+ skill_rows.append(f"| {agent.name} | {agent.description} |")
184
+ for skill in bundle.skills:
185
+ skill_rows.append(f"| {skill.name} | {skill.description} |")
186
+
187
+ skill_table = "\n".join(skill_rows) if skill_rows else "| (none) | - |"
188
+
189
+ section = AGENTS_MD_SECTION.format(
190
+ start_marker=AGENTS_MD_START,
191
+ end_marker=AGENTS_MD_END,
192
+ skill_table=skill_table,
193
+ )
194
+
195
+ if agents_md_path.exists():
196
+ existing = agents_md_path.read_text(encoding="utf-8")
197
+ if AGENTS_MD_START in existing and AGENTS_MD_END in existing:
198
+ # Replace existing section
199
+ start_idx = existing.index(AGENTS_MD_START)
200
+ end_idx = existing.index(AGENTS_MD_END) + len(AGENTS_MD_END)
201
+ updated = existing[:start_idx] + section + existing[end_idx:]
202
+ agents_md_path.write_text(updated, encoding="utf-8")
203
+ else:
204
+ # Append section
205
+ agents_md_path.write_text(
206
+ existing.rstrip() + "\n\n" + section + "\n",
207
+ encoding="utf-8",
208
+ )
209
+ else:
210
+ # Create new file
211
+ content = f"# AGENTS.md\n\n{section}\n"
212
+ agents_md_path.parent.mkdir(parents=True, exist_ok=True)
213
+ agents_md_path.write_text(content, encoding="utf-8")
214
+
215
+ return [agents_md_path]
@@ -13,7 +13,9 @@ from agent_brain_cli.runtime.types import (
13
13
  PluginCommand,
14
14
  PluginManifest,
15
15
  PluginParameter,
16
+ PluginScript,
16
17
  PluginSkill,
18
+ PluginTemplate,
17
19
  TriggerPattern,
18
20
  )
19
21
 
@@ -195,6 +197,62 @@ def parse_manifest(path: Path) -> PluginManifest:
195
197
  )
196
198
 
197
199
 
200
+ def parse_templates(templates_dir: Path) -> list[PluginTemplate]:
201
+ """Parse template files from the templates/ directory.
202
+
203
+ Args:
204
+ templates_dir: Path to the templates directory.
205
+
206
+ Returns:
207
+ List of parsed PluginTemplate objects.
208
+ """
209
+ templates: list[PluginTemplate] = []
210
+ if not templates_dir.is_dir():
211
+ return templates
212
+ for tpl_file in sorted(templates_dir.iterdir()):
213
+ if tpl_file.is_file():
214
+ try:
215
+ content = tpl_file.read_text(encoding="utf-8")
216
+ templates.append(
217
+ PluginTemplate(
218
+ name=tpl_file.name,
219
+ content=content,
220
+ source_path=str(tpl_file),
221
+ )
222
+ )
223
+ except OSError as exc:
224
+ logger.warning("Failed to read template %s: %s", tpl_file, exc)
225
+ return templates
226
+
227
+
228
+ def parse_scripts(scripts_dir: Path) -> list[PluginScript]:
229
+ """Parse script files from the scripts/ directory.
230
+
231
+ Args:
232
+ scripts_dir: Path to the scripts directory.
233
+
234
+ Returns:
235
+ List of parsed PluginScript objects.
236
+ """
237
+ scripts: list[PluginScript] = []
238
+ if not scripts_dir.is_dir():
239
+ return scripts
240
+ for script_file in sorted(scripts_dir.iterdir()):
241
+ if script_file.is_file():
242
+ try:
243
+ content = script_file.read_text(encoding="utf-8")
244
+ scripts.append(
245
+ PluginScript(
246
+ name=script_file.name,
247
+ content=content,
248
+ source_path=str(script_file),
249
+ )
250
+ )
251
+ except OSError as exc:
252
+ logger.warning("Failed to read script %s: %s", script_file, exc)
253
+ return scripts
254
+
255
+
198
256
  def parse_plugin_dir(plugin_dir: Path) -> PluginBundle:
199
257
  """Parse an entire plugin directory into a PluginBundle.
200
258
 
@@ -255,10 +313,18 @@ def parse_plugin_dir(plugin_dir: Path) -> PluginBundle:
255
313
  except (ValueError, OSError) as exc:
256
314
  logger.warning("Failed to parse skill %s: %s", skill_file, exc)
257
315
 
316
+ # Parse templates
317
+ templates = parse_templates(plugin_dir / "templates")
318
+
319
+ # Parse scripts
320
+ scripts = parse_scripts(plugin_dir / "scripts")
321
+
258
322
  return PluginBundle(
259
323
  commands=commands,
260
324
  agents=agents,
261
325
  skills=skills,
326
+ templates=templates,
327
+ scripts=scripts,
262
328
  manifest=manifest,
263
329
  source_dir=str(plugin_dir),
264
330
  )
@@ -0,0 +1,234 @@
1
+ """Generic skill-runtime converter.
2
+
3
+ Transforms all plugin artifacts (commands, agents, skills, templates, scripts)
4
+ into flat skill directories. Each artifact becomes a directory with a SKILL.md
5
+ file. This converter supports any runtime that uses skill directories (Codex,
6
+ Qwen, Cursor, etc.) via the --dir option.
7
+
8
+ Transformation rules:
9
+ command agent-brain-init.md → <dir>/agent-brain-init/SKILL.md
10
+ agent research-assistant.md → <dir>/agent-brain-research/SKILL.md
11
+ skill using-agent-brain/ → <dir>/agent-brain-using/SKILL.md + references/
12
+ templates/* → <dir>/agent-brain-setup/assets/
13
+ scripts/* → <dir>/agent-brain-verify/scripts/
14
+ """
15
+
16
+ import logging
17
+ import shutil
18
+ from pathlib import Path
19
+
20
+ import yaml
21
+
22
+ from agent_brain_cli.runtime.types import (
23
+ PluginAgent,
24
+ PluginBundle,
25
+ PluginCommand,
26
+ PluginSkill,
27
+ RuntimeType,
28
+ Scope,
29
+ )
30
+
31
+ logger = logging.getLogger(__name__)
32
+
33
+ LEGACY_PATH = ".claude/agent-brain"
34
+ NEW_PATH = ".agent-brain"
35
+
36
+
37
+ def _replace_paths(text: str) -> str:
38
+ """Replace legacy state dir paths with new runtime-neutral paths."""
39
+ return text.replace(LEGACY_PATH, NEW_PATH)
40
+
41
+
42
+ def _build_skill_md(frontmatter: dict, body: str) -> str: # type: ignore[type-arg]
43
+ """Build a SKILL.md file from frontmatter and body."""
44
+ yaml_str = yaml.dump(frontmatter, default_flow_style=False, sort_keys=False)
45
+ return f"---\n{yaml_str}---\n{body}\n"
46
+
47
+
48
+ def _skill_dir_name(name: str, prefix: str = "agent-brain-") -> str:
49
+ """Derive a skill directory name from an artifact name.
50
+
51
+ Ensures the name starts with the agent-brain- prefix for namespacing.
52
+ """
53
+ if name.startswith(prefix):
54
+ return name
55
+ return f"{prefix}{name}"
56
+
57
+
58
+ class SkillRuntimeConverter:
59
+ """Converter that flattens all plugin artifacts into skill directories.
60
+
61
+ Commands become skills with their body as instructions.
62
+ Agents become orchestration skills referencing dependent skills.
63
+ Existing skills are copied with references intact.
64
+ Templates and scripts become asset skill directories.
65
+ """
66
+
67
+ @property
68
+ def runtime_type(self) -> RuntimeType:
69
+ return RuntimeType.SKILL_RUNTIME
70
+
71
+ def convert_command(self, command: PluginCommand) -> str:
72
+ """Convert a command into a SKILL.md file.
73
+
74
+ The command body becomes the skill instructions, with
75
+ allowed-tools set to common tools needed for CLI commands.
76
+ """
77
+ fm: dict[str, object] = {
78
+ "name": command.name,
79
+ "description": command.description,
80
+ "allowed-tools": ["Bash", "Read", "Write"],
81
+ }
82
+ if command.parameters:
83
+ params_text = "\n\n## Parameters\n\n"
84
+ for p in command.parameters:
85
+ req = " (required)" if p.required else ""
86
+ default = f" [default: {p.default}]" if p.default else ""
87
+ params_text += f"- **{p.name}**: {p.description}{req}{default}\n"
88
+ body = _replace_paths(command.body) + params_text
89
+ else:
90
+ body = _replace_paths(command.body)
91
+ return _build_skill_md(fm, body)
92
+
93
+ def convert_agent(self, agent: PluginAgent) -> str:
94
+ """Convert an agent into an orchestration SKILL.md file.
95
+
96
+ The agent body becomes skill instructions with a note that
97
+ this is an orchestration skill.
98
+ """
99
+ fm: dict[str, object] = {
100
+ "name": agent.name,
101
+ "description": agent.description,
102
+ "allowed-tools": ["Bash", "Read", "Write", "Grep", "Glob"],
103
+ }
104
+ header = "<!-- Orchestration skill converted from agent -->\n\n"
105
+ if agent.skills:
106
+ header += "## Related Skills\n\n"
107
+ for skill_name in agent.skills:
108
+ header += f"- {skill_name}\n"
109
+ header += "\n"
110
+ body = header + _replace_paths(agent.body)
111
+ return _build_skill_md(fm, body)
112
+
113
+ def convert_skill(self, skill: PluginSkill) -> str:
114
+ """Convert a skill, preserving its existing format."""
115
+ fm: dict[str, object] = {
116
+ "name": skill.name,
117
+ "description": skill.description,
118
+ "allowed-tools": skill.allowed_tools,
119
+ }
120
+ if skill.license:
121
+ fm["license"] = skill.license
122
+ if skill.metadata:
123
+ fm["metadata"] = skill.metadata
124
+ return _build_skill_md(fm, _replace_paths(skill.body))
125
+
126
+ def install(
127
+ self,
128
+ bundle: PluginBundle,
129
+ target_dir: Path,
130
+ scope: Scope,
131
+ ) -> list[Path]:
132
+ """Install all plugin artifacts as flat skill directories.
133
+
134
+ Each artifact gets its own directory under target_dir with a
135
+ SKILL.md file. References, templates, and scripts are included
136
+ as assets.
137
+ """
138
+ created: list[Path] = []
139
+
140
+ # Commands → skill directories
141
+ for cmd in bundle.commands:
142
+ skill_name = _skill_dir_name(cmd.name)
143
+ skill_dir = target_dir / skill_name
144
+ skill_dir.mkdir(parents=True, exist_ok=True)
145
+ skill_file = skill_dir / "SKILL.md"
146
+ skill_file.write_text(self.convert_command(cmd), encoding="utf-8")
147
+ created.append(skill_file)
148
+
149
+ # Agents → orchestration skill directories
150
+ for agent in bundle.agents:
151
+ skill_name = _skill_dir_name(agent.name)
152
+ skill_dir = target_dir / skill_name
153
+ skill_dir.mkdir(parents=True, exist_ok=True)
154
+ skill_file = skill_dir / "SKILL.md"
155
+ skill_file.write_text(self.convert_agent(agent), encoding="utf-8")
156
+ created.append(skill_file)
157
+
158
+ # Skills → skill directories (with references)
159
+ for skill in bundle.skills:
160
+ skill_name = _skill_dir_name(skill.name)
161
+ skill_dir = target_dir / skill_name
162
+ skill_dir.mkdir(parents=True, exist_ok=True)
163
+ skill_file = skill_dir / "SKILL.md"
164
+ skill_file.write_text(self.convert_skill(skill), encoding="utf-8")
165
+ created.append(skill_file)
166
+
167
+ # Copy references if they exist
168
+ if skill.source_path:
169
+ refs_src = Path(skill.source_path).parent / "references"
170
+ if refs_src.is_dir():
171
+ refs_dest = skill_dir / "references"
172
+ if refs_dest.exists():
173
+ shutil.rmtree(refs_dest)
174
+ shutil.copytree(refs_src, refs_dest)
175
+ for ref in refs_dest.rglob("*"):
176
+ if ref.is_file():
177
+ created.append(ref)
178
+
179
+ # Templates → agent-brain-setup/assets/
180
+ if bundle.templates:
181
+ setup_dir = target_dir / "agent-brain-setup"
182
+ assets_dir = setup_dir / "assets"
183
+ assets_dir.mkdir(parents=True, exist_ok=True)
184
+ # Create a setup SKILL.md if it doesn't exist
185
+ setup_skill = setup_dir / "SKILL.md"
186
+ if not setup_skill.exists():
187
+ fm: dict[str, object] = {
188
+ "name": "agent-brain-setup",
189
+ "description": "Agent Brain setup templates and configuration",
190
+ "allowed-tools": ["Bash", "Read", "Write"],
191
+ }
192
+ body = (
193
+ "This skill contains setup templates for Agent Brain.\n\n"
194
+ "## Assets\n\n"
195
+ )
196
+ for tpl in bundle.templates:
197
+ body += f"- `assets/{tpl.name}`\n"
198
+ setup_skill.write_text(_build_skill_md(fm, body), encoding="utf-8")
199
+ created.append(setup_skill)
200
+
201
+ for tpl in bundle.templates:
202
+ tpl_file = assets_dir / tpl.name
203
+ tpl_file.write_text(tpl.content, encoding="utf-8")
204
+ created.append(tpl_file)
205
+
206
+ # Scripts → agent-brain-verify/scripts/
207
+ if bundle.scripts:
208
+ verify_dir = target_dir / "agent-brain-verify"
209
+ scripts_dir = verify_dir / "scripts"
210
+ scripts_dir.mkdir(parents=True, exist_ok=True)
211
+ # Create a verify SKILL.md if it doesn't exist
212
+ verify_skill = verify_dir / "SKILL.md"
213
+ if not verify_skill.exists():
214
+ fm_v: dict[str, object] = {
215
+ "name": "agent-brain-verify",
216
+ "description": "Agent Brain verification and health check scripts",
217
+ "allowed-tools": ["Bash", "Read"],
218
+ }
219
+ body_v = (
220
+ "This skill contains verification scripts for "
221
+ "Agent Brain.\n\n"
222
+ "## Scripts\n\n"
223
+ )
224
+ for script in bundle.scripts:
225
+ body_v += f"- `scripts/{script.name}`\n"
226
+ verify_skill.write_text(_build_skill_md(fm_v, body_v), encoding="utf-8")
227
+ created.append(verify_skill)
228
+
229
+ for script in bundle.scripts:
230
+ script_file = scripts_dir / script.name
231
+ script_file.write_text(script.content, encoding="utf-8")
232
+ created.append(script_file)
233
+
234
+ return created
@@ -10,6 +10,8 @@ class RuntimeType(str, Enum):
10
10
  CLAUDE = "claude"
11
11
  OPENCODE = "opencode"
12
12
  GEMINI = "gemini"
13
+ SKILL_RUNTIME = "skill-runtime"
14
+ CODEX = "codex"
13
15
 
14
16
 
15
17
  class Scope(str, Enum):
@@ -75,6 +77,24 @@ class PluginSkill:
75
77
  references: list[str] = field(default_factory=list)
76
78
 
77
79
 
80
+ @dataclass
81
+ class PluginTemplate:
82
+ """A template file from the plugin templates/ directory."""
83
+
84
+ name: str
85
+ content: str
86
+ source_path: str = ""
87
+
88
+
89
+ @dataclass
90
+ class PluginScript:
91
+ """A script file from the plugin scripts/ directory."""
92
+
93
+ name: str
94
+ content: str
95
+ source_path: str = ""
96
+
97
+
78
98
  @dataclass
79
99
  class PluginManifest:
80
100
  """Parsed plugin.json manifest."""
@@ -96,5 +116,7 @@ class PluginBundle:
96
116
  commands: list[PluginCommand] = field(default_factory=list)
97
117
  agents: list[PluginAgent] = field(default_factory=list)
98
118
  skills: list[PluginSkill] = field(default_factory=list)
119
+ templates: list[PluginTemplate] = field(default_factory=list)
120
+ scripts: list[PluginScript] = field(default_factory=list)
99
121
  manifest: PluginManifest = field(default_factory=PluginManifest)
100
122
  source_dir: str = ""
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "agent-brain-cli"
3
- version = "9.0.0"
3
+ version = "9.3.0"
4
4
  description = "Agent Brain CLI - Command-line interface for managing AI agent memory and knowledge retrieval"
5
5
  authors = ["Spillwave Solutions"]
6
6
  readme = "README.md"
@@ -27,7 +27,7 @@ httpx = "^0.28.0"
27
27
  rich = "^13.9.0"
28
28
  pyyaml = "^6.0.0"
29
29
  pydantic = "^2.10.0"
30
- agent-brain-rag = "^9.0.0"
30
+ agent-brain-rag = "^9.3.0"
31
31
 
32
32
  [tool.poetry.group.dev.dependencies]
33
33
  pytest = "^8.3.0"