paimon 0.1.0__tar.gz

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.
paimon-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,45 @@
1
+ Metadata-Version: 2.4
2
+ Name: paimon
3
+ Version: 0.1.0
4
+ Summary: A minimal code agent built on litellm + textual
5
+ Requires-Python: >=3.10
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: litellm>=1.89.4
8
+ Requires-Dist: textual>=8.2.7
9
+
10
+ # Paimon
11
+
12
+ A minimal terminal code agent built on **litellm** (LLM access) and **textual** (TUI).
13
+
14
+ ## Features (MVP)
15
+
16
+ - Streaming agent loop: LLM + tool calls until the task is done
17
+ - Four tools: `read_file`, `write_file`, `edit_file`, `bash`
18
+ - Live streaming output + reasoning display in a Textual UI
19
+ - Confirmation prompt before dangerous actions (`bash`, `write_file`, `edit_file`)
20
+
21
+ ## Setup
22
+
23
+ Set these environment variables (uses an OpenAI-compatible endpoint via the
24
+ litellm `openai/` prefix):
25
+
26
+ | Variable | Description | Example |
27
+ |----------|-------------|---------|
28
+ | `PAIMON_API_KEY` | API key for the endpoint | `tp-...` |
29
+ | `PAIMON_MODEL` | litellm model id | `openai/mimo-v2.5-pro` |
30
+ | `PAIMON_API_BASE` | base URL of the endpoint | `https://token-plan-cn.xiaomimimo.com/v1` |
31
+
32
+ ## Run
33
+
34
+ ```bash
35
+ uv run paimon # or: uv run main.py
36
+ ```
37
+
38
+ ## Layout
39
+
40
+ | File | Role |
41
+ |------|------|
42
+ | `paimon/config.py` | model config |
43
+ | `paimon/tools.py` | tool schemas + execution |
44
+ | `paimon/agent.py` | UI-agnostic agent loop (yields typed events) |
45
+ | `paimon/app.py` | Textual TUI |
paimon-0.1.0/README.md ADDED
@@ -0,0 +1,36 @@
1
+ # Paimon
2
+
3
+ A minimal terminal code agent built on **litellm** (LLM access) and **textual** (TUI).
4
+
5
+ ## Features (MVP)
6
+
7
+ - Streaming agent loop: LLM + tool calls until the task is done
8
+ - Four tools: `read_file`, `write_file`, `edit_file`, `bash`
9
+ - Live streaming output + reasoning display in a Textual UI
10
+ - Confirmation prompt before dangerous actions (`bash`, `write_file`, `edit_file`)
11
+
12
+ ## Setup
13
+
14
+ Set these environment variables (uses an OpenAI-compatible endpoint via the
15
+ litellm `openai/` prefix):
16
+
17
+ | Variable | Description | Example |
18
+ |----------|-------------|---------|
19
+ | `PAIMON_API_KEY` | API key for the endpoint | `tp-...` |
20
+ | `PAIMON_MODEL` | litellm model id | `openai/mimo-v2.5-pro` |
21
+ | `PAIMON_API_BASE` | base URL of the endpoint | `https://token-plan-cn.xiaomimimo.com/v1` |
22
+
23
+ ## Run
24
+
25
+ ```bash
26
+ uv run paimon # or: uv run main.py
27
+ ```
28
+
29
+ ## Layout
30
+
31
+ | File | Role |
32
+ |------|------|
33
+ | `paimon/config.py` | model config |
34
+ | `paimon/tools.py` | tool schemas + execution |
35
+ | `paimon/agent.py` | UI-agnostic agent loop (yields typed events) |
36
+ | `paimon/app.py` | Textual TUI |
@@ -0,0 +1 @@
1
+ """Paimon, a minimal code agent built on litellm + textual."""
@@ -0,0 +1,159 @@
1
+ """The agent loop: stream from the LLM, run tool calls, repeat until done.
2
+
3
+ ``Agent.run`` is UI-agnostic: it yields typed events that a CLI or a TUI can
4
+ render however it likes.
5
+ """
6
+
7
+ import json
8
+ from dataclasses import dataclass
9
+ from datetime import date
10
+ from pathlib import Path
11
+ from typing import AsyncIterator, Awaitable, Callable, Optional
12
+
13
+ import litellm
14
+
15
+ from . import config, tools
16
+
17
+ litellm.telemetry = False
18
+ litellm.suppress_debug_info = True
19
+
20
+
21
+ # ---- Events yielded by Agent.run -------------------------------------------
22
+
23
+
24
+ @dataclass
25
+ class TextDelta:
26
+ text: str
27
+
28
+
29
+ @dataclass
30
+ class ReasoningDelta:
31
+ text: str
32
+
33
+
34
+ @dataclass
35
+ class ToolStart:
36
+ id: str
37
+ name: str
38
+ args: dict
39
+
40
+
41
+ @dataclass
42
+ class ToolEnd:
43
+ id: str
44
+ name: str
45
+ result: str
46
+ denied: bool = False
47
+
48
+
49
+ @dataclass
50
+ class TurnEnd:
51
+ pass
52
+
53
+
54
+ # A confirm callback returns True to allow a dangerous tool, False to deny.
55
+ ConfirmFn = Callable[[str, dict], Awaitable[bool]]
56
+
57
+
58
+ def _system_prompt(cwd: Path) -> str:
59
+ return f"""You are Paimon, a concise coding assistant operating in a terminal.
60
+
61
+ You help with software engineering tasks by reading and editing files and running
62
+ shell commands. You have these tools: read_file, write_file, edit_file, bash.
63
+
64
+ Guidelines:
65
+ - Working directory: {cwd}
66
+ - Today's date: {date.today().isoformat()}
67
+ - Prefer reading a file before editing it. For edits, use edit_file with a unique
68
+ old_string; only use write_file for new files or full rewrites.
69
+ - Use the bash tool for listing, searching (grep/find/ls), git, and running tests.
70
+ - Be direct. When the task is done, briefly state what you did. Don't narrate every step.
71
+ """
72
+
73
+
74
+ class Agent:
75
+ def __init__(self, cwd: Optional[Path] = None, confirm: Optional[ConfirmFn] = None):
76
+ self.cwd = Path(cwd or Path.cwd())
77
+ self.confirm = confirm
78
+ self.messages: list[dict] = [
79
+ {"role": "system", "content": _system_prompt(self.cwd)}
80
+ ]
81
+
82
+ async def run(self, user_input: str) -> AsyncIterator[object]:
83
+ """Run one user turn to completion, yielding events along the way."""
84
+ self.messages.append({"role": "user", "content": user_input})
85
+
86
+ while True:
87
+ response = await litellm.acompletion(
88
+ model=config.MODEL,
89
+ api_base=config.API_BASE,
90
+ api_key=config.API_KEY,
91
+ messages=self.messages,
92
+ tools=tools.TOOLS,
93
+ stream=True,
94
+ )
95
+
96
+ content = ""
97
+ # index -> {"id", "name", "args"} accumulated across stream deltas
98
+ calls: dict[int, dict] = {}
99
+
100
+ async for chunk in response:
101
+ delta = chunk.choices[0].delta
102
+
103
+ reasoning = getattr(delta, "reasoning_content", None)
104
+ if reasoning:
105
+ yield ReasoningDelta(reasoning)
106
+
107
+ if delta.content:
108
+ content += delta.content
109
+ yield TextDelta(delta.content)
110
+
111
+ for tc in delta.tool_calls or []:
112
+ slot = calls.setdefault(tc.index, {"id": "", "name": "", "args": ""})
113
+ if tc.id:
114
+ slot["id"] = tc.id
115
+ if tc.function and tc.function.name:
116
+ slot["name"] = tc.function.name
117
+ if tc.function and tc.function.arguments:
118
+ slot["args"] += tc.function.arguments
119
+
120
+ ordered = [calls[i] for i in sorted(calls)]
121
+
122
+ assistant_msg: dict = {"role": "assistant", "content": content or None}
123
+ if ordered:
124
+ assistant_msg["tool_calls"] = [
125
+ {
126
+ "id": c["id"],
127
+ "type": "function",
128
+ "function": {"name": c["name"], "arguments": c["args"]},
129
+ }
130
+ for c in ordered
131
+ ]
132
+ self.messages.append(assistant_msg)
133
+
134
+ if not ordered:
135
+ yield TurnEnd()
136
+ return
137
+
138
+ for c in ordered:
139
+ try:
140
+ args = json.loads(c["args"] or "{}")
141
+ except json.JSONDecodeError:
142
+ args = {}
143
+ name = c["name"]
144
+ yield ToolStart(c["id"], name, args)
145
+
146
+ denied = False
147
+ if self.confirm and name in tools.DANGEROUS:
148
+ allowed = await self.confirm(name, args)
149
+ if not allowed:
150
+ denied = True
151
+ result = "User denied this operation."
152
+ if not denied:
153
+ result = await tools.execute_tool(name, args, self.cwd)
154
+
155
+ yield ToolEnd(c["id"], name, result, denied=denied)
156
+ self.messages.append(
157
+ {"role": "tool", "tool_call_id": c["id"], "content": result}
158
+ )
159
+ # loop again so the model can react to tool results
@@ -0,0 +1,171 @@
1
+ """Textual TUI for the Paimon agent."""
2
+
3
+ import json
4
+ from pathlib import Path
5
+
6
+ from rich.text import Text
7
+ from textual import on, work
8
+ from textual.app import App, ComposeResult
9
+ from textual.containers import Vertical, VerticalScroll
10
+ from textual.screen import ModalScreen
11
+ from textual.widgets import Button, Input, Static
12
+
13
+ from .agent import (
14
+ Agent,
15
+ ReasoningDelta,
16
+ TextDelta,
17
+ ToolEnd,
18
+ ToolStart,
19
+ TurnEnd,
20
+ )
21
+
22
+
23
+ class ConfirmScreen(ModalScreen[bool]):
24
+ """Yes/No confirmation for a dangerous tool call."""
25
+
26
+ BINDINGS = [("y", "allow", "Allow"), ("n", "deny", "Deny"), ("escape", "deny", "Deny")]
27
+
28
+ def __init__(self, tool_name: str, args: dict) -> None:
29
+ super().__init__()
30
+ self.tool_name = tool_name
31
+ self.args = args
32
+
33
+ def compose(self) -> ComposeResult:
34
+ detail = self.args.get("command") or self.args.get("path") or ""
35
+ body = Text()
36
+ body.append("Allow this action?\n\n", style="bold")
37
+ body.append(f"{self.tool_name}", style="bold yellow")
38
+ body.append(f" {detail}", style="dim")
39
+ with Vertical(id="confirm-box"):
40
+ yield Static(body)
41
+ with Vertical(id="confirm-buttons"):
42
+ yield Button("Allow (y)", variant="success", id="allow")
43
+ yield Button("Deny (n)", variant="error", id="deny")
44
+
45
+ @on(Button.Pressed, "#allow")
46
+ def action_allow(self) -> None:
47
+ self.dismiss(True)
48
+
49
+ @on(Button.Pressed, "#deny")
50
+ def action_deny(self) -> None:
51
+ self.dismiss(False)
52
+
53
+
54
+ class PaimonApp(App):
55
+ CSS = """
56
+ #log { height: 1fr; padding: 0 1; }
57
+ #log > Static { margin-bottom: 1; }
58
+ ConfirmScreen { align: center middle; }
59
+ #confirm-box { width: 70%; height: auto; padding: 1 2; border: round $warning; background: $surface; }
60
+ #confirm-buttons { height: auto; margin-top: 1; }
61
+ #confirm-buttons Button { margin-right: 2; width: auto; }
62
+ """
63
+
64
+ BINDINGS = [("ctrl+c", "quit", "Quit")]
65
+
66
+ def __init__(self) -> None:
67
+ super().__init__()
68
+ self.agent = Agent(cwd=Path.cwd(), confirm=self._confirm)
69
+
70
+ def compose(self) -> ComposeResult:
71
+ yield VerticalScroll(id="log")
72
+ yield Input(placeholder="Ask Paimon to do something… (Ctrl+C to quit)")
73
+
74
+ def on_mount(self) -> None:
75
+ self.query_one(Input).focus()
76
+
77
+ # ---- rendering helpers --------------------------------------------------
78
+
79
+ def _add(self, renderable) -> Static:
80
+ log = self.query_one("#log", VerticalScroll)
81
+ widget = Static(renderable)
82
+ log.mount(widget)
83
+ log.scroll_end(animate=False)
84
+ return widget
85
+
86
+ def _scroll(self) -> None:
87
+ self.query_one("#log", VerticalScroll).scroll_end(animate=False)
88
+
89
+ # ---- confirmation hook (called from the agent loop) --------------------
90
+
91
+ async def _confirm(self, tool_name: str, args: dict) -> bool:
92
+ return await self.push_screen_wait(ConfirmScreen(tool_name, args))
93
+
94
+ # ---- input → turn -------------------------------------------------------
95
+
96
+ @on(Input.Submitted)
97
+ def handle_submit(self, event: Input.Submitted) -> None:
98
+ text = event.value.strip()
99
+ if not text:
100
+ return
101
+ event.input.value = ""
102
+ header = Text("You\n", style="bold cyan")
103
+ header.append(text)
104
+ self._add(header)
105
+ self.run_turn(text)
106
+
107
+ @work(exclusive=True)
108
+ async def run_turn(self, text: str) -> None:
109
+ inp = self.query_one(Input)
110
+ inp.disabled = True
111
+ inp.placeholder = "Paimon is working…"
112
+
113
+ assistant: Static | None = None
114
+ buffer = ""
115
+ reasoning: Static | None = None
116
+ reasoning_buf = ""
117
+
118
+ try:
119
+ async for ev in self.agent.run(text):
120
+ if isinstance(ev, ReasoningDelta):
121
+ reasoning_buf += ev.text
122
+ body = Text(reasoning_buf, style="dim italic")
123
+ if reasoning is None:
124
+ reasoning = self._add(body)
125
+ else:
126
+ reasoning.update(body)
127
+ self._scroll()
128
+
129
+ elif isinstance(ev, TextDelta):
130
+ buffer += ev.text
131
+ body = Text("Paimon\n", style="bold green")
132
+ body.append(buffer)
133
+ if assistant is None:
134
+ assistant = self._add(body)
135
+ else:
136
+ assistant.update(body)
137
+ self._scroll()
138
+
139
+ elif isinstance(ev, ToolStart):
140
+ detail = ev.args.get("command") or ev.args.get("path") or json.dumps(ev.args)
141
+ line = Text("⚙ ", style="yellow")
142
+ line.append(ev.name, style="bold yellow")
143
+ line.append(f" {detail}", style="dim")
144
+ self._add(line)
145
+ # start fresh assistant/reasoning blocks after a tool runs
146
+ assistant, buffer = None, ""
147
+ reasoning, reasoning_buf = None, ""
148
+
149
+ elif isinstance(ev, ToolEnd):
150
+ preview = "\n".join(ev.result.splitlines()[:15])
151
+ if len(ev.result.splitlines()) > 15:
152
+ preview += "\n…"
153
+ style = "red" if ev.denied else "dim"
154
+ self._add(Text(preview or "(no output)", style=style))
155
+
156
+ elif isinstance(ev, TurnEnd):
157
+ pass
158
+ except Exception as exc: # noqa: BLE001 — show errors instead of crashing the UI
159
+ self._add(Text(f"Error: {exc}", style="bold red"))
160
+ finally:
161
+ inp.disabled = False
162
+ inp.placeholder = "Ask Paimon to do something… (Ctrl+C to quit)"
163
+ inp.focus()
164
+
165
+
166
+ def main() -> None:
167
+ PaimonApp().run()
168
+
169
+
170
+ if __name__ == "__main__":
171
+ main()
@@ -0,0 +1,9 @@
1
+ """Configuration: model settings read from environment variables.
2
+ """
3
+
4
+ import os
5
+
6
+ # litellm uses the "openai/" prefix to talk to any OpenAI-compatible endpoint.
7
+ MODEL = os.environ.get("PAIMON_MODEL")
8
+ API_BASE = os.environ.get("PAIMON_API_BASE")
9
+ API_KEY = os.environ.get("PAIMON_API_KEY")
@@ -0,0 +1,157 @@
1
+ """Tool definitions and execution.
2
+
3
+ Each tool is described with an OpenAI-style JSON schema (sent to the model) and
4
+ implemented by a small Python function. ``execute_tool`` dispatches by name.
5
+ """
6
+
7
+ import asyncio
8
+ from pathlib import Path
9
+
10
+ # Tools whose side effects warrant a user confirmation before running.
11
+ DANGEROUS = {"bash", "write_file", "edit_file"}
12
+
13
+ MAX_OUTPUT = 30_000 # truncate tool output sent back to the model
14
+
15
+ TOOLS = [
16
+ {
17
+ "type": "function",
18
+ "function": {
19
+ "name": "read_file",
20
+ "description": "Read a text file and return its contents with line numbers.",
21
+ "parameters": {
22
+ "type": "object",
23
+ "properties": {
24
+ "path": {"type": "string", "description": "File path, relative to the working directory or absolute."},
25
+ "offset": {"type": "integer", "description": "1-indexed line to start from (optional)."},
26
+ "limit": {"type": "integer", "description": "Maximum number of lines to read (optional)."},
27
+ },
28
+ "required": ["path"],
29
+ },
30
+ },
31
+ },
32
+ {
33
+ "type": "function",
34
+ "function": {
35
+ "name": "write_file",
36
+ "description": "Create or overwrite a file with the given content.",
37
+ "parameters": {
38
+ "type": "object",
39
+ "properties": {
40
+ "path": {"type": "string"},
41
+ "content": {"type": "string"},
42
+ },
43
+ "required": ["path", "content"],
44
+ },
45
+ },
46
+ },
47
+ {
48
+ "type": "function",
49
+ "function": {
50
+ "name": "edit_file",
51
+ "description": "Replace an exact substring in a file. old_string must appear exactly once.",
52
+ "parameters": {
53
+ "type": "object",
54
+ "properties": {
55
+ "path": {"type": "string"},
56
+ "old_string": {"type": "string", "description": "Exact text to replace (must be unique in the file)."},
57
+ "new_string": {"type": "string", "description": "Replacement text."},
58
+ },
59
+ "required": ["path", "old_string", "new_string"],
60
+ },
61
+ },
62
+ },
63
+ {
64
+ "type": "function",
65
+ "function": {
66
+ "name": "bash",
67
+ "description": "Run a shell command in the working directory and return its combined stdout/stderr. Use this for listing, searching (grep/find/ls), git, running tests, etc.",
68
+ "parameters": {
69
+ "type": "object",
70
+ "properties": {
71
+ "command": {"type": "string"},
72
+ },
73
+ "required": ["command"],
74
+ },
75
+ },
76
+ },
77
+ ]
78
+
79
+
80
+ def _resolve(path: str, cwd: Path) -> Path:
81
+ p = Path(path)
82
+ return p if p.is_absolute() else cwd / p
83
+
84
+
85
+ def _read_file(args: dict, cwd: Path) -> str:
86
+ path = _resolve(args["path"], cwd)
87
+ if not path.exists():
88
+ return f"Error: file not found: {path}"
89
+ lines = path.read_text(errors="replace").splitlines()
90
+ offset = max(1, int(args.get("offset", 1)))
91
+ limit = args.get("limit")
92
+ end = offset - 1 + int(limit) if limit else len(lines)
93
+ selected = lines[offset - 1 : end]
94
+ if not selected:
95
+ return "(file is empty or offset past end of file)"
96
+ width = len(str(offset + len(selected) - 1))
97
+ return "\n".join(f"{offset + i:>{width}} {line}" for i, line in enumerate(selected))
98
+
99
+
100
+ def _write_file(args: dict, cwd: Path) -> str:
101
+ path = _resolve(args["path"], cwd)
102
+ path.parent.mkdir(parents=True, exist_ok=True)
103
+ path.write_text(args["content"])
104
+ n = args["content"].count("\n") + 1
105
+ return f"Wrote {n} lines to {path}"
106
+
107
+
108
+ def _edit_file(args: dict, cwd: Path) -> str:
109
+ path = _resolve(args["path"], cwd)
110
+ if not path.exists():
111
+ return f"Error: file not found: {path}"
112
+ text = path.read_text()
113
+ old = args["old_string"]
114
+ count = text.count(old)
115
+ if count == 0:
116
+ return "Error: old_string not found in file."
117
+ if count > 1:
118
+ return f"Error: old_string is not unique (found {count} times). Add more context to make it unique."
119
+ path.write_text(text.replace(old, args["new_string"], 1))
120
+ return f"Edited {path}"
121
+
122
+
123
+ async def _bash(args: dict, cwd: Path) -> str:
124
+ proc = await asyncio.create_subprocess_shell(
125
+ args["command"],
126
+ cwd=str(cwd),
127
+ stdout=asyncio.subprocess.PIPE,
128
+ stderr=asyncio.subprocess.STDOUT,
129
+ )
130
+ try:
131
+ stdout, _ = await asyncio.wait_for(proc.communicate(), timeout=120)
132
+ except asyncio.TimeoutError:
133
+ proc.kill()
134
+ return "Error: command timed out after 120s."
135
+ out = stdout.decode(errors="replace")
136
+ status = f"(exit code {proc.returncode})"
137
+ return f"{out}\n{status}" if out.strip() else status
138
+
139
+
140
+ async def execute_tool(name: str, args: dict, cwd: Path) -> str:
141
+ """Dispatch a tool call. Always returns a string for the model."""
142
+ try:
143
+ if name == "read_file":
144
+ return _read_file(args, cwd)
145
+ if name == "write_file":
146
+ return _write_file(args, cwd)
147
+ if name == "edit_file":
148
+ return _edit_file(args, cwd)
149
+ if name == "bash":
150
+ result = await _bash(args, cwd)
151
+ else:
152
+ return f"Error: unknown tool {name!r}"
153
+ except Exception as exc: # noqa: BLE001 — surface any tool error to the model
154
+ return f"Error executing {name}: {exc}"
155
+ if len(result) > MAX_OUTPUT:
156
+ result = result[:MAX_OUTPUT] + f"\n... (truncated, {len(result) - MAX_OUTPUT} more chars)"
157
+ return result
@@ -0,0 +1,45 @@
1
+ Metadata-Version: 2.4
2
+ Name: paimon
3
+ Version: 0.1.0
4
+ Summary: A minimal code agent built on litellm + textual
5
+ Requires-Python: >=3.10
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: litellm>=1.89.4
8
+ Requires-Dist: textual>=8.2.7
9
+
10
+ # Paimon
11
+
12
+ A minimal terminal code agent built on **litellm** (LLM access) and **textual** (TUI).
13
+
14
+ ## Features (MVP)
15
+
16
+ - Streaming agent loop: LLM + tool calls until the task is done
17
+ - Four tools: `read_file`, `write_file`, `edit_file`, `bash`
18
+ - Live streaming output + reasoning display in a Textual UI
19
+ - Confirmation prompt before dangerous actions (`bash`, `write_file`, `edit_file`)
20
+
21
+ ## Setup
22
+
23
+ Set these environment variables (uses an OpenAI-compatible endpoint via the
24
+ litellm `openai/` prefix):
25
+
26
+ | Variable | Description | Example |
27
+ |----------|-------------|---------|
28
+ | `PAIMON_API_KEY` | API key for the endpoint | `tp-...` |
29
+ | `PAIMON_MODEL` | litellm model id | `openai/mimo-v2.5-pro` |
30
+ | `PAIMON_API_BASE` | base URL of the endpoint | `https://token-plan-cn.xiaomimimo.com/v1` |
31
+
32
+ ## Run
33
+
34
+ ```bash
35
+ uv run paimon # or: uv run main.py
36
+ ```
37
+
38
+ ## Layout
39
+
40
+ | File | Role |
41
+ |------|------|
42
+ | `paimon/config.py` | model config |
43
+ | `paimon/tools.py` | tool schemas + execution |
44
+ | `paimon/agent.py` | UI-agnostic agent loop (yields typed events) |
45
+ | `paimon/app.py` | Textual TUI |
@@ -0,0 +1,13 @@
1
+ README.md
2
+ pyproject.toml
3
+ paimon/__init__.py
4
+ paimon/agent.py
5
+ paimon/app.py
6
+ paimon/config.py
7
+ paimon/tools.py
8
+ paimon.egg-info/PKG-INFO
9
+ paimon.egg-info/SOURCES.txt
10
+ paimon.egg-info/dependency_links.txt
11
+ paimon.egg-info/entry_points.txt
12
+ paimon.egg-info/requires.txt
13
+ paimon.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ paimon = paimon.app:main
@@ -0,0 +1,2 @@
1
+ litellm>=1.89.4
2
+ textual>=8.2.7
@@ -0,0 +1 @@
1
+ paimon
@@ -0,0 +1,19 @@
1
+ [project]
2
+ name = "paimon"
3
+ version = "0.1.0"
4
+ description = "A minimal code agent built on litellm + textual"
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+ dependencies = [
8
+ "litellm>=1.89.4",
9
+ "textual>=8.2.7",
10
+ ]
11
+
12
+ [project.scripts]
13
+ paimon = "paimon.app:main"
14
+
15
+ [tool.uv]
16
+ package = true
17
+
18
+ [tool.ruff]
19
+ line-length = 120
paimon-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+