pi-mono-coding 0.1.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.
pi_coding/__init__.py ADDED
@@ -0,0 +1,215 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from typing import Any
5
+
6
+ from pi_agent import Agent
7
+
8
+ from pi_coding.config import (
9
+ ENV_AGENT_DIR,
10
+ VERSION,
11
+ get_agent_dir,
12
+ get_bin_dir,
13
+ get_sessions_dir,
14
+ get_settings_path,
15
+ )
16
+ from pi_coding.core.defaults import DEFAULT_THINKING_LEVEL
17
+ from pi_coding.tools import (
18
+ all_tools,
19
+ bash_tool,
20
+ coding_tools,
21
+ create_all_tools,
22
+ create_bash_tool,
23
+ create_coding_tools,
24
+ create_edit_tool,
25
+ create_find_tool,
26
+ create_grep_tool,
27
+ create_ls_tool,
28
+ create_read_only_tools,
29
+ create_read_tool,
30
+ create_write_tool,
31
+ edit_tool,
32
+ find_tool,
33
+ grep_tool,
34
+ ls_tool,
35
+ read_only_tools,
36
+ read_tool,
37
+ write_tool,
38
+ )
39
+ from pi_coding.utils import (
40
+ DEFAULT_MAX_BYTES,
41
+ DEFAULT_MAX_LINES,
42
+ EditDiffError,
43
+ EditDiffResult,
44
+ FuzzyMatchResult,
45
+ GREP_MAX_LINE_LENGTH,
46
+ TruncationOptions,
47
+ TruncationResult,
48
+ compute_edit_diff,
49
+ detect_line_ending,
50
+ expand_path,
51
+ file_exists,
52
+ format_size,
53
+ fuzzy_find_text,
54
+ generate_diff_string,
55
+ normalize_for_fuzzy_match,
56
+ normalize_to_lf,
57
+ resolve_read_path,
58
+ resolve_to_cwd,
59
+ restore_line_endings,
60
+ strip_bom,
61
+ truncate_head,
62
+ truncate_line,
63
+ truncate_tail,
64
+ )
65
+
66
+ __all__ = [
67
+ "get_coding_tools",
68
+ "get_read_only_tools",
69
+ "get_all_tools",
70
+ "create_coding_agent",
71
+ "CodingAgentConfig",
72
+ "read_tool",
73
+ "write_tool",
74
+ "edit_tool",
75
+ "bash_tool",
76
+ "ls_tool",
77
+ "grep_tool",
78
+ "find_tool",
79
+ "create_read_tool",
80
+ "create_write_tool",
81
+ "create_edit_tool",
82
+ "create_bash_tool",
83
+ "create_ls_tool",
84
+ "create_grep_tool",
85
+ "create_find_tool",
86
+ "coding_tools",
87
+ "read_only_tools",
88
+ "all_tools",
89
+ "create_coding_tools",
90
+ "create_read_only_tools",
91
+ "create_all_tools",
92
+ "DEFAULT_MAX_BYTES",
93
+ "DEFAULT_MAX_LINES",
94
+ "GREP_MAX_LINE_LENGTH",
95
+ "TruncationOptions",
96
+ "TruncationResult",
97
+ "format_size",
98
+ "truncate_head",
99
+ "truncate_tail",
100
+ "truncate_line",
101
+ "expand_path",
102
+ "resolve_to_cwd",
103
+ "resolve_read_path",
104
+ "file_exists",
105
+ "detect_line_ending",
106
+ "normalize_to_lf",
107
+ "restore_line_endings",
108
+ "normalize_for_fuzzy_match",
109
+ "fuzzy_find_text",
110
+ "FuzzyMatchResult",
111
+ "strip_bom",
112
+ "generate_diff_string",
113
+ "compute_edit_diff",
114
+ "EditDiffResult",
115
+ "EditDiffError",
116
+ # Configuration
117
+ "VERSION",
118
+ "ENV_AGENT_DIR",
119
+ "get_agent_dir",
120
+ "get_bin_dir",
121
+ "get_sessions_dir",
122
+ "get_settings_path",
123
+ # Defaults
124
+ "DEFAULT_THINKING_LEVEL",
125
+ ]
126
+
127
+
128
+ def get_coding_tools(cwd: str | None = None) -> list:
129
+ return create_coding_tools(cwd or os.getcwd())
130
+
131
+
132
+ def get_read_only_tools(cwd: str | None = None) -> list:
133
+ return create_read_only_tools(cwd or os.getcwd())
134
+
135
+
136
+ def get_all_tools(cwd: str | None = None) -> list:
137
+ return create_all_tools(cwd or os.getcwd())
138
+
139
+
140
+ class CodingAgentConfig:
141
+ def __init__(
142
+ self,
143
+ model: Any = None,
144
+ system_prompt: str | None = None,
145
+ working_dir: str | None = None,
146
+ tools: list | None = None,
147
+ ):
148
+ self.model = model
149
+ self.system_prompt = system_prompt
150
+ self.working_dir = working_dir
151
+ self.tools = tools
152
+
153
+
154
+ def create_coding_agent(
155
+ config: CodingAgentConfig | None = None,
156
+ **kwargs: Any,
157
+ ) -> Agent:
158
+ cfg = config or CodingAgentConfig()
159
+
160
+ cwd = cfg.working_dir or os.getcwd()
161
+
162
+ system_prompt = cfg.system_prompt or """You are an expert coding assistant with access to file system tools.
163
+
164
+ ## Core Principles
165
+
166
+ 1. **Read before writing**: Always read files before suggesting changes
167
+ 2. **Be precise**: Use exact string matches when editing
168
+ 3. **Show context**: Display file previews before editing
169
+ 4. **Explain changes**: Clearly describe what you're changing and why
170
+ 5. **Handle errors**: Gracefully report errors and suggest solutions
171
+
172
+ ## Available Tools
173
+
174
+ ### File Operations
175
+ - **read**: Read file contents with line numbers, supports images
176
+ - **write**: Create or overwrite files (creates directories)
177
+ - **edit**: Search and replace text in files (exact match required)
178
+
179
+ ### Search & Navigation
180
+ - **ls**: List directory contents with type indicators
181
+ - **grep**: Search file contents using ripgrep
182
+ - **find**: Find files by glob pattern using fd
183
+
184
+ ### Execution
185
+ - **bash**: Execute shell commands with streaming output
186
+
187
+ ## Workflow
188
+
189
+ When asked to make code changes:
190
+ 1. Use ls to understand structure
191
+ 2. Read relevant files
192
+ 3. Explain what you're going to do
193
+ 4. Make changes with edit or write
194
+ 5. Verify changes by reading back
195
+
196
+ ## Best Practices
197
+
198
+ - Use relative paths when possible
199
+ - Check if files exist before editing
200
+ - Be conservative with replacements
201
+ - Provide clear, concise explanations"""
202
+
203
+ tools = cfg.tools or create_coding_tools(cwd)
204
+
205
+ agent = Agent(
206
+ options={
207
+ "model": cfg.model,
208
+ "tools": tools,
209
+ **kwargs,
210
+ }
211
+ )
212
+
213
+ agent.set_system_prompt(system_prompt)
214
+
215
+ return agent
pi_coding/config.py ADDED
@@ -0,0 +1,59 @@
1
+ """Configuration paths for pi_coding agent."""
2
+
3
+ import os
4
+ from pathlib import Path
5
+
6
+ # Version from package (can be updated)
7
+ VERSION = "0.1.0"
8
+
9
+ # App configuration
10
+ APP_NAME = "pi"
11
+ CONFIG_DIR_NAME = ".pi"
12
+
13
+ # Environment variable for custom agent directory
14
+ ENV_AGENT_DIR = f"{APP_NAME.upper()}_CODING_AGENT_DIR"
15
+
16
+
17
+ def get_agent_dir() -> Path:
18
+ """Get the agent config directory (e.g., ~/.pi/agent/).
19
+
20
+ Checks ENV_AGENT_DIR first, then uses default ~/.pi/agent/
21
+
22
+ Returns:
23
+ Path to agent directory
24
+ """
25
+ env_dir = os.environ.get(ENV_AGENT_DIR)
26
+ if env_dir:
27
+ if env_dir == "~":
28
+ return Path.home()
29
+ if env_dir.startswith("~/"):
30
+ return Path.home() / env_dir[2:]
31
+ return Path(env_dir)
32
+ return Path.home() / CONFIG_DIR_NAME / "agent"
33
+
34
+
35
+ def get_sessions_dir() -> Path:
36
+ """Get the sessions directory (e.g., ~/.pi/agent/sessions/).
37
+
38
+ Returns:
39
+ Path to sessions directory
40
+ """
41
+ return get_agent_dir() / "sessions"
42
+
43
+
44
+ def get_settings_path() -> Path:
45
+ """Get path to settings.json (e.g., ~/.pi/agent/settings.json).
46
+
47
+ Returns:
48
+ Path to settings.json file
49
+ """
50
+ return get_agent_dir() / "settings.json"
51
+
52
+
53
+ def get_bin_dir() -> Path:
54
+ """Get path to managed binaries directory (fd, rg).
55
+
56
+ Returns:
57
+ Path to bin directory
58
+ """
59
+ return get_agent_dir() / "bin"
@@ -0,0 +1,5 @@
1
+ """Core module for pi_coding."""
2
+
3
+ from pi_coding.core.defaults import DEFAULT_THINKING_LEVEL
4
+
5
+ __all__ = ["DEFAULT_THINKING_LEVEL"]
@@ -0,0 +1,4 @@
1
+ """Core default constants for pi_coding module."""
2
+
3
+ # Default thinking level for agent reasoning
4
+ DEFAULT_THINKING_LEVEL = "medium"
@@ -0,0 +1,70 @@
1
+ """Coding agent tools - read, write, edit, bash, ls, grep, find."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pi_coding.tools.bash import bash_tool, create_bash_tool
6
+ from pi_coding.tools.edit import edit_tool, create_edit_tool
7
+ from pi_coding.tools.find import find_tool, create_find_tool
8
+ from pi_coding.tools.grep import grep_tool, create_grep_tool
9
+ from pi_coding.tools.ls import ls_tool, create_ls_tool
10
+ from pi_coding.tools.read import read_tool, create_read_tool
11
+ from pi_coding.tools.write import write_tool, create_write_tool
12
+
13
+ coding_tools = [read_tool, bash_tool, edit_tool, write_tool]
14
+ read_only_tools = [read_tool, bash_tool, ls_tool, grep_tool, find_tool]
15
+ all_tools = [read_tool, write_tool, edit_tool, bash_tool, ls_tool, grep_tool, find_tool]
16
+
17
+
18
+ def create_coding_tools(cwd: str) -> list:
19
+ return [
20
+ create_read_tool(cwd),
21
+ create_bash_tool(cwd),
22
+ create_edit_tool(cwd),
23
+ create_write_tool(cwd),
24
+ ]
25
+
26
+
27
+ def create_read_only_tools(cwd: str) -> list:
28
+ return [
29
+ create_read_tool(cwd),
30
+ create_bash_tool(cwd),
31
+ create_ls_tool(cwd),
32
+ create_grep_tool(cwd),
33
+ create_find_tool(cwd),
34
+ ]
35
+
36
+
37
+ def create_all_tools(cwd: str) -> list:
38
+ return [
39
+ create_read_tool(cwd),
40
+ create_write_tool(cwd),
41
+ create_edit_tool(cwd),
42
+ create_bash_tool(cwd),
43
+ create_ls_tool(cwd),
44
+ create_grep_tool(cwd),
45
+ create_find_tool(cwd),
46
+ ]
47
+
48
+
49
+ __all__ = [
50
+ "read_tool",
51
+ "write_tool",
52
+ "edit_tool",
53
+ "bash_tool",
54
+ "ls_tool",
55
+ "grep_tool",
56
+ "find_tool",
57
+ "create_read_tool",
58
+ "create_write_tool",
59
+ "create_edit_tool",
60
+ "create_bash_tool",
61
+ "create_ls_tool",
62
+ "create_grep_tool",
63
+ "create_find_tool",
64
+ "coding_tools",
65
+ "read_only_tools",
66
+ "all_tools",
67
+ "create_coding_tools",
68
+ "create_read_only_tools",
69
+ "create_all_tools",
70
+ ]
@@ -0,0 +1,213 @@
1
+ """Bash tool - executes shell commands with streaming output and truncation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import os
7
+ import tempfile
8
+ from pi_agent.types import AgentToolUpdateCallback
9
+ from typing import Any
10
+
11
+ from pi_agent import AgentTool, AgentToolResult
12
+ from pi_ai.types import TextContent
13
+
14
+ from pi_coding.utils import DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, format_size, truncate_tail
15
+
16
+ _BASH_TOOL_PARAMETERS = {
17
+ "type": "object",
18
+ "properties": {
19
+ "command": {
20
+ "type": "string",
21
+ "description": "Bash command to execute",
22
+ },
23
+ "timeout": {
24
+ "type": "number",
25
+ "description": "Timeout in seconds (optional, no default timeout)",
26
+ },
27
+ },
28
+ "required": ["command"],
29
+ }
30
+
31
+
32
+ async def _execute_bash(
33
+ tool_call_id: str,
34
+ params: dict[str, Any],
35
+ cwd: str,
36
+ cancel_event: asyncio.Event | None,
37
+ on_update: AgentToolUpdateCallback | None,
38
+ ) -> AgentToolResult:
39
+ command = params.get("command")
40
+ timeout = params.get("timeout")
41
+
42
+ if not command:
43
+ return AgentToolResult(
44
+ content=[TextContent(type="text", text="Error: 'command' parameter is required")],
45
+ details={"error": "missing_parameter"},
46
+ )
47
+
48
+ if cancel_event and cancel_event.is_set():
49
+ return AgentToolResult(
50
+ content=[TextContent(type="text", text="Operation aborted")],
51
+ details={"error": "aborted"},
52
+ )
53
+
54
+ shell = os.environ.get("SHELL", "/bin/bash")
55
+ temp_file: tempfile._TemporaryFileWrapper | None = None
56
+ temp_file_path: str | None = None
57
+
58
+ chunks: list[bytes] = []
59
+ chunks_bytes = 0
60
+ max_chunks_bytes = DEFAULT_MAX_BYTES * 2
61
+ total_bytes = 0
62
+
63
+ try:
64
+ process = await asyncio.create_subprocess_shell(
65
+ command,
66
+ cwd=cwd,
67
+ shell=True,
68
+ stdout=asyncio.subprocess.PIPE,
69
+ stderr=asyncio.subprocess.STDOUT,
70
+ executable=shell,
71
+ )
72
+
73
+ async def read_stream():
74
+ nonlocal total_bytes, chunks_bytes, temp_file, temp_file_path
75
+
76
+ if process.stdout is None:
77
+ return
78
+
79
+ while True:
80
+ try:
81
+ chunk = await process.stdout.read(4096)
82
+ if not chunk:
83
+ break
84
+
85
+ total_bytes += len(chunk)
86
+
87
+ if total_bytes > DEFAULT_MAX_BYTES and temp_file is None:
88
+ temp_file = tempfile.NamedTemporaryFile(
89
+ mode="wb", suffix=".log", prefix="pi-bash-", delete=False
90
+ )
91
+ temp_file_path = temp_file.name
92
+ for c in chunks:
93
+ temp_file.write(c)
94
+
95
+ if temp_file:
96
+ temp_file.write(chunk)
97
+
98
+ chunks.append(chunk)
99
+ chunks_bytes += len(chunk)
100
+
101
+ while chunks_bytes > max_chunks_bytes and len(chunks) > 1:
102
+ removed = chunks.pop(0)
103
+ chunks_bytes -= len(removed)
104
+
105
+ if on_update:
106
+ full_buffer = b"".join(chunks)
107
+ full_text = full_buffer.decode("utf-8", errors="replace")
108
+ truncation = truncate_tail(full_text)
109
+ on_update(
110
+ AgentToolResult(
111
+ content=[TextContent(type="text", text=truncation.content or "")],
112
+ details={
113
+ "truncation": truncation.__dict__ if truncation.truncated else None,
114
+ "full_output_path": temp_file_path,
115
+ },
116
+ )
117
+ )
118
+
119
+ except asyncio.CancelledError:
120
+ break
121
+
122
+ read_task = asyncio.create_task(read_stream())
123
+
124
+ try:
125
+ if timeout is not None and timeout > 0:
126
+ await asyncio.wait_for(process.wait(), timeout=timeout)
127
+ else:
128
+ await process.wait()
129
+ except asyncio.TimeoutError:
130
+ process.kill()
131
+ await process.wait()
132
+ if temp_file:
133
+ temp_file.close()
134
+ full_buffer = b"".join(chunks)
135
+ output = full_buffer.decode("utf-8", errors="replace")
136
+ return AgentToolResult(
137
+ content=[
138
+ TextContent(
139
+ type="text",
140
+ text=f"{output}\n\nCommand timed out after {timeout} seconds",
141
+ )
142
+ ],
143
+ details={"error": "timeout", "timeout": timeout, "command": command},
144
+ )
145
+
146
+ await read_task
147
+
148
+ if temp_file:
149
+ temp_file.close()
150
+
151
+ if cancel_event and cancel_event.is_set():
152
+ full_buffer = b"".join(chunks)
153
+ output = full_buffer.decode("utf-8", errors="replace")
154
+ return AgentToolResult(
155
+ content=[TextContent(type="text", text=f"{output}\n\nCommand aborted")],
156
+ details={"error": "aborted", "command": command},
157
+ )
158
+
159
+ full_buffer = b"".join(chunks)
160
+ full_output = full_buffer.decode("utf-8", errors="replace")
161
+ truncation = truncate_tail(full_output)
162
+ output_text = truncation.content or "(no output)"
163
+
164
+ details: dict[str, Any] = {}
165
+ if truncation.truncated:
166
+ details["truncation"] = truncation.__dict__
167
+ details["full_output_path"] = temp_file_path
168
+
169
+ start_line = truncation.total_lines - truncation.output_lines + 1
170
+ end_line = truncation.total_lines
171
+
172
+ if truncation.last_line_partial:
173
+ last_line_size = format_size(len(full_output.split("\n")[-1].encode("utf-8")))
174
+ output_text += f"\n\n[Showing last {format_size(truncation.output_bytes)} of line {end_line} (line is {last_line_size}). Full output: {temp_file_path}]"
175
+ elif truncation.truncated_by == "lines":
176
+ output_text += f"\n\n[Showing lines {start_line}-{end_line} of {truncation.total_lines}. Full output: {temp_file_path}]"
177
+ else:
178
+ output_text += f"\n\n[Showing lines {start_line}-{end_line} of {truncation.total_lines} ({format_size(DEFAULT_MAX_BYTES)} limit). Full output: {temp_file_path}]"
179
+
180
+ if process.returncode is not None and process.returncode != 0:
181
+ output_text += f"\n\nCommand exited with code {process.returncode}"
182
+ return AgentToolResult(
183
+ content=[TextContent(type="text", text=output_text)],
184
+ details={"error": "nonzero_exit", "exit_code": process.returncode, **details},
185
+ )
186
+
187
+ return AgentToolResult(
188
+ content=[TextContent(type="text", text=output_text)],
189
+ details=details if details else None,
190
+ )
191
+
192
+ except Exception as e:
193
+ if temp_file:
194
+ temp_file.close()
195
+ return AgentToolResult(
196
+ content=[TextContent(type="text", text=f"Error running command: {e}")],
197
+ details={"error": str(e), "command": command},
198
+ )
199
+
200
+
201
+ def create_bash_tool(cwd: str, options: dict[str, Any] | None = None) -> AgentTool:
202
+ return AgentTool(
203
+ name="bash",
204
+ label="Bash",
205
+ description=f"Execute a bash command in the current working directory. Returns stdout and stderr. Output is truncated to last {DEFAULT_MAX_LINES} lines or {DEFAULT_MAX_BYTES // 1024}KB (whichever is hit first). If truncated, full output is saved to a temp file. Optionally provide a timeout in seconds.",
206
+ parameters=_BASH_TOOL_PARAMETERS,
207
+ execute=lambda tool_call_id, params, cancel_event, on_update: _execute_bash(
208
+ tool_call_id, params, cwd, cancel_event, on_update
209
+ ),
210
+ )
211
+
212
+
213
+ bash_tool = create_bash_tool(os.getcwd())