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.
- axion/__init__.py +3 -0
- axion/api/__init__.py +0 -0
- axion/api/anthropic.py +460 -0
- axion/api/client.py +259 -0
- axion/api/error.py +161 -0
- axion/api/ollama.py +597 -0
- axion/api/openai_compat.py +805 -0
- axion/api/openai_responses.py +627 -0
- axion/api/prompt_cache.py +31 -0
- axion/api/sse.py +98 -0
- axion/api/types.py +451 -0
- axion/cli/__init__.py +0 -0
- axion/cli/init_cmd.py +50 -0
- axion/cli/input.py +290 -0
- axion/cli/main.py +2953 -0
- axion/cli/render.py +489 -0
- axion/cli/tui.py +766 -0
- axion/commands/__init__.py +0 -0
- axion/commands/handlers/__init__.py +0 -0
- axion/commands/handlers/agents.py +51 -0
- axion/commands/handlers/builtin_commands.py +367 -0
- axion/commands/handlers/mcp.py +59 -0
- axion/commands/handlers/models.py +75 -0
- axion/commands/handlers/plugins.py +55 -0
- axion/commands/handlers/skills.py +61 -0
- axion/commands/parsing.py +317 -0
- axion/commands/registry.py +166 -0
- axion/compat_harness/__init__.py +0 -0
- axion/compat_harness/extractor.py +145 -0
- axion/plugins/__init__.py +0 -0
- axion/plugins/hooks.py +22 -0
- axion/plugins/manager.py +391 -0
- axion/plugins/manifest.py +270 -0
- axion/runtime/__init__.py +0 -0
- axion/runtime/bash.py +388 -0
- axion/runtime/bootstrap.py +39 -0
- axion/runtime/claude_subscription.py +300 -0
- axion/runtime/compact.py +233 -0
- axion/runtime/config.py +397 -0
- axion/runtime/conversation.py +1073 -0
- axion/runtime/file_ops.py +613 -0
- axion/runtime/git.py +213 -0
- axion/runtime/hooks.py +235 -0
- axion/runtime/image.py +212 -0
- axion/runtime/lanes.py +282 -0
- axion/runtime/lsp.py +425 -0
- axion/runtime/mcp/__init__.py +0 -0
- axion/runtime/mcp/client.py +76 -0
- axion/runtime/mcp/lifecycle.py +96 -0
- axion/runtime/mcp/stdio.py +318 -0
- axion/runtime/mcp/tool_bridge.py +79 -0
- axion/runtime/memory.py +196 -0
- axion/runtime/oauth.py +329 -0
- axion/runtime/openai_subscription.py +346 -0
- axion/runtime/permissions.py +247 -0
- axion/runtime/plan_mode.py +96 -0
- axion/runtime/policy_engine.py +259 -0
- axion/runtime/prompt.py +586 -0
- axion/runtime/recovery.py +261 -0
- axion/runtime/remote.py +28 -0
- axion/runtime/sandbox.py +68 -0
- axion/runtime/scheduler.py +231 -0
- axion/runtime/session.py +365 -0
- axion/runtime/sharing.py +159 -0
- axion/runtime/skills.py +124 -0
- axion/runtime/tasks.py +258 -0
- axion/runtime/usage.py +241 -0
- axion/runtime/workers.py +186 -0
- axion/telemetry/__init__.py +0 -0
- axion/telemetry/events.py +67 -0
- axion/telemetry/profile.py +49 -0
- axion/telemetry/sink.py +60 -0
- axion/telemetry/tracer.py +95 -0
- axion/tools/__init__.py +0 -0
- axion/tools/lane_completion.py +33 -0
- axion/tools/registry.py +853 -0
- axion/tools/tool_search.py +226 -0
- axion_code-1.0.0.dist-info/METADATA +709 -0
- axion_code-1.0.0.dist-info/RECORD +82 -0
- axion_code-1.0.0.dist-info/WHEEL +4 -0
- axion_code-1.0.0.dist-info/entry_points.txt +2 -0
- 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
|
+
)
|