limbo-code 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.
- limbo/__init__.py +3 -0
- limbo/__main__.py +6 -0
- limbo/agent.py +456 -0
- limbo/app.py +107 -0
- limbo/config.py +99 -0
- limbo/history.py +90 -0
- limbo/llm/__init__.py +1 -0
- limbo/llm/anthropic_client.py +320 -0
- limbo/llm/catalog.py +234 -0
- limbo/llm/client.py +21 -0
- limbo/llm/factory.py +46 -0
- limbo/llm/openai_client.py +261 -0
- limbo/models.py +81 -0
- limbo/sessions.py +258 -0
- limbo/skills.py +89 -0
- limbo/tools/__init__.py +1 -0
- limbo/tools/base.py +102 -0
- limbo/tools/bash.py +172 -0
- limbo/tools/edit.py +70 -0
- limbo/tools/find.py +71 -0
- limbo/tools/grep.py +182 -0
- limbo/tools/ignore.py +111 -0
- limbo/tools/ls.py +41 -0
- limbo/tools/read.py +105 -0
- limbo/tools/registry.py +66 -0
- limbo/tools/write.py +34 -0
- limbo/trace.py +100 -0
- limbo/ui/__init__.py +1 -0
- limbo/ui/app.py +52 -0
- limbo/ui/app.tcss +174 -0
- limbo/ui/banner.py +83 -0
- limbo/ui/commands.py +61 -0
- limbo/ui/screens/__init__.py +1 -0
- limbo/ui/screens/game2048.py +213 -0
- limbo/ui/screens/main.py +369 -0
- limbo/ui/screens/session_picker.py +59 -0
- limbo/ui/widgets/__init__.py +1 -0
- limbo/ui/widgets/chat.py +149 -0
- limbo/ui/widgets/command_menu.py +45 -0
- limbo/ui/widgets/input.py +199 -0
- limbo/ui/widgets/status_bar.py +32 -0
- limbo/ui/widgets/tool_card.py +179 -0
- limbo_code-0.1.0.dist-info/METADATA +16 -0
- limbo_code-0.1.0.dist-info/RECORD +46 -0
- limbo_code-0.1.0.dist-info/WHEEL +4 -0
- limbo_code-0.1.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"""User input widget."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from rich.cells import cell_len
|
|
8
|
+
from textual.binding import Binding
|
|
9
|
+
from textual.message import Message
|
|
10
|
+
from textual.widgets import TextArea
|
|
11
|
+
|
|
12
|
+
# Widget height is text rows + 2 rows of round border. The input starts at
|
|
13
|
+
# one text row (height 3) and grows up to MAX_TEXT_ROWS before scrolling.
|
|
14
|
+
MIN_TEXT_ROWS = 1
|
|
15
|
+
MAX_TEXT_ROWS = 8
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class UserSubmitted(Message):
|
|
19
|
+
"""Event emitted when the user submits a message."""
|
|
20
|
+
|
|
21
|
+
def __init__(self, message: str):
|
|
22
|
+
self.message = message
|
|
23
|
+
super().__init__()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class InputWidget(TextArea):
|
|
27
|
+
"""Multi-line input that submits on Enter (Shift+Enter for newline).
|
|
28
|
+
|
|
29
|
+
The widget grows vertically as lines are added (up to MAX_TEXT_ROWS),
|
|
30
|
+
and recalls previously submitted input with the up/down arrow keys
|
|
31
|
+
(shell-style history, in-memory per session).
|
|
32
|
+
|
|
33
|
+
When the screen's slash-command menu is open, Enter/Tab/up/down/Esc are
|
|
34
|
+
redirected to the menu instead of their default behavior.
|
|
35
|
+
|
|
36
|
+
Styles live in ``limbo/ui/app.tcss``.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
# Use priority bindings so these actions run before TextArea's default
|
|
40
|
+
# key handling, which would otherwise insert a newline on Enter.
|
|
41
|
+
BINDINGS = [
|
|
42
|
+
Binding("enter", "submit", "Submit", priority=True),
|
|
43
|
+
Binding("shift+enter", "newline", "Newline", priority=True),
|
|
44
|
+
Binding("up", "menu_up", show=False, priority=True),
|
|
45
|
+
Binding("down", "menu_down", show=False, priority=True),
|
|
46
|
+
Binding("tab", "menu_complete", show=False, priority=True),
|
|
47
|
+
Binding("escape", "menu_cancel", show=False, priority=True),
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
51
|
+
super().__init__(*args, **kwargs)
|
|
52
|
+
self.show_line_numbers = False
|
|
53
|
+
# Shell-style input history (in-memory, per session).
|
|
54
|
+
self._history: list[str] = []
|
|
55
|
+
self._history_index: int | None = None
|
|
56
|
+
self._draft: str = ""
|
|
57
|
+
# Last value applied via history recall; used to tell recall edits
|
|
58
|
+
# apart from manual edits in the (async) Changed handler.
|
|
59
|
+
self._history_value: str | None = None
|
|
60
|
+
|
|
61
|
+
def _menu_screen(self):
|
|
62
|
+
"""The screen, but only when its slash-command menu is open."""
|
|
63
|
+
screen = self.screen
|
|
64
|
+
if getattr(screen, "slash_menu_open", False):
|
|
65
|
+
return screen
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
# -- auto-growing height ---------------------------------------------------
|
|
69
|
+
|
|
70
|
+
def _visual_rows(self) -> int:
|
|
71
|
+
"""Number of display rows the current text occupies (soft-wrapped)."""
|
|
72
|
+
width = self.wrap_width
|
|
73
|
+
rows = 0
|
|
74
|
+
for line in self.document.lines:
|
|
75
|
+
length = cell_len(line)
|
|
76
|
+
if width > 0:
|
|
77
|
+
rows += max(1, -(-length // width))
|
|
78
|
+
else:
|
|
79
|
+
rows += 1
|
|
80
|
+
return max(MIN_TEXT_ROWS, rows)
|
|
81
|
+
|
|
82
|
+
def _auto_resize(self) -> None:
|
|
83
|
+
height = min(self._visual_rows(), MAX_TEXT_ROWS) + 2
|
|
84
|
+
current = self.styles.height
|
|
85
|
+
if current is not None and getattr(current, "value", None) == height:
|
|
86
|
+
return
|
|
87
|
+
self.styles.height = height
|
|
88
|
+
|
|
89
|
+
def on_text_area_changed(self, event: TextArea.Changed) -> None:
|
|
90
|
+
if event.text_area is not self:
|
|
91
|
+
return
|
|
92
|
+
self._auto_resize()
|
|
93
|
+
# Editing by hand while browsing history cancels the recall position,
|
|
94
|
+
# so the next up/down starts fresh from the latest entry. Changes
|
|
95
|
+
# applied by history recall itself keep the navigation position.
|
|
96
|
+
if self._history_value is not None and self.text == self._history_value:
|
|
97
|
+
return
|
|
98
|
+
self._history_value = None
|
|
99
|
+
self._history_index = None
|
|
100
|
+
|
|
101
|
+
def on_resize(self) -> None:
|
|
102
|
+
# Terminal width changes affect soft-wrapping, hence the row count.
|
|
103
|
+
self._auto_resize()
|
|
104
|
+
|
|
105
|
+
# -- submission ------------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
def action_submit(self) -> None:
|
|
108
|
+
screen = self._menu_screen()
|
|
109
|
+
if screen is not None and screen.slash_menu_complete(execute=True):
|
|
110
|
+
return
|
|
111
|
+
text = self.text.strip()
|
|
112
|
+
if text:
|
|
113
|
+
if not self._history or self._history[-1] != text:
|
|
114
|
+
self._history.append(text)
|
|
115
|
+
self._history_index = None
|
|
116
|
+
self._history_value = None
|
|
117
|
+
self._draft = ""
|
|
118
|
+
self.post_message(UserSubmitted(text))
|
|
119
|
+
self.clear()
|
|
120
|
+
|
|
121
|
+
def action_newline(self) -> None:
|
|
122
|
+
"""Insert a newline at the current cursor position."""
|
|
123
|
+
self.insert("\n")
|
|
124
|
+
|
|
125
|
+
# -- history recall --------------------------------------------------------
|
|
126
|
+
|
|
127
|
+
def _set_text_from_history(self, value: str) -> None:
|
|
128
|
+
self._history_value = value
|
|
129
|
+
self.text = value
|
|
130
|
+
self.move_cursor(self.document.end)
|
|
131
|
+
|
|
132
|
+
def _cursor_on_first_visual_row(self) -> bool:
|
|
133
|
+
"""Whether the cursor sits on the first *visual* (soft-wrapped) row.
|
|
134
|
+
|
|
135
|
+
With soft wrap a single logical line spans several display rows; the
|
|
136
|
+
logical row from ``cursor_location`` stays 0, so the column is used
|
|
137
|
+
to approximate the visual position (wide chars make this approximate).
|
|
138
|
+
"""
|
|
139
|
+
row, col = self.cursor_location
|
|
140
|
+
if row > 0:
|
|
141
|
+
return False
|
|
142
|
+
width = self.wrap_width
|
|
143
|
+
return width <= 0 or col < width
|
|
144
|
+
|
|
145
|
+
def _history_prev(self) -> None:
|
|
146
|
+
if not self._history:
|
|
147
|
+
self.action_cursor_up()
|
|
148
|
+
return
|
|
149
|
+
if self._history_index is None:
|
|
150
|
+
# Multi-line/soft-wrapped input: up moves the cursor until it
|
|
151
|
+
# reaches the first visual row; only then does it start
|
|
152
|
+
# recalling history.
|
|
153
|
+
if not self._cursor_on_first_visual_row():
|
|
154
|
+
self.action_cursor_up()
|
|
155
|
+
return
|
|
156
|
+
self._draft = self.text
|
|
157
|
+
self._history_index = len(self._history) - 1
|
|
158
|
+
else:
|
|
159
|
+
self._history_index = max(0, self._history_index - 1)
|
|
160
|
+
self._set_text_from_history(self._history[self._history_index])
|
|
161
|
+
|
|
162
|
+
def _history_next(self) -> None:
|
|
163
|
+
if self._history_index is None:
|
|
164
|
+
self.action_cursor_down()
|
|
165
|
+
return
|
|
166
|
+
self._history_index += 1
|
|
167
|
+
if self._history_index >= len(self._history):
|
|
168
|
+
self._history_index = None
|
|
169
|
+
self._set_text_from_history(self._draft)
|
|
170
|
+
else:
|
|
171
|
+
self._set_text_from_history(self._history[self._history_index])
|
|
172
|
+
|
|
173
|
+
# -- slash-menu key redirection --------------------------------------------
|
|
174
|
+
|
|
175
|
+
def action_menu_up(self) -> None:
|
|
176
|
+
screen = self._menu_screen()
|
|
177
|
+
if screen is None:
|
|
178
|
+
self._history_prev()
|
|
179
|
+
else:
|
|
180
|
+
screen.slash_menu_move(-1)
|
|
181
|
+
|
|
182
|
+
def action_menu_down(self) -> None:
|
|
183
|
+
screen = self._menu_screen()
|
|
184
|
+
if screen is None:
|
|
185
|
+
self._history_next()
|
|
186
|
+
else:
|
|
187
|
+
screen.slash_menu_move(1)
|
|
188
|
+
|
|
189
|
+
def action_menu_complete(self) -> None:
|
|
190
|
+
screen = self._menu_screen()
|
|
191
|
+
if screen is None:
|
|
192
|
+
self.screen.focus_next()
|
|
193
|
+
else:
|
|
194
|
+
screen.slash_menu_complete(execute=False)
|
|
195
|
+
|
|
196
|
+
def action_menu_cancel(self) -> None:
|
|
197
|
+
screen = self._menu_screen()
|
|
198
|
+
if screen is not None:
|
|
199
|
+
screen.slash_menu_close()
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Top status bar: agent state on the left, model/workdir on the right."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from textual.app import ComposeResult
|
|
8
|
+
from textual.containers import Horizontal
|
|
9
|
+
from textual.widgets import Static
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class StatusBar(Horizontal):
|
|
13
|
+
"""One-line status bar docked at the top of the screen."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, model: str, workdir: str, *args: Any, **kwargs: Any) -> None:
|
|
16
|
+
super().__init__(*args, **kwargs)
|
|
17
|
+
# NOTE: attribute names must not collide with MessagePump internals
|
|
18
|
+
# (e.g. `_context` is a method on MessagePump).
|
|
19
|
+
self._state_label = Static("● idle", id="statusbar-state", markup=False)
|
|
20
|
+
self._context_label = Static(
|
|
21
|
+
f"{model} · {workdir}", id="statusbar-context", markup=False
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
def compose(self) -> ComposeResult:
|
|
25
|
+
yield self._state_label
|
|
26
|
+
yield self._context_label
|
|
27
|
+
|
|
28
|
+
def set_state(self, text: str, style: str = "idle") -> None:
|
|
29
|
+
"""Update the left side. ``style`` is one of idle/thinking/tool."""
|
|
30
|
+
self._state_label.update(f"● {text}")
|
|
31
|
+
self._state_label.set_class(style == "thinking", "thinking")
|
|
32
|
+
self._state_label.set_class(style == "tool", "tool")
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""Inline tool-call card shown in the chat flow.
|
|
2
|
+
|
|
3
|
+
A tool card renders as a single summary line (state symbol + tool name +
|
|
4
|
+
argument summary + elapsed time) and can be expanded to show the full tool
|
|
5
|
+
output. State machine: running → success | error.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import time
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from rich.text import Text
|
|
14
|
+
from textual.app import ComposeResult
|
|
15
|
+
from textual.containers import Vertical
|
|
16
|
+
from textual.css.query import NoMatches
|
|
17
|
+
from textual.widgets import RichLog, Static
|
|
18
|
+
|
|
19
|
+
_STATE_SYMBOLS = {
|
|
20
|
+
"running": "…",
|
|
21
|
+
"success": "✓",
|
|
22
|
+
"error": "✗",
|
|
23
|
+
}
|
|
24
|
+
_STATE_LABELS = {
|
|
25
|
+
"running": "运行中",
|
|
26
|
+
"success": "",
|
|
27
|
+
"error": "失败",
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
# Argument keys worth showing in the one-line summary, in priority order.
|
|
31
|
+
_SUMMARY_KEYS = ("path", "command", "pattern", "old_text")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ToolCard(Vertical):
|
|
35
|
+
"""One-line tool summary that expands to show full output."""
|
|
36
|
+
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
tool_id: str,
|
|
40
|
+
name: str,
|
|
41
|
+
arguments: dict[str, Any],
|
|
42
|
+
*args: Any,
|
|
43
|
+
**kwargs: Any,
|
|
44
|
+
) -> None:
|
|
45
|
+
super().__init__(*args, **kwargs)
|
|
46
|
+
self.tool_id = tool_id
|
|
47
|
+
self.tool_name = name
|
|
48
|
+
self.arguments = arguments
|
|
49
|
+
self.state = "running"
|
|
50
|
+
self._started = time.monotonic()
|
|
51
|
+
self._elapsed: float | None = None
|
|
52
|
+
self._has_body = False
|
|
53
|
+
self._body_content: tuple[str, str | None] | None = None
|
|
54
|
+
self.add_class("running")
|
|
55
|
+
|
|
56
|
+
def compose(self) -> ComposeResult:
|
|
57
|
+
yield Static(self._header_text(), classes="tool-header", markup=False)
|
|
58
|
+
body = RichLog(classes="tool-body", wrap=True, max_lines=1000)
|
|
59
|
+
body.display = False
|
|
60
|
+
yield body
|
|
61
|
+
|
|
62
|
+
def on_mount(self) -> None:
|
|
63
|
+
# State transitions may have happened before composition finished
|
|
64
|
+
# (events can arrive in the same loop turn as the card creation).
|
|
65
|
+
self._refresh_header()
|
|
66
|
+
if self._body_content is not None:
|
|
67
|
+
self._write_body(*self._body_content)
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def header(self) -> Static:
|
|
71
|
+
return self.query_one(".tool-header", Static)
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def body(self) -> RichLog:
|
|
75
|
+
return self.query_one(".tool-body", RichLog)
|
|
76
|
+
|
|
77
|
+
# -- state transitions -------------------------------------------------
|
|
78
|
+
|
|
79
|
+
def set_success(self, output: str) -> None:
|
|
80
|
+
self._set_state("success")
|
|
81
|
+
self._set_body(output, lexer=self._lexer_for_body())
|
|
82
|
+
|
|
83
|
+
def set_error(self, error: str) -> None:
|
|
84
|
+
self._set_state("error")
|
|
85
|
+
self._set_body(error)
|
|
86
|
+
|
|
87
|
+
# -- expansion ----------------------------------------------------------
|
|
88
|
+
|
|
89
|
+
def toggle(self) -> None:
|
|
90
|
+
if self._has_body:
|
|
91
|
+
self.body.display = not self.body.display
|
|
92
|
+
|
|
93
|
+
def on_click(self) -> None:
|
|
94
|
+
self.toggle()
|
|
95
|
+
|
|
96
|
+
# -- internals ----------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
def _set_state(self, state: str) -> None:
|
|
99
|
+
if self.state != state:
|
|
100
|
+
self.remove_class(self.state)
|
|
101
|
+
self.state = state
|
|
102
|
+
self.add_class(state)
|
|
103
|
+
if state != "running" and self._elapsed is None:
|
|
104
|
+
self._elapsed = time.monotonic() - self._started
|
|
105
|
+
self._refresh_header()
|
|
106
|
+
|
|
107
|
+
def _refresh_header(self) -> None:
|
|
108
|
+
try:
|
|
109
|
+
self.header.update(self._header_text())
|
|
110
|
+
except NoMatches:
|
|
111
|
+
pass # Not composed yet; on_mount refreshes.
|
|
112
|
+
|
|
113
|
+
def _set_body(self, content: str, lexer: str | None = None) -> None:
|
|
114
|
+
if not content:
|
|
115
|
+
return
|
|
116
|
+
self._has_body = True
|
|
117
|
+
self._body_content = (content, lexer)
|
|
118
|
+
try:
|
|
119
|
+
self._write_body(content, lexer)
|
|
120
|
+
except NoMatches:
|
|
121
|
+
pass # Not composed yet; on_mount writes the body.
|
|
122
|
+
|
|
123
|
+
def _write_body(self, content: str, lexer: str | None = None) -> None:
|
|
124
|
+
renderable: Any = Text(content)
|
|
125
|
+
if lexer:
|
|
126
|
+
try:
|
|
127
|
+
from rich.syntax import Syntax
|
|
128
|
+
|
|
129
|
+
renderable = Syntax(content, lexer, theme="ansi_dark")
|
|
130
|
+
except Exception: # noqa: BLE001 - fall back to plain text
|
|
131
|
+
renderable = Text(content)
|
|
132
|
+
self.body.write(renderable)
|
|
133
|
+
|
|
134
|
+
def _lexer_for_body(self) -> str | None:
|
|
135
|
+
"""Pick a syntax-highlighting lexer based on the tool and its target."""
|
|
136
|
+
if self.tool_name == "edit":
|
|
137
|
+
return "diff"
|
|
138
|
+
if self.tool_name in ("read", "write"):
|
|
139
|
+
path = self.arguments.get("path")
|
|
140
|
+
if isinstance(path, str) and path:
|
|
141
|
+
try:
|
|
142
|
+
from pygments.lexers import ( # type: ignore[import-untyped]
|
|
143
|
+
get_lexer_for_filename,
|
|
144
|
+
)
|
|
145
|
+
from pygments.util import ( # type: ignore[import-untyped]
|
|
146
|
+
ClassNotFound,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
try:
|
|
150
|
+
aliases: list[str] = get_lexer_for_filename(path).aliases
|
|
151
|
+
return aliases[0] if aliases else None
|
|
152
|
+
except ClassNotFound:
|
|
153
|
+
return None
|
|
154
|
+
except Exception: # noqa: BLE001
|
|
155
|
+
return None
|
|
156
|
+
return None
|
|
157
|
+
|
|
158
|
+
def _summary(self) -> str:
|
|
159
|
+
for key in _SUMMARY_KEYS:
|
|
160
|
+
value = self.arguments.get(key)
|
|
161
|
+
if isinstance(value, str) and value:
|
|
162
|
+
first_line = value.splitlines()[0] if value.strip() else value
|
|
163
|
+
return (
|
|
164
|
+
first_line if len(first_line) <= 60 else first_line[:57] + "..."
|
|
165
|
+
)
|
|
166
|
+
return ""
|
|
167
|
+
|
|
168
|
+
def _header_text(self) -> str:
|
|
169
|
+
symbol = _STATE_SYMBOLS.get(self.state, "?")
|
|
170
|
+
parts = [f"{symbol} {self.tool_name}"]
|
|
171
|
+
summary = self._summary()
|
|
172
|
+
if summary:
|
|
173
|
+
parts.append(summary)
|
|
174
|
+
label = _STATE_LABELS.get(self.state, "")
|
|
175
|
+
if label:
|
|
176
|
+
parts.append(f"({label})")
|
|
177
|
+
if self._elapsed is not None and self._elapsed >= 0.05:
|
|
178
|
+
parts.append(f"{self._elapsed:.1f}s")
|
|
179
|
+
return " ".join(parts)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: limbo-code
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A minimal terminal AI coding agent
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Requires-Dist: httpx>=0.27
|
|
7
|
+
Requires-Dist: openai>=1.30
|
|
8
|
+
Requires-Dist: pydantic>=2.0
|
|
9
|
+
Requires-Dist: textual>=0.58
|
|
10
|
+
Requires-Dist: toml>=0.10
|
|
11
|
+
Provides-Extra: dev
|
|
12
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
13
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
14
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
15
|
+
Requires-Dist: respx>=0.21; extra == 'dev'
|
|
16
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
limbo/__init__.py,sha256=nmQZ4JiUyT_OMv0zZlfqfN9ZhmPsR65wPgxl1a2LsWM,72
|
|
2
|
+
limbo/__main__.py,sha256=zAxWJyXU_Gmqu2jxQJM2qmjxX3FllekW2u4HaUdGAAY,126
|
|
3
|
+
limbo/agent.py,sha256=98SBFk4ev4v3Zl1_zZxNdsqAuDhi8n2NKNwAMy2d6qU,16496
|
|
4
|
+
limbo/app.py,sha256=E0rsLGAQAPZFLOD8Q-iIyx23F61mq4FawdqCGk2EVhA,3066
|
|
5
|
+
limbo/config.py,sha256=5CUuUYyiWyXO0mk3LbhmoRy8xon8nGvDK9g8F3gBmOc,3146
|
|
6
|
+
limbo/history.py,sha256=1E3JgkxXNmGJsvLpx261VqbIh7hc3VpUqzHPK_C1aSg,3066
|
|
7
|
+
limbo/models.py,sha256=ZvpA56OlfA3vWye3VJNqey3kQ8MfSRt80Yokps65e1w,2156
|
|
8
|
+
limbo/sessions.py,sha256=RHWRQD9nWojvxcZvnafZEZH2SKDvO_ueetkxNdp_zig,8428
|
|
9
|
+
limbo/skills.py,sha256=GyLtmdAo-cE3dqE6anLoE4nFW_hkKOyKeIWl_gTIaQ4,2959
|
|
10
|
+
limbo/trace.py,sha256=qqoWOXupUJHDja_3dFO6mtJ5ou891HD93xgJNt0lLqw,3299
|
|
11
|
+
limbo/llm/__init__.py,sha256=t2HYY6CphVNUIxWioSdhoQWqMBCq3IJRv366SAckMAs,26
|
|
12
|
+
limbo/llm/anthropic_client.py,sha256=2p6Jsqw0UUeMIRXBNLaJhBARbOkuAyo0GdMg3g_000Q,12568
|
|
13
|
+
limbo/llm/catalog.py,sha256=pLft6sn5yew4yVIaRRIWW4hcEq-CpDuBFJgUGLfCwpA,8294
|
|
14
|
+
limbo/llm/client.py,sha256=6DFajD-wY705HEQX48fiRxhVWZcCKiw9bCf4t7NAIOE,599
|
|
15
|
+
limbo/llm/factory.py,sha256=rKqYviEAKc7UFD0g779amfV8KAkjeSewN_GySw8RX4Q,1536
|
|
16
|
+
limbo/llm/openai_client.py,sha256=uZkUYt8a0FgCK8T6STHJ5PagvsTHyxtAHjkARU3zQeY,9942
|
|
17
|
+
limbo/tools/__init__.py,sha256=FFDBp7pUO15wxLeJGOxJ7TMV3GrNxzwUGKpZiCgP8Dg,27
|
|
18
|
+
limbo/tools/base.py,sha256=l6UQB6FWZRm-n-vY79rSIWAk3CtysfjuzvynyjAUraw,3423
|
|
19
|
+
limbo/tools/bash.py,sha256=UZV5XaXiiEtzo3a7mfRpjuyW5Sq3k2rdWnt8Oocr09s,6257
|
|
20
|
+
limbo/tools/edit.py,sha256=aQeX5Yrm8Jb2EH6TtsROLG6MZoe60_Qm7RuXbbu8oFU,2422
|
|
21
|
+
limbo/tools/find.py,sha256=shvmcxrF-A0NZup9Hz1Oix9WHJoHJuEcN3ysnqghLbQ,2205
|
|
22
|
+
limbo/tools/grep.py,sha256=Wjh3KpYnV1yifKNfLlGn3mMkzivxe4hym_SA-KMtSXI,6229
|
|
23
|
+
limbo/tools/ignore.py,sha256=NXurQ3HnpRvgTz3ICF1gGDOMDgt_91wdxA7bTpBVl1U,3449
|
|
24
|
+
limbo/tools/ls.py,sha256=Rf9Us8_Y1R5YEsTUaIoC7wyYSayHvcLw2aL5APx7BUE,1212
|
|
25
|
+
limbo/tools/read.py,sha256=sBog3GAMJlTyJfLTOhcqcf7xJ3jHf6CdPuEOYYtLHZM,3511
|
|
26
|
+
limbo/tools/registry.py,sha256=0xXcRQt8YswrjIgxFdObPkQelS0h3HG3A4V4NnJCQhI,2301
|
|
27
|
+
limbo/tools/write.py,sha256=tphwTuVcqngwRuzLVoKuv_S9IWStJgAMPI8zWPtOCHI,1103
|
|
28
|
+
limbo/ui/__init__.py,sha256=pcY6-j4Z4FcG0CxRYVssqowkAewo7l3kQvc1Y1IVvGU,28
|
|
29
|
+
limbo/ui/app.py,sha256=pqhP7uEc6dAjov2MAXl1he7v4i4EWr_E1ggRquPJkX4,1371
|
|
30
|
+
limbo/ui/app.tcss,sha256=dF4NqtaqtLP39pL40YO4U2Q57BSXTHa_BVojxvbnn9g,3712
|
|
31
|
+
limbo/ui/banner.py,sha256=FJJyYgp6cVbEbjzqFv_YXlFE1g38BcRdsW30ncKz4dM,3342
|
|
32
|
+
limbo/ui/commands.py,sha256=36bSpvJdsz7fFhHfd-hcn7in8o2wGVoJ_aLNghFEVUk,1922
|
|
33
|
+
limbo/ui/screens/__init__.py,sha256=HsHJQF2SQ9BCUxhchFdDECHh_vk2XJuc9DQFMv8l1jw,21
|
|
34
|
+
limbo/ui/screens/game2048.py,sha256=9nepzBqDjrgtktKV8i-NhxbxddPj5t4P9cYPYGKw_YM,7317
|
|
35
|
+
limbo/ui/screens/main.py,sha256=F9RbTvk8jkn-53gBdh7wKgEQy3xZejE3VQzUK5fG0PA,14266
|
|
36
|
+
limbo/ui/screens/session_picker.py,sha256=0bUCn9QMY-V3iLZlDlCxAKzYnoc7JSwQ-x8YUpyWb5g,1817
|
|
37
|
+
limbo/ui/widgets/__init__.py,sha256=eIeZxQhS1gKqW7kSRh0yRfozVyvDfiSYpK9w5sqnfbE,24
|
|
38
|
+
limbo/ui/widgets/chat.py,sha256=EFKZf3QqCYNEQ0xGWnh0urQKnMdzKQ0Sw604J9Z_FTs,6017
|
|
39
|
+
limbo/ui/widgets/command_menu.py,sha256=oifnskHQEEPCvlyixzaUgBezdH12u2467zVpk-LYn-k,1398
|
|
40
|
+
limbo/ui/widgets/input.py,sha256=ACVtU9CZy34Y4R608ne4nDiI-9xti6HC61T75yXwHy8,7256
|
|
41
|
+
limbo/ui/widgets/status_bar.py,sha256=Ht0Ov7U-22WIwLE2PKmQSmkr7FH8sJumUq7QaiEQzAo,1237
|
|
42
|
+
limbo/ui/widgets/tool_card.py,sha256=aI4ZQ-kF9UtTr-bZfWyC_kvHMm6v17pW8pjaq8Zj76E,6014
|
|
43
|
+
limbo_code-0.1.0.dist-info/METADATA,sha256=YDSA6cwi0nBEHvLK07934qqb4DywrWSk6rxHz73seQc,502
|
|
44
|
+
limbo_code-0.1.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
45
|
+
limbo_code-0.1.0.dist-info/entry_points.txt,sha256=YU3Qkp1sof5v9ROdiBCprNUJ1KuxeehhS95nxFoKPRI,41
|
|
46
|
+
limbo_code-0.1.0.dist-info/RECORD,,
|