xdog-coding 0.57.1__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 (71) hide show
  1. xdog/coding/__init__.py +3 -0
  2. xdog/coding/cli/__init__.py +1 -0
  3. xdog/coding/cli/args.py +148 -0
  4. xdog/coding/cli/config_selector.py +74 -0
  5. xdog/coding/cli/file_processor.py +80 -0
  6. xdog/coding/cli/initial_message.py +55 -0
  7. xdog/coding/cli/list_models.py +27 -0
  8. xdog/coding/cli/session_picker.py +45 -0
  9. xdog/coding/config.py +274 -0
  10. xdog/coding/core/__init__.py +1 -0
  11. xdog/coding/core/agent_session.py +281 -0
  12. xdog/coding/core/bash_executor.py +65 -0
  13. xdog/coding/core/compaction/__init__.py +1 -0
  14. xdog/coding/core/compaction/compaction.py +106 -0
  15. xdog/coding/core/compaction/utils.py +80 -0
  16. xdog/coding/core/defaults.py +35 -0
  17. xdog/coding/core/event_bus.py +56 -0
  18. xdog/coding/core/exec_utils.py +127 -0
  19. xdog/coding/core/extensions/__init__.py +1 -0
  20. xdog/coding/core/extensions/loader.py +116 -0
  21. xdog/coding/core/extensions/runner.py +79 -0
  22. xdog/coding/core/extensions/types.py +95 -0
  23. xdog/coding/core/keybindings.py +64 -0
  24. xdog/coding/core/messages.py +192 -0
  25. xdog/coding/core/prompt_templates.py +65 -0
  26. xdog/coding/core/resource_loader.py +137 -0
  27. xdog/coding/core/sdk.py +169 -0
  28. xdog/coding/core/session_manager.py +156 -0
  29. xdog/coding/core/settings_manager.py +93 -0
  30. xdog/coding/core/skills.py +75 -0
  31. xdog/coding/core/slash_commands.py +208 -0
  32. xdog/coding/core/system_prompt.py +115 -0
  33. xdog/coding/core/timings.py +84 -0
  34. xdog/coding/core/tools/__init__.py +36 -0
  35. xdog/coding/core/tools/bash.py +74 -0
  36. xdog/coding/core/tools/edit.py +104 -0
  37. xdog/coding/core/tools/find.py +91 -0
  38. xdog/coding/core/tools/grep.py +148 -0
  39. xdog/coding/core/tools/ls.py +99 -0
  40. xdog/coding/core/tools/path_utils.py +94 -0
  41. xdog/coding/core/tools/read.py +97 -0
  42. xdog/coding/core/tools/truncate.py +58 -0
  43. xdog/coding/core/tools/write.py +62 -0
  44. xdog/coding/main.py +191 -0
  45. xdog/coding/modes/__init__.py +1 -0
  46. xdog/coding/modes/interactive/__init__.py +1 -0
  47. xdog/coding/modes/interactive/components/__init__.py +1 -0
  48. xdog/coding/modes/interactive/components/assistant_message.py +22 -0
  49. xdog/coding/modes/interactive/components/chat_log.py +99 -0
  50. xdog/coding/modes/interactive/components/custom_editor.py +255 -0
  51. xdog/coding/modes/interactive/components/footer.py +47 -0
  52. xdog/coding/modes/interactive/components/tool_execution.py +216 -0
  53. xdog/coding/modes/interactive/components/user_message.py +23 -0
  54. xdog/coding/modes/interactive/interactive_mode.py +526 -0
  55. xdog/coding/modes/interactive/theme.py +183 -0
  56. xdog/coding/modes/print_mode.py +106 -0
  57. xdog/coding/modes/rpc/__init__.py +1 -0
  58. xdog/coding/modes/rpc/rpc_mode.py +189 -0
  59. xdog/coding/py.typed +0 -0
  60. xdog/coding/utils/__init__.py +1 -0
  61. xdog/coding/utils/frontmatter.py +72 -0
  62. xdog/coding/utils/git.py +90 -0
  63. xdog/coding/utils/mime.py +60 -0
  64. xdog/coding/utils/shell.py +77 -0
  65. xdog/coding/utils/sleep.py +49 -0
  66. xdog_coding-0.57.1.dist-info/METADATA +46 -0
  67. xdog_coding-0.57.1.dist-info/RECORD +71 -0
  68. xdog_coding-0.57.1.dist-info/WHEEL +4 -0
  69. xdog_coding-0.57.1.dist-info/entry_points.txt +2 -0
  70. xdog_coding-0.57.1.dist-info/licenses/LICENSE +662 -0
  71. xdog_coding-0.57.1.dist-info/licenses/LICENSE-EXCEPTION.md +82 -0
@@ -0,0 +1,3 @@
1
+ """coding - Interactive coding agent CLI."""
2
+
3
+ __version__ = "0.57.1"
@@ -0,0 +1 @@
1
+ """CLI sub-package for argument parsing and interactive selectors."""
@@ -0,0 +1,148 @@
1
+ """CLI argument parsing via click."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ import click
9
+ from xdog.coding.cli.list_models import list_models_command
10
+ from xdog.coding.cli.session_picker import pick_session_command
11
+
12
+
13
+ @click.command(context_settings={"help_option_names": ["-h", "--help"]})
14
+ @click.option(
15
+ "-m", "--model",
16
+ default=None,
17
+ help="Model to use (e.g. sonnet, opus, haiku, or a full model id).",
18
+ )
19
+ @click.option(
20
+ "-r", "--resume",
21
+ is_flag=True,
22
+ default=False,
23
+ help="Resume the most recent session.",
24
+ )
25
+ @click.option(
26
+ "--resume-id",
27
+ default=None,
28
+ help="Resume a specific session by ID.",
29
+ )
30
+ @click.option(
31
+ "-p", "--prompt",
32
+ default=None,
33
+ help="Initial prompt to send (non-interactive).",
34
+ )
35
+ @click.option(
36
+ "--print",
37
+ "print_mode",
38
+ is_flag=True,
39
+ default=False,
40
+ help="Run in non-interactive print mode.",
41
+ )
42
+ @click.option(
43
+ "--output-format",
44
+ type=click.Choice(["text", "json", "markdown"]),
45
+ default="text",
46
+ help="Output format for print mode.",
47
+ )
48
+ @click.option(
49
+ "--working-dir",
50
+ type=click.Path(exists=True, file_okay=False, path_type=Path),
51
+ default=None,
52
+ help="Working directory for the agent.",
53
+ )
54
+ @click.option(
55
+ "--config",
56
+ "config_path",
57
+ type=click.Path(exists=True, dir_okay=False, path_type=Path),
58
+ default=None,
59
+ help="Path to a config file.",
60
+ )
61
+ @click.option(
62
+ "--thinking-level",
63
+ type=click.Choice(["none", "normal", "deep", "ultrathink"]),
64
+ default=None,
65
+ help="Thinking / reasoning level.",
66
+ )
67
+ @click.option(
68
+ "--list-models",
69
+ is_flag=True,
70
+ default=False,
71
+ help="List available models and exit.",
72
+ )
73
+ @click.option(
74
+ "--pick-session",
75
+ is_flag=True,
76
+ default=False,
77
+ help="Interactively pick a session to resume.",
78
+ )
79
+ @click.option(
80
+ "--rpc",
81
+ is_flag=True,
82
+ default=False,
83
+ help="Run in RPC mode for IDE integration.",
84
+ )
85
+ @click.option(
86
+ "--verbose",
87
+ is_flag=True,
88
+ default=False,
89
+ help="Enable verbose logging.",
90
+ )
91
+ @click.argument("files", nargs=-1, type=click.Path(exists=True, path_type=Path))
92
+ def cli(
93
+ model: str | None,
94
+ resume: bool,
95
+ resume_id: str | None,
96
+ prompt: str | None,
97
+ print_mode: bool,
98
+ output_format: str,
99
+ working_dir: Path | None,
100
+ config_path: Path | None,
101
+ thinking_level: str | None,
102
+ list_models: bool,
103
+ pick_session: bool,
104
+ rpc: bool,
105
+ verbose: bool,
106
+ files: tuple[Path, ...],
107
+ ) -> None:
108
+ """pi - Interactive coding agent CLI.
109
+
110
+ Optionally pass FILES to include their contents in the initial context.
111
+ """
112
+ # Early-exit commands
113
+ if list_models:
114
+ list_models_command()
115
+ return
116
+
117
+ if pick_session:
118
+ pick_session_command()
119
+ return
120
+
121
+ # Build overrides dict from CLI flags
122
+ overrides: dict[str, Any] = {}
123
+ if model is not None:
124
+ overrides["model"] = model
125
+ if thinking_level is not None:
126
+ overrides["thinking_level"] = thinking_level
127
+
128
+ # Defer to the main entry-point for actual execution
129
+ from xdog.coding.main import run_agent
130
+
131
+ run_agent(
132
+ overrides=overrides,
133
+ resume=resume,
134
+ resume_id=resume_id,
135
+ prompt=prompt,
136
+ print_mode=print_mode,
137
+ output_format=output_format,
138
+ working_dir=working_dir,
139
+ config_path=config_path,
140
+ rpc=rpc,
141
+ verbose=verbose,
142
+ files=files,
143
+ )
144
+
145
+
146
+ def parse_args() -> None:
147
+ """Parse CLI arguments and run the application."""
148
+ cli(standalone_mode=True)
@@ -0,0 +1,74 @@
1
+ """Interactive config selector: set default model, thinking level, etc."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ from xdog.coding.config import GlobalConfig, get_global_settings_path
8
+
9
+
10
+ def _prompt_provider() -> str:
11
+ """Ask the user which provider to configure."""
12
+ providers = ["anthropic", "openai", "google"]
13
+ print("\nAvailable providers:")
14
+ for idx, name in enumerate(providers, 1):
15
+ print(f" {idx}. {name}")
16
+ while True:
17
+ choice = input("\nSelect provider [1-3]: ").strip()
18
+ if choice.isdigit() and 1 <= int(choice) <= len(providers):
19
+ return providers[int(choice) - 1]
20
+ print("Invalid selection, try again.")
21
+
22
+
23
+ def _prompt_api_key(provider: str) -> str:
24
+ """Show environment variable instructions for a provider.
25
+
26
+ API keys are managed by the ai package via environment variables,
27
+ not stored in the coding agent's config file.
28
+ """
29
+ env_vars = {
30
+ "anthropic": "ANTHROPIC_API_KEY",
31
+ "openai": "OPENAI_API_KEY",
32
+ "google": "GOOGLE_API_KEY",
33
+ }
34
+ var = env_vars.get(provider, f"{provider.upper()}_API_KEY")
35
+ print(f"\n Set the {var} environment variable to use {provider}.")
36
+ print(f" Example: export {var}=your-key-here")
37
+ return provider
38
+
39
+
40
+ def config_selector(config_path: Path | None = None) -> GlobalConfig:
41
+ """Interactive configuration wizard.
42
+
43
+ Lets the user set the default model and other preferences.
44
+ API keys are managed by the ai package via environment variables.
45
+
46
+ Returns the updated :class:`GlobalConfig`.
47
+ """
48
+ cfg = GlobalConfig.load(config_path)
49
+
50
+ print("=== Pi Coding Agent - Configuration ===\n")
51
+ print("Note: API keys are set via environment variables")
52
+ print(" (ANTHROPIC_API_KEY, OPENAI_API_KEY, GOOGLE_API_KEY)\n")
53
+
54
+ # Default model
55
+ new_model = input(f"Default model [{cfg.default_model}]: ").strip()
56
+ if new_model:
57
+ cfg = cfg.model_copy(update={"default_model": new_model})
58
+
59
+ # Thinking level
60
+ new_thinking = input(f"Thinking level [{cfg.thinking_level}] (off/normal/high): ").strip()
61
+ if new_thinking:
62
+ cfg = cfg.model_copy(update={"thinking_level": new_thinking})
63
+
64
+ # Custom instructions
65
+ if cfg.custom_instructions:
66
+ print(f"\nCurrent custom instructions: {cfg.custom_instructions[:80]}...")
67
+ new_instructions = input("Custom instructions (leave empty to keep): ").strip()
68
+ if new_instructions:
69
+ cfg = cfg.model_copy(update={"custom_instructions": new_instructions})
70
+
71
+ save_path = config_path or get_global_settings_path()
72
+ cfg.save(save_path)
73
+ print(f"\nConfiguration saved to {save_path}")
74
+ return cfg
@@ -0,0 +1,80 @@
1
+ """Process files passed as CLI arguments into context messages."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ # Binary / image extensions that should be handled differently
9
+ _IMAGE_EXTENSIONS = frozenset({".png", ".jpg", ".jpeg", ".gif", ".bmp", ".webp", ".svg"})
10
+ _BINARY_EXTENSIONS = frozenset({
11
+ ".zip", ".tar", ".gz", ".bz2", ".xz",
12
+ ".pdf", ".doc", ".docx", ".xls", ".xlsx",
13
+ ".exe", ".dll", ".so", ".dylib",
14
+ })
15
+
16
+ MAX_FILE_SIZE = 512 * 1024 # 512 KiB
17
+
18
+
19
+ def _is_image(path: Path) -> bool:
20
+ return path.suffix.lower() in _IMAGE_EXTENSIONS
21
+
22
+
23
+ def _is_binary(path: Path) -> bool:
24
+ return path.suffix.lower() in _BINARY_EXTENSIONS
25
+
26
+
27
+ def process_file(path: Path) -> dict[str, Any] | None:
28
+ """Read a single file and return a context dict for the system prompt.
29
+
30
+ Returns ``None`` when the file cannot be processed (binary, too large, etc.).
31
+ """
32
+ resolved = path.resolve()
33
+ if not resolved.is_file():
34
+ return None
35
+
36
+ if _is_binary(resolved):
37
+ return {
38
+ "type": "file_reference",
39
+ "path": str(resolved),
40
+ "note": "Binary file - contents not included.",
41
+ }
42
+
43
+ if _is_image(resolved):
44
+ return {
45
+ "type": "image",
46
+ "path": str(resolved),
47
+ }
48
+
49
+ try:
50
+ stat = resolved.stat()
51
+ except OSError:
52
+ return None
53
+
54
+ if stat.st_size > MAX_FILE_SIZE:
55
+ return {
56
+ "type": "file_reference",
57
+ "path": str(resolved),
58
+ "note": f"File too large ({stat.st_size:,} bytes) - contents not included.",
59
+ }
60
+
61
+ try:
62
+ content = resolved.read_text(encoding="utf-8", errors="replace")
63
+ except OSError:
64
+ return None
65
+
66
+ return {
67
+ "type": "file_content",
68
+ "path": str(resolved),
69
+ "content": content,
70
+ }
71
+
72
+
73
+ def process_files(paths: tuple[Path, ...] | list[Path]) -> list[dict[str, Any]]:
74
+ """Process multiple files and return a list of context dicts."""
75
+ results: list[dict[str, Any]] = []
76
+ for p in paths:
77
+ entry = process_file(Path(p))
78
+ if entry is not None:
79
+ results.append(entry)
80
+ return results
@@ -0,0 +1,55 @@
1
+ """Initial message builder: construct the first message from CLI args and piped stdin."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ from xdog.coding.cli.file_processor import process_files
9
+
10
+
11
+ def build_initial_message(
12
+ *,
13
+ prompt: str | None = None,
14
+ files: tuple[Path, ...] = (),
15
+ read_stdin: bool = False,
16
+ ) -> str | None:
17
+ """Build the initial user message from CLI arguments.
18
+
19
+ Combines prompt text, file contents, and piped stdin into
20
+ a single message string. Returns None if no input is provided.
21
+ """
22
+ parts: list[str] = []
23
+
24
+ # Read piped stdin
25
+ if read_stdin and not sys.stdin.isatty():
26
+ try:
27
+ stdin_content = sys.stdin.read()
28
+ if stdin_content.strip():
29
+ parts.append(stdin_content.strip())
30
+ except Exception:
31
+ pass
32
+
33
+ # Process file arguments
34
+ if files:
35
+ file_entries = process_files(files)
36
+ for entry in file_entries:
37
+ etype = entry.get("type", "")
38
+ path = entry.get("path", "")
39
+ if etype == "file_content":
40
+ content = entry.get("content", "")
41
+ parts.append(f"File: {path}\n```\n{content}\n```")
42
+ elif etype == "image":
43
+ parts.append(f"[Image: {path}]")
44
+ elif etype == "file_reference":
45
+ note = entry.get("note", "")
46
+ parts.append(f"File: {path} ({note})")
47
+
48
+ # Add the explicit prompt last
49
+ if prompt:
50
+ parts.append(prompt)
51
+
52
+ if not parts:
53
+ return None
54
+
55
+ return "\n\n".join(parts)
@@ -0,0 +1,27 @@
1
+ """List available models in a user-friendly table."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ def list_models_command() -> None:
7
+ """Print a table of all registered models and exit."""
8
+ import xdog.ai as ai
9
+ runtime = ai.load()
10
+ models = sorted(
11
+ (m for m in runtime.models() if m.model_type == "chat"),
12
+ key=lambda m: (m.provider, m.id),
13
+ )
14
+
15
+ if not models:
16
+ print("No models available. Run 'xdog-ai login copilot' first.")
17
+ return
18
+
19
+ header = f" {'Model ID':<45s} {'Provider':<15s} {'Protocol':<20s}"
20
+ separator = " " + "-" * 84
21
+
22
+ print("\nAvailable models:\n")
23
+ print(header)
24
+ print(separator)
25
+ for m in models:
26
+ print(f" {m.id:<45s} {m.provider:<15s} {m.api:<20s}")
27
+ print()
@@ -0,0 +1,45 @@
1
+ """Interactive session picker for resuming previous sessions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import datetime, timezone
6
+
7
+ from xdog.coding.core.session_manager import SessionManager, SessionMeta
8
+
9
+
10
+ def _format_session(meta: SessionMeta, idx: int) -> str:
11
+ """Format one session line for display."""
12
+ ts = datetime.fromtimestamp(meta.updated_at, tz=timezone.utc)
13
+ time_str = ts.strftime("%Y-%m-%d %H:%M")
14
+ summary = meta.summary[:60] if meta.summary else "(no summary)"
15
+ return f" {idx:>3d}. [{time_str}] {meta.session_id[:8]} {summary}"
16
+
17
+
18
+ def pick_session_command() -> str | None:
19
+ """Show recent sessions and let the user pick one.
20
+
21
+ Returns the chosen session ID or ``None`` if the user cancels.
22
+ """
23
+ manager = SessionManager()
24
+ sessions = manager.list_sessions(limit=20)
25
+
26
+ if not sessions:
27
+ print("No sessions found.")
28
+ return None
29
+
30
+ print("\nRecent sessions:\n")
31
+ for idx, meta in enumerate(sessions, 1):
32
+ print(_format_session(meta, idx))
33
+ print()
34
+
35
+ while True:
36
+ choice = input("Pick a session number (or 'q' to cancel): ").strip()
37
+ if choice.lower() == "q":
38
+ return None
39
+ if choice.isdigit():
40
+ num = int(choice)
41
+ if 1 <= num <= len(sessions):
42
+ selected = sessions[num - 1]
43
+ print(f"\nResuming session {selected.session_id[:8]}...")
44
+ return selected.session_id
45
+ print("Invalid selection, try again.")