axion-code 1.0.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.
Files changed (82) hide show
  1. axion/__init__.py +3 -0
  2. axion/api/__init__.py +0 -0
  3. axion/api/anthropic.py +460 -0
  4. axion/api/client.py +259 -0
  5. axion/api/error.py +161 -0
  6. axion/api/ollama.py +597 -0
  7. axion/api/openai_compat.py +805 -0
  8. axion/api/openai_responses.py +627 -0
  9. axion/api/prompt_cache.py +31 -0
  10. axion/api/sse.py +98 -0
  11. axion/api/types.py +451 -0
  12. axion/cli/__init__.py +0 -0
  13. axion/cli/init_cmd.py +50 -0
  14. axion/cli/input.py +290 -0
  15. axion/cli/main.py +2953 -0
  16. axion/cli/render.py +489 -0
  17. axion/cli/tui.py +766 -0
  18. axion/commands/__init__.py +0 -0
  19. axion/commands/handlers/__init__.py +0 -0
  20. axion/commands/handlers/agents.py +51 -0
  21. axion/commands/handlers/builtin_commands.py +367 -0
  22. axion/commands/handlers/mcp.py +59 -0
  23. axion/commands/handlers/models.py +75 -0
  24. axion/commands/handlers/plugins.py +55 -0
  25. axion/commands/handlers/skills.py +61 -0
  26. axion/commands/parsing.py +317 -0
  27. axion/commands/registry.py +166 -0
  28. axion/compat_harness/__init__.py +0 -0
  29. axion/compat_harness/extractor.py +145 -0
  30. axion/plugins/__init__.py +0 -0
  31. axion/plugins/hooks.py +22 -0
  32. axion/plugins/manager.py +391 -0
  33. axion/plugins/manifest.py +270 -0
  34. axion/runtime/__init__.py +0 -0
  35. axion/runtime/bash.py +388 -0
  36. axion/runtime/bootstrap.py +39 -0
  37. axion/runtime/claude_subscription.py +300 -0
  38. axion/runtime/compact.py +233 -0
  39. axion/runtime/config.py +397 -0
  40. axion/runtime/conversation.py +1073 -0
  41. axion/runtime/file_ops.py +613 -0
  42. axion/runtime/git.py +213 -0
  43. axion/runtime/hooks.py +235 -0
  44. axion/runtime/image.py +212 -0
  45. axion/runtime/lanes.py +282 -0
  46. axion/runtime/lsp.py +425 -0
  47. axion/runtime/mcp/__init__.py +0 -0
  48. axion/runtime/mcp/client.py +76 -0
  49. axion/runtime/mcp/lifecycle.py +96 -0
  50. axion/runtime/mcp/stdio.py +318 -0
  51. axion/runtime/mcp/tool_bridge.py +79 -0
  52. axion/runtime/memory.py +196 -0
  53. axion/runtime/oauth.py +329 -0
  54. axion/runtime/openai_subscription.py +346 -0
  55. axion/runtime/permissions.py +247 -0
  56. axion/runtime/plan_mode.py +96 -0
  57. axion/runtime/policy_engine.py +259 -0
  58. axion/runtime/prompt.py +586 -0
  59. axion/runtime/recovery.py +261 -0
  60. axion/runtime/remote.py +28 -0
  61. axion/runtime/sandbox.py +68 -0
  62. axion/runtime/scheduler.py +231 -0
  63. axion/runtime/session.py +365 -0
  64. axion/runtime/sharing.py +159 -0
  65. axion/runtime/skills.py +124 -0
  66. axion/runtime/tasks.py +258 -0
  67. axion/runtime/usage.py +241 -0
  68. axion/runtime/workers.py +186 -0
  69. axion/telemetry/__init__.py +0 -0
  70. axion/telemetry/events.py +67 -0
  71. axion/telemetry/profile.py +49 -0
  72. axion/telemetry/sink.py +60 -0
  73. axion/telemetry/tracer.py +95 -0
  74. axion/tools/__init__.py +0 -0
  75. axion/tools/lane_completion.py +33 -0
  76. axion/tools/registry.py +853 -0
  77. axion/tools/tool_search.py +226 -0
  78. axion_code-1.0.0.dist-info/METADATA +709 -0
  79. axion_code-1.0.0.dist-info/RECORD +82 -0
  80. axion_code-1.0.0.dist-info/WHEEL +4 -0
  81. axion_code-1.0.0.dist-info/entry_points.txt +2 -0
  82. axion_code-1.0.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,317 @@
1
+ """Slash command parsing with full variant matching and typo-tolerant suggestions.
2
+
3
+ Maps to: rust/crates/commands/src/lib.rs (parsing, SlashCommand enum)
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from dataclasses import dataclass, field
9
+ from typing import Any
10
+
11
+ from axion.commands.registry import CommandRegistry, SlashCommandSpec, get_command_registry
12
+
13
+ # ---------------------------------------------------------------------------
14
+ # Parsed command variants
15
+ # ---------------------------------------------------------------------------
16
+
17
+ @dataclass
18
+ class ParsedCommand:
19
+ """Result of parsing a slash command input."""
20
+
21
+ name: str
22
+ args: str = ""
23
+ spec: SlashCommandSpec | None = None
24
+ # Structured arguments for commands with specific parsing
25
+ parsed_args: dict[str, Any] = field(default_factory=dict)
26
+
27
+
28
+ @dataclass
29
+ class CommandParseError:
30
+ """Error when a slash command cannot be parsed."""
31
+
32
+ input_text: str
33
+ message: str
34
+ suggestions: list[str] = field(default_factory=list)
35
+
36
+
37
+ # ---------------------------------------------------------------------------
38
+ # Argument parsers for specific commands
39
+ # ---------------------------------------------------------------------------
40
+
41
+ VALID_PERMISSION_MODES = {"read-only", "workspace-write", "danger-full-access", "prompt", "allow"}
42
+
43
+
44
+ def _parse_model_args(args: str) -> dict[str, Any]:
45
+ """Parse /model [name] arguments."""
46
+ if not args.strip():
47
+ return {"action": "show"}
48
+ return {"action": "set", "model": args.strip()}
49
+
50
+
51
+ def _parse_permissions_args(args: str) -> dict[str, Any]:
52
+ """Parse /permissions [mode] arguments."""
53
+ mode = args.strip().lower()
54
+ if not mode:
55
+ return {"action": "show"}
56
+ if mode not in VALID_PERMISSION_MODES:
57
+ return {"action": "invalid", "mode": mode, "valid": sorted(VALID_PERMISSION_MODES)}
58
+ return {"action": "set", "mode": mode}
59
+
60
+
61
+ def _parse_session_args(args: str) -> dict[str, Any]:
62
+ """Parse /session [action] [target] arguments."""
63
+ parts = args.strip().split(maxsplit=1)
64
+ action = parts[0].lower() if parts else "show"
65
+ target = parts[1].strip() if len(parts) > 1 else ""
66
+
67
+ if action in ("list", "ls"):
68
+ return {"action": "list"}
69
+ if action in ("show", "info"):
70
+ return {"action": "show", "target": target}
71
+ if action == "fork":
72
+ return {"action": "fork", "branch_name": target or None}
73
+ if action == "switch":
74
+ return {"action": "switch", "target": target}
75
+ if action in ("delete", "rm"):
76
+ return {"action": "delete", "target": target}
77
+ # Default: show current
78
+ return {"action": "show"}
79
+
80
+
81
+ def _parse_mcp_args(args: str) -> dict[str, Any]:
82
+ """Parse /mcp [action] [target] arguments."""
83
+ parts = args.strip().split(maxsplit=1)
84
+ action = parts[0].lower() if parts else "list"
85
+ target = parts[1].strip() if len(parts) > 1 else ""
86
+
87
+ if action in ("list", "ls", ""):
88
+ return {"action": "list"}
89
+ if action == "show":
90
+ return {"action": "show", "server": target}
91
+ if action == "help":
92
+ return {"action": "help"}
93
+ return {"action": action, "target": target}
94
+
95
+
96
+ def _parse_plugins_args(args: str) -> dict[str, Any]:
97
+ """Parse /plugins [action] [target] arguments."""
98
+ parts = args.strip().split(maxsplit=1)
99
+ action = parts[0].lower() if parts else "list"
100
+ target = parts[1].strip() if len(parts) > 1 else ""
101
+
102
+ return {"action": action, "target": target}
103
+
104
+
105
+ def _parse_effort_args(args: str) -> dict[str, Any]:
106
+ """Parse /effort [level] arguments."""
107
+ level = args.strip().lower()
108
+ if not level:
109
+ return {"action": "show"}
110
+ if level in ("low", "medium", "high"):
111
+ return {"action": "set", "level": level}
112
+ return {"action": "invalid", "level": level}
113
+
114
+
115
+ def _parse_output_style_args(args: str) -> dict[str, Any]:
116
+ """Parse /output-style [style] arguments."""
117
+ style = args.strip().lower()
118
+ if not style:
119
+ return {"action": "show"}
120
+ if style in ("brief", "verbose", "default"):
121
+ return {"action": "set", "style": style}
122
+ return {"action": "invalid", "style": style}
123
+
124
+
125
+ # Command-specific parsers
126
+ _ARGUMENT_PARSERS: dict[str, Any] = {
127
+ "model": _parse_model_args,
128
+ "permissions": _parse_permissions_args,
129
+ "session": _parse_session_args,
130
+ "mcp": _parse_mcp_args,
131
+ "plugins": _parse_plugins_args,
132
+ "effort": _parse_effort_args,
133
+ "output-style": _parse_output_style_args,
134
+ }
135
+
136
+
137
+ # ---------------------------------------------------------------------------
138
+ # Main parsing function
139
+ # ---------------------------------------------------------------------------
140
+
141
+ def parse_slash_command(
142
+ input_text: str,
143
+ registry: CommandRegistry | None = None,
144
+ ) -> ParsedCommand | CommandParseError:
145
+ """Parse a slash command from user input.
146
+
147
+ Returns ParsedCommand on success, CommandParseError on failure.
148
+ Handles alias resolution, argument parsing, and fuzzy suggestions.
149
+ """
150
+ reg = registry or get_command_registry()
151
+
152
+ stripped = input_text.strip()
153
+ if not stripped.startswith("/"):
154
+ return CommandParseError(
155
+ input_text=stripped,
156
+ message="Not a slash command",
157
+ suggestions=[],
158
+ )
159
+
160
+ # Split command name and args
161
+ parts = stripped[1:].split(maxsplit=1)
162
+ cmd_name = parts[0].lower() if parts else ""
163
+ cmd_args = parts[1] if len(parts) > 1 else ""
164
+
165
+ if not cmd_name:
166
+ return CommandParseError(
167
+ input_text=stripped,
168
+ message="Empty command",
169
+ suggestions=["/help"],
170
+ )
171
+
172
+ # Look up in registry
173
+ spec = reg.get(cmd_name)
174
+ if spec is not None:
175
+ # Parse arguments if there's a specific parser
176
+ parsed_args: dict[str, Any] = {}
177
+ parser = _ARGUMENT_PARSERS.get(spec.name)
178
+ if parser:
179
+ parsed_args = parser(cmd_args)
180
+
181
+ return ParsedCommand(
182
+ name=spec.name,
183
+ args=cmd_args,
184
+ spec=spec,
185
+ parsed_args=parsed_args,
186
+ )
187
+
188
+ # Not found — suggest similar commands
189
+ suggestions = suggest_commands(cmd_name, reg, limit=3)
190
+ return CommandParseError(
191
+ input_text=stripped,
192
+ message=f"Unknown command: /{cmd_name}",
193
+ suggestions=suggestions,
194
+ )
195
+
196
+
197
+ # ---------------------------------------------------------------------------
198
+ # Fuzzy suggestions (Levenshtein distance)
199
+ # ---------------------------------------------------------------------------
200
+
201
+ def suggest_commands(
202
+ input_name: str,
203
+ registry: CommandRegistry | None = None,
204
+ limit: int = 3,
205
+ ) -> list[str]:
206
+ """Suggest similar commands using Levenshtein distance."""
207
+ reg = registry or get_command_registry()
208
+ all_names = [s.name for s in reg.all_specs()]
209
+
210
+ try:
211
+ from rapidfuzz import fuzz
212
+
213
+ scored = [(name, fuzz.ratio(input_name, name)) for name in all_names]
214
+ scored.sort(key=lambda x: x[1], reverse=True)
215
+ return [f"/{name}" for name, score in scored[:limit] if score > 40]
216
+ except ImportError:
217
+ # Fallback: simple prefix + substring matching
218
+ results: list[str] = []
219
+ for name in all_names:
220
+ if name.startswith(input_name[:2]) or input_name in name:
221
+ results.append(f"/{name}")
222
+ if len(results) >= limit:
223
+ break
224
+ return results
225
+
226
+
227
+ # ---------------------------------------------------------------------------
228
+ # Help rendering
229
+ # ---------------------------------------------------------------------------
230
+
231
+ _CATEGORY_LABELS: dict[str, str] = {
232
+ "core": "Core Commands",
233
+ "model": "Model & Permissions",
234
+ "session": "Session Management",
235
+ "config": "Configuration",
236
+ "tools": "Tool Management",
237
+ "info": "Info & Diagnostics",
238
+ "auth": "Authentication",
239
+ "workflow": "Task & Workflow",
240
+ "advanced": "Advanced",
241
+ "misc": "Miscellaneous",
242
+ "plugin": "Plugin Commands",
243
+ "skill": "Skills",
244
+ }
245
+
246
+ _CATEGORY_ORDER = [
247
+ "core", "model", "session", "config", "tools",
248
+ "info", "auth", "workflow", "utility",
249
+ ]
250
+
251
+
252
+ def render_help(
253
+ registry: CommandRegistry | None = None,
254
+ ) -> str:
255
+ """Render categorized help text for all available slash commands."""
256
+ reg = registry or get_command_registry()
257
+ lines: list[str] = ["Available commands:", ""]
258
+
259
+ rendered_categories: set[str] = set()
260
+ for cat in _CATEGORY_ORDER:
261
+ specs = reg.by_category(cat)
262
+ if not specs:
263
+ continue
264
+
265
+ rendered_categories.add(cat)
266
+ label = _CATEGORY_LABELS.get(cat, cat.title())
267
+ lines.append(f" {label}:")
268
+
269
+ for spec in sorted(specs, key=lambda s: s.name):
270
+ hint = f" {spec.argument_hint}" if spec.argument_hint else ""
271
+ aliases = ""
272
+ if spec.aliases:
273
+ aliases = f" (aliases: {', '.join('/' + a for a in spec.aliases)})"
274
+ lines.append(f" /{spec.name}{hint} — {spec.summary}{aliases}")
275
+
276
+ lines.append("")
277
+
278
+ # Remaining categories
279
+ for cat in sorted(reg.categories()):
280
+ if cat in rendered_categories:
281
+ continue
282
+ specs = reg.by_category(cat)
283
+ if not specs:
284
+ continue
285
+ label = _CATEGORY_LABELS.get(cat, cat.title())
286
+ lines.append(f" {label}:")
287
+ for spec in sorted(specs, key=lambda s: s.name):
288
+ hint = f" {spec.argument_hint}" if spec.argument_hint else ""
289
+ lines.append(f" /{spec.name}{hint} — {spec.summary}")
290
+ lines.append("")
291
+
292
+ return "\n".join(lines)
293
+
294
+
295
+ def render_help_detail(
296
+ command_name: str,
297
+ registry: CommandRegistry | None = None,
298
+ ) -> str | None:
299
+ """Render detailed help for a specific command."""
300
+ reg = registry or get_command_registry()
301
+ spec = reg.get(command_name)
302
+ if spec is None:
303
+ return None
304
+
305
+ lines = [
306
+ f"/{spec.name}",
307
+ f" {spec.summary}",
308
+ ]
309
+ if spec.argument_hint:
310
+ lines.append(f" Usage: /{spec.name} {spec.argument_hint}")
311
+ if spec.aliases:
312
+ lines.append(f" Aliases: {', '.join('/' + a for a in spec.aliases)}")
313
+ if spec.resume_supported:
314
+ lines.append(" Supports --resume mode")
315
+ lines.append(f" Category: {spec.category}")
316
+
317
+ return "\n".join(lines)
@@ -0,0 +1,166 @@
1
+ """Slash command registry — ONLY commands that actually have handlers.
2
+
3
+ No fake commands. If it shows in /help, it works.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import enum
9
+ from dataclasses import dataclass, field
10
+
11
+
12
+ @dataclass
13
+ class SlashCommandSpec:
14
+ """Metadata for a slash command."""
15
+
16
+ name: str
17
+ aliases: list[str] = field(default_factory=list)
18
+ summary: str = ""
19
+ argument_hint: str | None = None
20
+ resume_supported: bool = False
21
+ source: str = "builtin"
22
+ category: str = "general"
23
+
24
+
25
+ class CommandSource(enum.Enum):
26
+ BUILTIN = "builtin"
27
+ PLUGIN = "plugin"
28
+ SKILL = "skill"
29
+
30
+
31
+ @dataclass
32
+ class CommandManifestEntry:
33
+ name: str
34
+ source: CommandSource = CommandSource.BUILTIN
35
+
36
+
37
+ # ---------------------------------------------------------------------------
38
+ # ONLY commands that have working handlers
39
+ # ---------------------------------------------------------------------------
40
+
41
+ SLASH_COMMAND_SPECS: list[SlashCommandSpec] = [
42
+ # -- Core --
43
+ SlashCommandSpec(name="help", summary="Show available commands", argument_hint="[command]", category="core"),
44
+ SlashCommandSpec(name="quit", aliases=["exit", "q"], summary="Exit the REPL", category="core"),
45
+ SlashCommandSpec(name="clear", summary="Clear conversation history", category="core"),
46
+ SlashCommandSpec(name="compact", summary="Compact session history to reduce tokens", category="core"),
47
+ SlashCommandSpec(name="cost", summary="Show token usage and costs", category="core"),
48
+ SlashCommandSpec(name="status", summary="Show session status", category="core"),
49
+
50
+ # -- Model --
51
+ SlashCommandSpec(name="model", summary="Show or switch AI model", argument_hint="[opus|sonnet|haiku|gpt-4o|grok-2|llama3.1]", category="model"),
52
+ SlashCommandSpec(name="models", summary="List available Ollama models", category="model"),
53
+ SlashCommandSpec(name="permissions", summary="Show or change permission mode", argument_hint="[allow|prompt|read-only]", category="model"),
54
+
55
+ # -- Session --
56
+ SlashCommandSpec(name="session", summary="Manage sessions (list/switch/new/fork/delete)", argument_hint="[list|switch|new|fork|delete]", category="session"),
57
+ SlashCommandSpec(name="resume", summary="Resume a previous session", argument_hint="[session_id|latest]", category="session"),
58
+ SlashCommandSpec(name="export", summary="Export transcript to markdown", argument_hint="[filename]", category="session"),
59
+ SlashCommandSpec(name="share", summary="Share session with teammates", argument_hint="[file|import <path>]", category="session"),
60
+
61
+ # -- Config & Setup --
62
+ SlashCommandSpec(name="config", summary="Show loaded configuration", category="config"),
63
+ SlashCommandSpec(name="init", summary="Create AXION.md for your project", category="config"),
64
+ SlashCommandSpec(name="sandbox", summary="Show sandbox status", category="config"),
65
+
66
+ # -- Tools & Plugins --
67
+ SlashCommandSpec(name="mcp", summary="Manage MCP servers", argument_hint="[list|show|help]", category="tools"),
68
+ SlashCommandSpec(name="plugins", summary="Manage plugins", argument_hint="[list|install|enable|disable]", category="tools"),
69
+ SlashCommandSpec(name="agents", summary="List available agents", category="tools"),
70
+ SlashCommandSpec(name="skills", summary="List available skills", category="tools"),
71
+
72
+ # -- Info --
73
+ SlashCommandSpec(name="version", summary="Show version", category="info"),
74
+ SlashCommandSpec(name="doctor", summary="Run health checks", category="info"),
75
+ SlashCommandSpec(name="memory", summary="Show project memory", category="info"),
76
+ SlashCommandSpec(name="diff", summary="Show git changes with syntax highlighting", category="info"),
77
+
78
+ # -- Auth --
79
+ SlashCommandSpec(name="login", summary="Save API key (axion login --provider openai)", category="auth"),
80
+ SlashCommandSpec(name="logout", summary="Remove saved credentials", category="auth"),
81
+ SlashCommandSpec(name="auth-mode", aliases=["auth"], summary="Show or switch auth (subscription/api)", argument_hint="[subscription|api|status]", category="auth"),
82
+
83
+ # -- Workflow --
84
+ SlashCommandSpec(name="plan", summary="Plan before coding (read-only exploration)", argument_hint="<task>", category="workflow"),
85
+ SlashCommandSpec(name="commit", summary="Auto-commit with AI message", argument_hint="[message]", category="workflow"),
86
+ SlashCommandSpec(name="undo", summary="Revert last change (git reset)", argument_hint="[hard|file.py]", category="workflow"),
87
+ SlashCommandSpec(name="review", summary="AI code review of recent changes", argument_hint="[file|HEAD~N]", category="workflow"),
88
+ SlashCommandSpec(name="test", summary="Generate tests for a file", argument_hint="<file> [pytest|jest]", category="workflow"),
89
+ SlashCommandSpec(name="init-project", aliases=["scaffold"], summary="Scaffold project from template", argument_hint="[react|django|fastapi|express|cli]", category="workflow"),
90
+ SlashCommandSpec(name="security-review", summary="AI security audit of code", argument_hint="[file]", category="workflow"),
91
+
92
+ # -- Media --
93
+ SlashCommandSpec(name="image", summary="Paste image from clipboard or file path", argument_hint="[path]", category="media"),
94
+
95
+ # -- Utility --
96
+ SlashCommandSpec(name="context", summary="Show context window usage (tokens/capacity)", category="utility"),
97
+ SlashCommandSpec(name="branch", summary="Show or switch git branch", argument_hint="[branch_name]", category="utility"),
98
+ SlashCommandSpec(name="hooks", summary="Show configured hooks", category="utility"),
99
+ SlashCommandSpec(name="copy", summary="Copy last response to clipboard", category="utility"),
100
+ SlashCommandSpec(name="rename", summary="Rename current session", argument_hint="<new_name>", category="utility"),
101
+ SlashCommandSpec(name="files", summary="List files referenced in this session", category="utility"),
102
+ SlashCommandSpec(name="summary", summary="AI summarizes the conversation so far", category="utility"),
103
+ SlashCommandSpec(name="stats", summary="Show detailed usage statistics", category="utility"),
104
+ ]
105
+
106
+
107
+ # ---------------------------------------------------------------------------
108
+ # Command registry
109
+ # ---------------------------------------------------------------------------
110
+
111
+ class CommandRegistry:
112
+ """Registry of slash commands — only commands with working handlers."""
113
+
114
+ def __init__(self) -> None:
115
+ self._commands: dict[str, SlashCommandSpec] = {}
116
+ self._by_category: dict[str, list[SlashCommandSpec]] = {}
117
+ self._register_builtins()
118
+
119
+ def _register_builtins(self) -> None:
120
+ for spec in SLASH_COMMAND_SPECS:
121
+ self._commands[spec.name] = spec
122
+ for alias in spec.aliases:
123
+ self._commands[alias] = spec
124
+ cat = spec.category
125
+ if cat not in self._by_category:
126
+ self._by_category[cat] = []
127
+ self._by_category[cat].append(spec)
128
+
129
+ def get(self, name: str) -> SlashCommandSpec | None:
130
+ return self._commands.get(name.lstrip("/").lower())
131
+
132
+ def all_specs(self) -> list[SlashCommandSpec]:
133
+ seen: set[str] = set()
134
+ specs: list[SlashCommandSpec] = []
135
+ for spec in self._commands.values():
136
+ if spec.name not in seen:
137
+ seen.add(spec.name)
138
+ specs.append(spec)
139
+ return specs
140
+
141
+ def command_names(self) -> list[str]:
142
+ return list(self._commands.keys())
143
+
144
+ def by_category(self, category: str) -> list[SlashCommandSpec]:
145
+ return self._by_category.get(category, [])
146
+
147
+ def categories(self) -> list[str]:
148
+ return sorted(self._by_category.keys())
149
+
150
+ def register_plugin_command(self, name: str, summary: str = "") -> None:
151
+ spec = SlashCommandSpec(name=name, summary=summary, source="plugin", category="plugin")
152
+ self._commands[name] = spec
153
+
154
+ def register_skill_command(self, name: str, summary: str = "") -> None:
155
+ spec = SlashCommandSpec(name=name, summary=summary, source="skill", category="skill")
156
+ self._commands[name] = spec
157
+
158
+
159
+ _registry: CommandRegistry | None = None
160
+
161
+
162
+ def get_command_registry() -> CommandRegistry:
163
+ global _registry
164
+ if _registry is None:
165
+ _registry = CommandRegistry()
166
+ return _registry
File without changes
@@ -0,0 +1,145 @@
1
+ """Extract TypeScript manifests from upstream repo.
2
+
3
+ Maps to: rust/crates/compat-harness/src/lib.rs
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import os
9
+ import re
10
+ from dataclasses import dataclass, field
11
+ from pathlib import Path
12
+
13
+
14
+ @dataclass
15
+ class UpstreamPaths:
16
+ """Resolver for upstream repo file paths."""
17
+
18
+ repo_root: Path
19
+
20
+ def commands_path(self) -> Path:
21
+ return self.repo_root / "src" / "commands.ts"
22
+
23
+ def tools_path(self) -> Path:
24
+ return self.repo_root / "src" / "tools.ts"
25
+
26
+ def cli_path(self) -> Path:
27
+ return self.repo_root / "src" / "entrypoints" / "cli.tsx"
28
+
29
+ @classmethod
30
+ def from_repo_root(cls, root: Path) -> UpstreamPaths:
31
+ return cls(repo_root=root)
32
+
33
+ @classmethod
34
+ def from_workspace_dir(cls, directory: Path) -> UpstreamPaths | None:
35
+ """Auto-discover upstream repo from workspace directory."""
36
+ candidates = _upstream_repo_candidates(directory)
37
+ for candidate in candidates:
38
+ if (candidate / "src" / "commands.ts").exists():
39
+ return cls(repo_root=candidate)
40
+ return None
41
+
42
+
43
+ @dataclass
44
+ class ExtractedManifest:
45
+ """Parsed manifests from upstream TypeScript."""
46
+
47
+ commands: list[str] = field(default_factory=list)
48
+ tools: list[str] = field(default_factory=list)
49
+ bootstrap_phases: list[str] = field(default_factory=list)
50
+
51
+
52
+ def extract_manifest(paths: UpstreamPaths) -> ExtractedManifest:
53
+ """Read and parse all upstream manifest files."""
54
+ manifest = ExtractedManifest()
55
+
56
+ # Extract commands
57
+ commands_path = paths.commands_path()
58
+ if commands_path.exists():
59
+ source = commands_path.read_text(encoding="utf-8")
60
+ manifest.commands = extract_commands(source)
61
+
62
+ # Extract tools
63
+ tools_path = paths.tools_path()
64
+ if tools_path.exists():
65
+ source = tools_path.read_text(encoding="utf-8")
66
+ manifest.tools = extract_tools(source)
67
+
68
+ # Extract bootstrap phases
69
+ cli_path = paths.cli_path()
70
+ if cli_path.exists():
71
+ source = cli_path.read_text(encoding="utf-8")
72
+ manifest.bootstrap_phases = extract_bootstrap_plan(source)
73
+
74
+ return manifest
75
+
76
+
77
+ def extract_commands(source: str) -> list[str]:
78
+ """Parse command names from TypeScript imports and feature gates."""
79
+ commands: list[str] = []
80
+ for line in source.splitlines():
81
+ # Match import { X } from './commands/X'
82
+ match = re.search(r"import\s*\{([^}]+)\}", line)
83
+ if match:
84
+ symbols = [s.strip() for s in match.group(1).split(",")]
85
+ commands.extend(s for s in symbols if s)
86
+
87
+ # Match feature('name', ...)
88
+ match = re.search(r"feature\(['\"]([^'\"]+)['\"]", line)
89
+ if match:
90
+ commands.append(match.group(1))
91
+
92
+ return sorted(set(commands))
93
+
94
+
95
+ def extract_tools(source: str) -> list[str]:
96
+ """Parse tool names from TypeScript imports."""
97
+ tools: list[str] = []
98
+ for line in source.splitlines():
99
+ match = re.search(r"import\s*\{([^}]+)\}", line)
100
+ if match:
101
+ symbols = [s.strip() for s in match.group(1).split(",")]
102
+ for s in symbols:
103
+ if s.endswith("Tool"):
104
+ tools.append(s)
105
+ return sorted(set(tools))
106
+
107
+
108
+ def extract_bootstrap_plan(source: str) -> list[str]:
109
+ """Detect bootstrap phases from CLI entry point."""
110
+ phases: list[str] = []
111
+ markers = {
112
+ "--version": "version_check",
113
+ "profiler": "profiler",
114
+ "system-prompt": "system_prompt",
115
+ "daemon": "daemon",
116
+ }
117
+ for line in source.splitlines():
118
+ for marker, phase in markers.items():
119
+ if marker in line:
120
+ phases.append(phase)
121
+ return list(dict.fromkeys(phases)) # Dedupe preserving order
122
+
123
+
124
+ def _upstream_repo_candidates(primary: Path) -> list[Path]:
125
+ """Find candidate upstream repo locations."""
126
+ candidates: list[Path] = []
127
+
128
+ # Check environment variable
129
+ env_root = os.environ.get("CLAUDE_CODE_UPSTREAM")
130
+ if env_root:
131
+ candidates.append(Path(env_root))
132
+
133
+ # Check parent directories
134
+ current = primary
135
+ for _ in range(5):
136
+ candidates.append(current)
137
+ parent = current.parent
138
+ if parent == current:
139
+ break
140
+ current = parent
141
+
142
+ # Check vendor directories
143
+ candidates.append(primary / "vendor" / "claude-code")
144
+
145
+ return candidates
File without changes
axion/plugins/hooks.py ADDED
@@ -0,0 +1,22 @@
1
+ """Plugin-level hook execution.
2
+
3
+ Maps to: rust/crates/plugins/src/hooks.rs
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from axion.plugins.manager import PluginRegistry
9
+ from axion.runtime.hooks import HookConfig, HookRunner
10
+
11
+
12
+ def hook_runner_from_registry(registry: PluginRegistry) -> HookRunner:
13
+ """Create a HookRunner from aggregated plugin hooks."""
14
+ hooks = registry.aggregated_hooks()
15
+
16
+ return HookRunner(
17
+ pre_tool_use=[HookConfig(command=cmd) for cmd in hooks["pre_tool_use"]],
18
+ post_tool_use=[HookConfig(command=cmd) for cmd in hooks["post_tool_use"]],
19
+ post_tool_use_failure=[
20
+ HookConfig(command=cmd) for cmd in hooks["post_tool_use_failure"]
21
+ ],
22
+ )