code-puppy 0.0.354__py3-none-any.whl → 0.0.356__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 (38) hide show
  1. code_puppy/agents/__init__.py +2 -0
  2. code_puppy/agents/event_stream_handler.py +74 -1
  3. code_puppy/agents/subagent_stream_handler.py +276 -0
  4. code_puppy/api/__init__.py +13 -0
  5. code_puppy/api/app.py +92 -0
  6. code_puppy/api/main.py +21 -0
  7. code_puppy/api/pty_manager.py +446 -0
  8. code_puppy/api/routers/__init__.py +12 -0
  9. code_puppy/api/routers/agents.py +36 -0
  10. code_puppy/api/routers/commands.py +198 -0
  11. code_puppy/api/routers/config.py +74 -0
  12. code_puppy/api/routers/sessions.py +191 -0
  13. code_puppy/api/templates/terminal.html +361 -0
  14. code_puppy/api/websocket.py +154 -0
  15. code_puppy/callbacks.py +73 -0
  16. code_puppy/command_line/core_commands.py +85 -0
  17. code_puppy/config.py +63 -0
  18. code_puppy/messaging/__init__.py +15 -0
  19. code_puppy/messaging/messages.py +27 -0
  20. code_puppy/messaging/rich_renderer.py +34 -0
  21. code_puppy/messaging/spinner/__init__.py +20 -2
  22. code_puppy/messaging/subagent_console.py +461 -0
  23. code_puppy/plugins/frontend_emitter/__init__.py +25 -0
  24. code_puppy/plugins/frontend_emitter/emitter.py +121 -0
  25. code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
  26. code_puppy/status_display.py +6 -2
  27. code_puppy/tools/agent_tools.py +53 -49
  28. code_puppy/tools/command_runner.py +292 -100
  29. code_puppy/tools/common.py +176 -1
  30. code_puppy/tools/display.py +6 -1
  31. code_puppy/tools/subagent_context.py +158 -0
  32. {code_puppy-0.0.354.dist-info → code_puppy-0.0.356.dist-info}/METADATA +4 -3
  33. {code_puppy-0.0.354.dist-info → code_puppy-0.0.356.dist-info}/RECORD +38 -21
  34. {code_puppy-0.0.354.data → code_puppy-0.0.356.data}/data/code_puppy/models.json +0 -0
  35. {code_puppy-0.0.354.data → code_puppy-0.0.356.data}/data/code_puppy/models_dev_api.json +0 -0
  36. {code_puppy-0.0.354.dist-info → code_puppy-0.0.356.dist-info}/WHEEL +0 -0
  37. {code_puppy-0.0.354.dist-info → code_puppy-0.0.356.dist-info}/entry_points.txt +0 -0
  38. {code_puppy-0.0.354.dist-info → code_puppy-0.0.356.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,446 @@
1
+ """PTY Manager for terminal emulation with cross-platform support.
2
+
3
+ Provides pseudo-terminal (PTY) functionality for interactive shell sessions
4
+ via WebSocket connections. Supports Unix (pty module) and Windows (pywinpty).
5
+ """
6
+
7
+ import asyncio
8
+ import logging
9
+ import os
10
+ import signal
11
+ import struct
12
+ import sys
13
+ from dataclasses import dataclass, field
14
+ from typing import Any, Callable, Optional
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+ # Platform detection
19
+ IS_WINDOWS = sys.platform == "win32"
20
+
21
+ # Conditional imports based on platform
22
+ if IS_WINDOWS:
23
+ try:
24
+ import winpty # type: ignore
25
+
26
+ HAS_WINPTY = True
27
+ except ImportError:
28
+ HAS_WINPTY = False
29
+ winpty = None
30
+ else:
31
+ import fcntl
32
+ import pty
33
+ import termios
34
+
35
+ HAS_WINPTY = False
36
+
37
+
38
+ @dataclass
39
+ class PTYSession:
40
+ """Represents an active PTY session."""
41
+
42
+ session_id: str
43
+ master_fd: Optional[int] = None # Unix only
44
+ slave_fd: Optional[int] = None # Unix only
45
+ pid: Optional[int] = None # Unix only
46
+ winpty_process: Any = None # Windows only
47
+ cols: int = 80
48
+ rows: int = 24
49
+ on_output: Optional[Callable[[bytes], None]] = None
50
+ _reader_task: Optional[asyncio.Task] = None # type: ignore
51
+ _running: bool = field(default=False, init=False)
52
+
53
+ def is_alive(self) -> bool:
54
+ """Check if the PTY session is still active."""
55
+ if IS_WINDOWS:
56
+ return self.winpty_process is not None and self.winpty_process.isalive()
57
+ else:
58
+ if self.pid is None:
59
+ return False
60
+ try:
61
+ os.waitpid(self.pid, os.WNOHANG)
62
+ return True
63
+ except ChildProcessError:
64
+ return False
65
+
66
+
67
+ class PTYManager:
68
+ """Manages PTY sessions for terminal emulation.
69
+
70
+ Provides cross-platform terminal emulation with support for:
71
+ - Unix systems via the pty module
72
+ - Windows via pywinpty (optional dependency)
73
+
74
+ Example:
75
+ manager = PTYManager()
76
+ session = await manager.create_session(
77
+ session_id="my-terminal",
78
+ on_output=lambda data: print(data.decode())
79
+ )
80
+ await manager.write(session.session_id, b"ls -la\n")
81
+ await manager.close_session(session.session_id)
82
+ """
83
+
84
+ def __init__(self) -> None:
85
+ self._sessions: dict[str, PTYSession] = {}
86
+ self._lock = asyncio.Lock()
87
+
88
+ @property
89
+ def sessions(self) -> dict[str, PTYSession]:
90
+ """Get all active sessions."""
91
+ return self._sessions.copy()
92
+
93
+ async def create_session(
94
+ self,
95
+ session_id: str,
96
+ cols: int = 80,
97
+ rows: int = 24,
98
+ on_output: Optional[Callable[[bytes], None]] = None,
99
+ shell: Optional[str] = None,
100
+ ) -> PTYSession:
101
+ """Create a new PTY session.
102
+
103
+ Args:
104
+ session_id: Unique identifier for the session
105
+ cols: Terminal width in columns
106
+ rows: Terminal height in rows
107
+ on_output: Callback for terminal output
108
+ shell: Shell to spawn (defaults to user's shell or /bin/bash)
109
+
110
+ Returns:
111
+ PTYSession: The created session
112
+
113
+ Raises:
114
+ RuntimeError: If session creation fails
115
+ """
116
+ async with self._lock:
117
+ if session_id in self._sessions:
118
+ logger.warning(f"Session {session_id} already exists, closing old one")
119
+ await self._close_session_internal(session_id)
120
+
121
+ if IS_WINDOWS:
122
+ session = await self._create_windows_session(
123
+ session_id, cols, rows, on_output, shell
124
+ )
125
+ else:
126
+ session = await self._create_unix_session(
127
+ session_id, cols, rows, on_output, shell
128
+ )
129
+
130
+ self._sessions[session_id] = session
131
+ logger.info(f"Created PTY session: {session_id}")
132
+ return session
133
+
134
+ async def _create_unix_session(
135
+ self,
136
+ session_id: str,
137
+ cols: int,
138
+ rows: int,
139
+ on_output: Optional[Callable[[bytes], None]],
140
+ shell: Optional[str],
141
+ ) -> PTYSession:
142
+ """Create a PTY session on Unix systems."""
143
+ shell = shell or os.environ.get("SHELL", "/bin/bash")
144
+
145
+ # Fork a new process with a PTY
146
+ pid, master_fd = pty.fork()
147
+
148
+ if pid == 0:
149
+ # Child process - exec the shell
150
+ os.execlp(shell, shell, "-i") # noqa: S606
151
+ else:
152
+ # Parent process
153
+ # Set terminal size
154
+ self._set_unix_winsize(master_fd, rows, cols)
155
+
156
+ # Make master_fd non-blocking
157
+ flags = fcntl.fcntl(master_fd, fcntl.F_GETFL)
158
+ fcntl.fcntl(master_fd, fcntl.F_SETFL, flags | os.O_NONBLOCK)
159
+
160
+ session = PTYSession(
161
+ session_id=session_id,
162
+ master_fd=master_fd,
163
+ pid=pid,
164
+ cols=cols,
165
+ rows=rows,
166
+ on_output=on_output,
167
+ )
168
+ session._running = True
169
+
170
+ # Start reader task
171
+ session._reader_task = asyncio.create_task(self._unix_reader_loop(session))
172
+
173
+ return session
174
+
175
+ async def _create_windows_session(
176
+ self,
177
+ session_id: str,
178
+ cols: int,
179
+ rows: int,
180
+ on_output: Optional[Callable[[bytes], None]],
181
+ shell: Optional[str],
182
+ ) -> PTYSession:
183
+ """Create a PTY session on Windows systems."""
184
+ if not HAS_WINPTY:
185
+ raise RuntimeError(
186
+ "pywinpty is required for Windows terminal support. "
187
+ "Install it with: pip install pywinpty"
188
+ )
189
+
190
+ shell = shell or os.environ.get("COMSPEC", "cmd.exe")
191
+
192
+ # Create winpty process
193
+ winpty_process = winpty.PtyProcess.spawn(
194
+ shell,
195
+ dimensions=(rows, cols),
196
+ )
197
+
198
+ session = PTYSession(
199
+ session_id=session_id,
200
+ winpty_process=winpty_process,
201
+ cols=cols,
202
+ rows=rows,
203
+ on_output=on_output,
204
+ )
205
+ session._running = True
206
+
207
+ # Start reader task
208
+ session._reader_task = asyncio.create_task(self._windows_reader_loop(session))
209
+
210
+ return session
211
+
212
+ def _set_unix_winsize(self, fd: int, rows: int, cols: int) -> None:
213
+ """Set the terminal window size on Unix."""
214
+ winsize = struct.pack("HHHH", rows, cols, 0, 0)
215
+ fcntl.ioctl(fd, termios.TIOCSWINSZ, winsize)
216
+
217
+ async def _unix_reader_loop(self, session: PTYSession) -> None:
218
+ """Read output from Unix PTY and forward to callback."""
219
+ loop = asyncio.get_event_loop()
220
+
221
+ try:
222
+ while session._running and session.master_fd is not None:
223
+ try:
224
+ data = await loop.run_in_executor(
225
+ None, self._read_unix_pty, session.master_fd
226
+ )
227
+
228
+ if data is None:
229
+ # No data available, wait a bit
230
+ await asyncio.sleep(0.01)
231
+ continue
232
+ elif data == b"":
233
+ # EOF - process terminated
234
+ break
235
+ elif session.on_output:
236
+ session.on_output(data)
237
+
238
+ except asyncio.CancelledError:
239
+ break
240
+
241
+ except Exception as e:
242
+ logger.error(f"Unix reader loop error: {e}")
243
+ finally:
244
+ session._running = False
245
+
246
+ def _read_unix_pty(self, fd: int) -> bytes | None:
247
+ """Read from Unix PTY file descriptor.
248
+
249
+ Returns:
250
+ bytes: Data read from PTY
251
+ None: No data available (would block)
252
+ b'': EOF (process terminated)
253
+ """
254
+ try:
255
+ data = os.read(fd, 4096)
256
+ return data
257
+ except BlockingIOError:
258
+ return None
259
+ except OSError:
260
+ return b""
261
+
262
+ async def _windows_reader_loop(self, session: PTYSession) -> None:
263
+ """Read output from Windows PTY and forward to callback."""
264
+ loop = asyncio.get_event_loop()
265
+
266
+ try:
267
+ while (
268
+ session._running
269
+ and session.winpty_process is not None
270
+ and session.winpty_process.isalive()
271
+ ):
272
+ try:
273
+ data = await loop.run_in_executor(
274
+ None, session.winpty_process.read, 4096
275
+ )
276
+ if data and session.on_output:
277
+ session.on_output(
278
+ data.encode() if isinstance(data, str) else data
279
+ )
280
+ except EOFError:
281
+ break
282
+ except asyncio.CancelledError:
283
+ break
284
+
285
+ await asyncio.sleep(0.01)
286
+
287
+ except Exception as e:
288
+ logger.error(f"Windows reader loop error: {e}")
289
+ finally:
290
+ session._running = False
291
+
292
+ async def write(self, session_id: str, data: bytes) -> bool:
293
+ """Write data to a PTY session.
294
+
295
+ Args:
296
+ session_id: The session to write to
297
+ data: Data to write
298
+
299
+ Returns:
300
+ bool: True if write succeeded
301
+ """
302
+ session = self._sessions.get(session_id)
303
+ if not session:
304
+ logger.warning(f"Session {session_id} not found")
305
+ return False
306
+
307
+ try:
308
+ if IS_WINDOWS:
309
+ if session.winpty_process:
310
+ session.winpty_process.write(
311
+ data.decode() if isinstance(data, bytes) else data
312
+ )
313
+ return True
314
+ else:
315
+ if session.master_fd is not None:
316
+ os.write(session.master_fd, data)
317
+ return True
318
+ except Exception as e:
319
+ logger.error(f"Write error for session {session_id}: {e}")
320
+
321
+ return False
322
+
323
+ async def resize(self, session_id: str, cols: int, rows: int) -> bool:
324
+ """Resize a PTY session.
325
+
326
+ Args:
327
+ session_id: The session to resize
328
+ cols: New width in columns
329
+ rows: New height in rows
330
+
331
+ Returns:
332
+ bool: True if resize succeeded
333
+ """
334
+ session = self._sessions.get(session_id)
335
+ if not session:
336
+ logger.warning(f"Session {session_id} not found")
337
+ return False
338
+
339
+ try:
340
+ if IS_WINDOWS:
341
+ if session.winpty_process:
342
+ session.winpty_process.setwinsize(rows, cols)
343
+ else:
344
+ if session.master_fd is not None:
345
+ self._set_unix_winsize(session.master_fd, rows, cols)
346
+
347
+ session.cols = cols
348
+ session.rows = rows
349
+ logger.debug(f"Resized session {session_id} to {cols}x{rows}")
350
+ return True
351
+
352
+ except Exception as e:
353
+ logger.error(f"Resize error for session {session_id}: {e}")
354
+ return False
355
+
356
+ async def close_session(self, session_id: str) -> bool:
357
+ """Close a PTY session.
358
+
359
+ Args:
360
+ session_id: The session to close
361
+
362
+ Returns:
363
+ bool: True if session was closed
364
+ """
365
+ async with self._lock:
366
+ return await self._close_session_internal(session_id)
367
+
368
+ async def _close_session_internal(self, session_id: str) -> bool:
369
+ """Internal session close without lock."""
370
+ session = self._sessions.pop(session_id, None)
371
+ if not session:
372
+ return False
373
+
374
+ session._running = False
375
+
376
+ # Cancel reader task
377
+ if session._reader_task:
378
+ session._reader_task.cancel()
379
+ try:
380
+ await session._reader_task
381
+ except asyncio.CancelledError:
382
+ pass
383
+
384
+ # Clean up platform-specific resources
385
+ if IS_WINDOWS:
386
+ if session.winpty_process:
387
+ try:
388
+ session.winpty_process.terminate()
389
+ except Exception as e:
390
+ logger.debug(f"Error terminating winpty: {e}")
391
+ else:
392
+ # Close file descriptors
393
+ if session.master_fd is not None:
394
+ try:
395
+ os.close(session.master_fd)
396
+ except OSError:
397
+ pass
398
+
399
+ # Terminate child process
400
+ if session.pid is not None:
401
+ try:
402
+ os.kill(session.pid, signal.SIGTERM)
403
+ os.waitpid(session.pid, 0)
404
+ except (OSError, ChildProcessError):
405
+ pass
406
+
407
+ logger.info(f"Closed PTY session: {session_id}")
408
+ return True
409
+
410
+ async def close_all(self) -> None:
411
+ """Close all PTY sessions."""
412
+ session_ids = list(self._sessions.keys())
413
+ for session_id in session_ids:
414
+ await self.close_session(session_id)
415
+ logger.info("Closed all PTY sessions")
416
+
417
+ def get_session(self, session_id: str) -> Optional[PTYSession]:
418
+ """Get a session by ID.
419
+
420
+ Args:
421
+ session_id: The session ID
422
+
423
+ Returns:
424
+ PTYSession or None if not found
425
+ """
426
+ return self._sessions.get(session_id)
427
+
428
+ def list_sessions(self) -> list[str]:
429
+ """List all active session IDs.
430
+
431
+ Returns:
432
+ List of session IDs
433
+ """
434
+ return list(self._sessions.keys())
435
+
436
+
437
+ # Global PTY manager instance
438
+ _pty_manager: Optional[PTYManager] = None
439
+
440
+
441
+ def get_pty_manager() -> PTYManager:
442
+ """Get or create the global PTY manager instance."""
443
+ global _pty_manager
444
+ if _pty_manager is None:
445
+ _pty_manager = PTYManager()
446
+ return _pty_manager
@@ -0,0 +1,12 @@
1
+ """API routers for Code Puppy REST endpoints.
2
+
3
+ This package contains the FastAPI router modules for different API domains:
4
+ - config: Configuration management endpoints
5
+ - commands: Command execution endpoints
6
+ - sessions: Session management endpoints
7
+ - agents: Agent-related endpoints
8
+ """
9
+
10
+ from code_puppy.api.routers import agents, commands, config, sessions
11
+
12
+ __all__ = ["config", "commands", "sessions", "agents"]
@@ -0,0 +1,36 @@
1
+ """Agents API endpoints for agent management.
2
+
3
+ This router provides REST endpoints for:
4
+ - Listing all available agents with their metadata
5
+ """
6
+
7
+ from typing import Any, Dict, List
8
+
9
+ from fastapi import APIRouter
10
+
11
+ router = APIRouter()
12
+
13
+
14
+ @router.get("/")
15
+ async def list_agents() -> List[Dict[str, Any]]:
16
+ """List all available agents.
17
+
18
+ Returns a list of all agents registered in the system,
19
+ including their name, display name, and description.
20
+
21
+ Returns:
22
+ List[Dict[str, Any]]: List of agent information dictionaries.
23
+ """
24
+ from code_puppy.agents import get_agent_descriptions, get_available_agents
25
+
26
+ agents_dict = get_available_agents()
27
+ descriptions = get_agent_descriptions()
28
+
29
+ return [
30
+ {
31
+ "name": name,
32
+ "display_name": display_name,
33
+ "description": descriptions.get(name, "No description"),
34
+ }
35
+ for name, display_name in agents_dict.items()
36
+ ]
@@ -0,0 +1,198 @@
1
+ """Commands API endpoints for slash command execution and autocomplete.
2
+
3
+ This router provides REST endpoints for:
4
+ - Listing all available slash commands
5
+ - Getting info about specific commands
6
+ - Executing slash commands
7
+ - Autocomplete suggestions for partial commands
8
+ """
9
+
10
+ from typing import Any, List, Optional
11
+
12
+ from fastapi import APIRouter, HTTPException
13
+ from pydantic import BaseModel
14
+
15
+ router = APIRouter()
16
+
17
+
18
+ # =============================================================================
19
+ # Pydantic Models
20
+ # =============================================================================
21
+
22
+
23
+ class CommandInfo(BaseModel):
24
+ """Information about a registered command."""
25
+
26
+ name: str
27
+ description: str
28
+ usage: str
29
+ aliases: List[str] = []
30
+ category: str = "core"
31
+ detailed_help: Optional[str] = None
32
+
33
+
34
+ class CommandExecuteRequest(BaseModel):
35
+ """Request to execute a slash command."""
36
+
37
+ command: str # Full command string, e.g., "/set model=gpt-4o"
38
+
39
+
40
+ class CommandExecuteResponse(BaseModel):
41
+ """Response from executing a slash command."""
42
+
43
+ success: bool
44
+ result: Any = None
45
+ error: Optional[str] = None
46
+
47
+
48
+ class AutocompleteRequest(BaseModel):
49
+ """Request for command autocomplete."""
50
+
51
+ partial: str # Partial command string, e.g., "/se" or "/set mo"
52
+
53
+
54
+ class AutocompleteResponse(BaseModel):
55
+ """Response with autocomplete suggestions."""
56
+
57
+ suggestions: List[str]
58
+
59
+
60
+ # =============================================================================
61
+ # Endpoints
62
+ # =============================================================================
63
+
64
+
65
+ @router.get("/")
66
+ async def list_commands() -> List[CommandInfo]:
67
+ """List all available slash commands.
68
+
69
+ Returns a sorted list of all unique commands (no alias duplicates),
70
+ with their metadata including name, description, usage, aliases,
71
+ category, and detailed help.
72
+
73
+ Returns:
74
+ List[CommandInfo]: Sorted list of command information.
75
+ """
76
+ from code_puppy.command_line.command_registry import get_unique_commands
77
+
78
+ commands = []
79
+ for cmd in get_unique_commands():
80
+ commands.append(
81
+ CommandInfo(
82
+ name=cmd.name,
83
+ description=cmd.description,
84
+ usage=cmd.usage,
85
+ aliases=cmd.aliases,
86
+ category=cmd.category,
87
+ detailed_help=cmd.detailed_help,
88
+ )
89
+ )
90
+ return sorted(commands, key=lambda c: c.name)
91
+
92
+
93
+ @router.get("/{name}")
94
+ async def get_command_info(name: str) -> CommandInfo:
95
+ """Get detailed info about a specific command.
96
+
97
+ Looks up a command by name or alias (case-insensitive).
98
+
99
+ Args:
100
+ name: Command name or alias (without leading /).
101
+
102
+ Returns:
103
+ CommandInfo: Full command information.
104
+
105
+ Raises:
106
+ HTTPException: 404 if command not found.
107
+ """
108
+ from code_puppy.command_line.command_registry import get_command
109
+
110
+ cmd = get_command(name)
111
+ if not cmd:
112
+ raise HTTPException(404, f"Command '/{name}' not found")
113
+
114
+ return CommandInfo(
115
+ name=cmd.name,
116
+ description=cmd.description,
117
+ usage=cmd.usage,
118
+ aliases=cmd.aliases,
119
+ category=cmd.category,
120
+ detailed_help=cmd.detailed_help,
121
+ )
122
+
123
+
124
+ @router.post("/execute")
125
+ async def execute_command(request: CommandExecuteRequest) -> CommandExecuteResponse:
126
+ """Execute a slash command.
127
+
128
+ Takes a command string (with or without leading /) and executes it
129
+ using the command handler.
130
+
131
+ Args:
132
+ request: CommandExecuteRequest with the command to execute.
133
+
134
+ Returns:
135
+ CommandExecuteResponse: Result of command execution.
136
+ """
137
+ from code_puppy.command_line.command_handler import handle_command
138
+
139
+ command = request.command
140
+ if not command.startswith("/"):
141
+ command = "/" + command
142
+
143
+ try:
144
+ result = handle_command(command)
145
+ return CommandExecuteResponse(success=True, result=result)
146
+ except Exception as e:
147
+ return CommandExecuteResponse(success=False, error=str(e))
148
+
149
+
150
+ @router.post("/autocomplete")
151
+ async def autocomplete_command(request: AutocompleteRequest) -> AutocompleteResponse:
152
+ """Get autocomplete suggestions for a partial command.
153
+
154
+ Provides intelligent autocomplete based on partial input:
155
+ - Empty input: returns all command names
156
+ - Partial command name: returns matching commands and aliases
157
+ - Complete command with args: returns usage hint
158
+
159
+ Args:
160
+ request: AutocompleteRequest with partial command string.
161
+
162
+ Returns:
163
+ AutocompleteResponse: List of autocomplete suggestions.
164
+ """
165
+ from code_puppy.command_line.command_registry import (
166
+ get_command,
167
+ get_unique_commands,
168
+ )
169
+
170
+ partial = request.partial.lstrip("/")
171
+
172
+ # If empty, return all command names
173
+ if not partial:
174
+ suggestions = [f"/{cmd.name}" for cmd in get_unique_commands()]
175
+ return AutocompleteResponse(suggestions=sorted(suggestions))
176
+
177
+ # Split into command name and args
178
+ parts = partial.split(maxsplit=1)
179
+ cmd_partial = parts[0].lower()
180
+
181
+ # If just the command name (no space yet), suggest matching commands
182
+ if len(parts) == 1:
183
+ suggestions = []
184
+ for cmd in get_unique_commands():
185
+ if cmd.name.startswith(cmd_partial):
186
+ suggestions.append(f"/{cmd.name}")
187
+ for alias in cmd.aliases:
188
+ if alias.startswith(cmd_partial):
189
+ suggestions.append(f"/{alias}")
190
+ return AutocompleteResponse(suggestions=sorted(set(suggestions)))
191
+
192
+ # Command name complete, suggest based on command type
193
+ # (For now, just return the command usage as a hint)
194
+ cmd = get_command(cmd_partial)
195
+ if cmd:
196
+ return AutocompleteResponse(suggestions=[cmd.usage])
197
+
198
+ return AutocompleteResponse(suggestions=[])