stcode 0.0.1a0__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 (111) hide show
  1. stcode/__init__.py +10 -0
  2. stcode/cli/__init__.py +9 -0
  3. stcode/cli/bootstrap.py +70 -0
  4. stcode/cli/labels.py +621 -0
  5. stcode/cli/logic/__init__.py +0 -0
  6. stcode/cli/logic/commands.py +36 -0
  7. stcode/cli/logic/completion.py +43 -0
  8. stcode/cli/logic/connection.py +46 -0
  9. stcode/cli/logic/requests.py +18 -0
  10. stcode/cli/logic/sessions.py +67 -0
  11. stcode/cli/logic/settings.py +35 -0
  12. stcode/cli/logic/state.py +43 -0
  13. stcode/cli/logic/workspace.py +17 -0
  14. stcode/cli/main.py +290 -0
  15. stcode/cli/models.py +61 -0
  16. stcode/cli/services/__init__.py +0 -0
  17. stcode/cli/services/client.py +393 -0
  18. stcode/cli/services/launcher.py +159 -0
  19. stcode/cli/services/paths.py +18 -0
  20. stcode/cli/services/preferences.py +107 -0
  21. stcode/cli/ui/__init__.py +0 -0
  22. stcode/cli/ui/app.py +136 -0
  23. stcode/cli/ui/app.tcss +3 -0
  24. stcode/cli/ui/components/__init__.py +0 -0
  25. stcode/cli/ui/components/banner.py +78 -0
  26. stcode/cli/ui/components/cards.py +229 -0
  27. stcode/cli/ui/components/prompt.py +225 -0
  28. stcode/cli/ui/components/status.py +95 -0
  29. stcode/cli/ui/components/transcript/__init__.py +51 -0
  30. stcode/cli/ui/components/transcript/entries.py +232 -0
  31. stcode/cli/ui/components/transcript/formatting.py +188 -0
  32. stcode/cli/ui/components/transcript/view.py +101 -0
  33. stcode/cli/ui/screens/__init__.py +0 -0
  34. stcode/cli/ui/screens/chat/__init__.py +0 -0
  35. stcode/cli/ui/screens/chat/chat.tcss +12 -0
  36. stcode/cli/ui/screens/chat/presenter.py +187 -0
  37. stcode/cli/ui/screens/chat/screen.py +682 -0
  38. stcode/cli/ui/screens/connect.py +126 -0
  39. stcode/cli/ui/screens/connect.tcss +49 -0
  40. stcode/cli/ui/screens/dialog.tcss +40 -0
  41. stcode/cli/ui/screens/sessions.py +105 -0
  42. stcode/cli/ui/screens/settings.py +137 -0
  43. stcode/cli/ui/screens/settings.tcss +69 -0
  44. stcode/cli/ui/screens/trust.py +50 -0
  45. stcode/cli/ui/theme.py +221 -0
  46. stcode/core/__main__.py +7 -0
  47. stcode/core/agent/__init__.py +41 -0
  48. stcode/core/agent/agent.py +651 -0
  49. stcode/core/agent/events.py +97 -0
  50. stcode/core/agent/supervisor.py +226 -0
  51. stcode/core/agent/task.py +120 -0
  52. stcode/core/common/__init__.py +21 -0
  53. stcode/core/common/compat.py +27 -0
  54. stcode/core/common/ids.py +43 -0
  55. stcode/core/common/tools.py +50 -0
  56. stcode/core/common/trace.py +406 -0
  57. stcode/core/common/truncate.py +76 -0
  58. stcode/core/configs.py +490 -0
  59. stcode/core/daemon/__init__.py +32 -0
  60. stcode/core/daemon/autonomy.py +71 -0
  61. stcode/core/daemon/main.py +126 -0
  62. stcode/core/daemon/protocol.py +499 -0
  63. stcode/core/daemon/runner.py +359 -0
  64. stcode/core/daemon/server.py +682 -0
  65. stcode/core/daemon/workspace.py +88 -0
  66. stcode/core/harness/__init__.py +81 -0
  67. stcode/core/harness/approvals.py +85 -0
  68. stcode/core/harness/context.py +260 -0
  69. stcode/core/harness/errors.py +40 -0
  70. stcode/core/harness/harness.py +417 -0
  71. stcode/core/harness/mcp.py +530 -0
  72. stcode/core/harness/outputs.py +99 -0
  73. stcode/core/harness/prompts/__init__.py +138 -0
  74. stcode/core/harness/prompts/execute.py +51 -0
  75. stcode/core/harness/prompts/plan.py +57 -0
  76. stcode/core/harness/prompts/sections.py +238 -0
  77. stcode/core/harness/skills/__init__.py +30 -0
  78. stcode/core/harness/skills/loader.py +247 -0
  79. stcode/core/harness/tools/__init__.py +111 -0
  80. stcode/core/harness/tools/base.py +640 -0
  81. stcode/core/harness/tools/files.py +276 -0
  82. stcode/core/harness/tools/interact.py +72 -0
  83. stcode/core/harness/tools/registry.py +113 -0
  84. stcode/core/harness/tools/repl.py +97 -0
  85. stcode/core/harness/tools/schema.py +186 -0
  86. stcode/core/harness/tools/search.py +356 -0
  87. stcode/core/harness/tools/shell.py +372 -0
  88. stcode/core/harness/tools/skill.py +66 -0
  89. stcode/core/harness/tools/todo.py +62 -0
  90. stcode/core/harness/tools/web.py +163 -0
  91. stcode/core/providers/__init__.py +31 -0
  92. stcode/core/providers/anthropic_claude.py +243 -0
  93. stcode/core/providers/base.py +73 -0
  94. stcode/core/providers/gateway.py +267 -0
  95. stcode/core/providers/google_gemini.py +225 -0
  96. stcode/core/providers/openai_gpt.py +250 -0
  97. stcode/core/providers/registry.py +46 -0
  98. stcode/core/providers/types.py +148 -0
  99. stcode/core/repl/__init__.py +19 -0
  100. stcode/core/repl/_worker.py +179 -0
  101. stcode/core/repl/client.py +374 -0
  102. stcode/core/session/__init__.py +39 -0
  103. stcode/core/session/session.py +493 -0
  104. stcode/core/team/__init__.py +38 -0
  105. stcode/core/team/mailbox.py +249 -0
  106. stcode/core/team/tools.py +145 -0
  107. stcode-0.0.1a0.dist-info/METADATA +229 -0
  108. stcode-0.0.1a0.dist-info/RECORD +111 -0
  109. stcode-0.0.1a0.dist-info/WHEEL +4 -0
  110. stcode-0.0.1a0.dist-info/entry_points.txt +3 -0
  111. stcode-0.0.1a0.dist-info/licenses/LICENSE +201 -0
stcode/cli/ui/theme.py ADDED
@@ -0,0 +1,221 @@
1
+ """
2
+ Themes — the project's green, in the terminal's own light or dark.
3
+
4
+ Two themes, one primary colour (`#78b032`). The default preference is `auto`, and
5
+ `auto` is ours to implement: Textual 8 detects the terminal's background nowhere, so
6
+ this asks.
7
+
8
+ Three sources, cheapest first:
9
+
10
+ 1. **`COLORFGBG`** — exported by a handful of terminals, free to read, and an answer
11
+ without a round trip.
12
+ 2. **`OSC 11`** — write `ESC ] 11 ; ? BEL`, read back `rgb:rrrr/gggg/bbbb`. Works in
13
+ most modern terminals, and the ones where it does not simply say nothing.
14
+ 3. **Dark.** Not a guess about your taste; a guess about terminals, most of which are.
15
+
16
+ The budget is the part that matters. A terminal that ignores the query must cost 200ms
17
+ once, never a UI that will not start — so the read is bounded by `select`, the tty is
18
+ put back exactly as it was, and every failure path returns rather than raises.
19
+
20
+ Detection runs **before** the app starts: Textual owns the tty afterwards, and two
21
+ things reading raw escape sequences off the same terminal is a race with a corrupted
22
+ screen at the end of it.
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import os
28
+ import re
29
+ import select
30
+ import sys
31
+ from typing import Literal
32
+
33
+ from textual.theme import Theme
34
+
35
+ from stcode.cli.models import ThemePreference
36
+
37
+ PRIMARY = "#78b032"
38
+ """stcode's green. The one colour the UI is built out of."""
39
+
40
+ TerminalMode = Literal["dark", "light"]
41
+
42
+ QUERY = "\x1b]11;?\x07"
43
+ REPLY = re.compile(r"rgb:([0-9a-fA-F]{2,4})/([0-9a-fA-F]{2,4})/([0-9a-fA-F]{2,4})")
44
+ TIMEOUT = 0.2
45
+ """Long enough for a terminal on the other end of an ssh hop, short enough that a
46
+ terminal which will never answer costs one fifth of a second, once."""
47
+
48
+ BRAND_TEXT_ON_LIGHT = "#3f6618"
49
+ """The green, darkened enough to be *read* on the light ground.
50
+
51
+ `primary` stays `#78b032` in both themes, as the brand does. But #78b032 on an off-white
52
+ background is about 2.6:1 — fine for a border, unreadable as a sentence — so anything
53
+ writing words asks `brand_text()` instead. In CSS the equivalent is `$text-primary`,
54
+ which Textual derives the same way; this constant exists because a Rich style string is
55
+ not CSS and cannot reference a variable.
56
+ """
57
+
58
+ STCODE_DARK = Theme(
59
+ name="stcode-dark",
60
+ primary=PRIMARY,
61
+ secondary="#4f7d1f",
62
+ accent=PRIMARY,
63
+ warning="#ffbe3d",
64
+ error="#ff625a",
65
+ success=PRIMARY,
66
+ foreground="#d6d6d2",
67
+ background="#101210",
68
+ surface="#191c19",
69
+ panel="#20241f",
70
+ dark=True,
71
+ )
72
+
73
+ STCODE_LIGHT = Theme(
74
+ name="stcode-light",
75
+ primary=PRIMARY,
76
+ secondary=BRAND_TEXT_ON_LIGHT,
77
+ accent=PRIMARY,
78
+ warning="#a66300",
79
+ error="#cf3028",
80
+ success=BRAND_TEXT_ON_LIGHT,
81
+ foreground="#1c1f1b",
82
+ background="#fbfbf8",
83
+ surface="#f2f3ee",
84
+ panel="#e8eae2",
85
+ dark=False,
86
+ )
87
+
88
+ THEMES = (STCODE_DARK, STCODE_LIGHT)
89
+
90
+
91
+ def brand_text(dark: bool) -> str:
92
+ """The brand colour, in the version that can be read as text on this ground."""
93
+ return PRIMARY if dark else BRAND_TEXT_ON_LIGHT
94
+
95
+
96
+ def error_text(dark: bool) -> str:
97
+ """The theme's red, for the same reason `brand_text` exists: a Rich style string is
98
+ not CSS and cannot say `$error`."""
99
+ return str(STCODE_DARK.error if dark else STCODE_LIGHT.error)
100
+
101
+
102
+ def theme_name_for(preference: ThemePreference, *, detected: TerminalMode) -> str:
103
+ """Which registered theme to use. An explicit choice always beats detection."""
104
+ if preference == "dark":
105
+ return STCODE_DARK.name
106
+ if preference == "light":
107
+ return STCODE_LIGHT.name
108
+ return STCODE_LIGHT.name if detected == "light" else STCODE_DARK.name
109
+
110
+
111
+ def background_is_light(reply: str) -> bool | None:
112
+ """Read an `OSC 11` reply. None when it is not one — which is not a failure.
113
+
114
+ Perceived luminance, not the mean: a saturated blue background is dark with one
115
+ channel at full, and a mid yellow is light with one channel at zero.
116
+ """
117
+ match = REPLY.search(reply or "")
118
+ if match is None:
119
+ return None
120
+ try:
121
+ channels = [int(value, 16) / (16 ** len(value) - 1) for value in match.groups()]
122
+ except (ValueError, ZeroDivisionError):
123
+ return None
124
+ red, green, blue = channels
125
+ return (0.2126 * red + 0.7152 * green + 0.0722 * blue) > 0.5
126
+
127
+
128
+ def mode_from_colorfgbg(value: str | None) -> TerminalMode | None:
129
+ """`COLORFGBG` as some terminals export it: `"<fg>;<bg>"`, ANSI indices.
130
+
131
+ The background is the last field. 0-6 and 8 are the dark half of the base palette;
132
+ `default` means the terminal's own, which for the terminals that set this variable
133
+ at all is light.
134
+ """
135
+ if not value:
136
+ return None
137
+ background = value.split(";")[-1].strip().lower()
138
+ if background == "default":
139
+ return "light"
140
+ if not background.isdigit():
141
+ return None
142
+ return "dark" if int(background) in (0, 1, 2, 3, 4, 5, 6, 8) else "light"
143
+
144
+
145
+ def detect_terminal_mode(timeout: float = TIMEOUT) -> TerminalMode:
146
+ """The terminal's background, as far as it will say. Dark when it will not."""
147
+ from_env = mode_from_colorfgbg(os.environ.get("COLORFGBG"))
148
+ if from_env is not None:
149
+ return from_env
150
+ reply = _query_background(timeout)
151
+ is_light = background_is_light(reply) if reply else None
152
+ if is_light is None:
153
+ return "dark"
154
+ return "light" if is_light else "dark"
155
+
156
+
157
+ def _query_background(timeout: float) -> str:
158
+ """Ask the tty directly and read whatever comes back inside `timeout`.
159
+
160
+ Raw mode for the duration and restored in a `finally`: leaving a terminal without
161
+ echo is worse than picking the wrong theme, and this runs before Textual has taken
162
+ the tty over. Every failure — no tty, no termios (Windows), a terminal that writes
163
+ nothing — returns the empty string, which the caller reads as "no answer".
164
+ """
165
+ if not (sys.stdin.isatty() and sys.stdout.isatty()):
166
+ return ""
167
+ try:
168
+ import termios
169
+ import tty
170
+ except ImportError: # pragma: no cover — Windows
171
+ return ""
172
+
173
+ fd = sys.stdin.fileno()
174
+ try:
175
+ saved = termios.tcgetattr(fd)
176
+ except termios.error:
177
+ return ""
178
+ try:
179
+ tty.setraw(fd, termios.TCSANOW)
180
+ sys.stdout.write(QUERY)
181
+ sys.stdout.flush()
182
+ chunks: list[str] = []
183
+ deadline_left = timeout
184
+ while deadline_left > 0:
185
+ ready, _, _ = select.select([fd], [], [], deadline_left)
186
+ if not ready:
187
+ break
188
+ chunks.append(os.read(fd, 64).decode("utf-8", errors="replace"))
189
+ text = "".join(chunks)
190
+ # Stop at the terminator the reply actually used — BEL or ST — rather than
191
+ # waiting out the whole budget on a terminal that already answered.
192
+ if text.endswith("\x07") or text.endswith("\x1b\\") or REPLY.search(text):
193
+ break
194
+ deadline_left = timeout / 4
195
+ return "".join(chunks)
196
+ except (OSError, ValueError):
197
+ return ""
198
+ finally:
199
+ with_suppressed_error = getattr(termios, "error", Exception)
200
+ try:
201
+ termios.tcsetattr(fd, termios.TCSADRAIN, saved)
202
+ except with_suppressed_error: # pragma: no cover
203
+ pass
204
+
205
+
206
+ __all__ = [
207
+ "BRAND_TEXT_ON_LIGHT",
208
+ "PRIMARY",
209
+ "QUERY",
210
+ "STCODE_DARK",
211
+ "STCODE_LIGHT",
212
+ "THEMES",
213
+ "TIMEOUT",
214
+ "TerminalMode",
215
+ "background_is_light",
216
+ "brand_text",
217
+ "detect_terminal_mode",
218
+ "error_text",
219
+ "mode_from_colorfgbg",
220
+ "theme_name_for",
221
+ ]
@@ -0,0 +1,7 @@
1
+ """`python -m stcode.core` — the daemon, same as `stcode-daemon`."""
2
+
3
+ import sys
4
+
5
+ from stcode.core.daemon.main import main
6
+
7
+ sys.exit(main())
@@ -0,0 +1,41 @@
1
+ """
2
+ Agent — the turn loop, and the sub-agent tool that spawns another one.
3
+
4
+ agent = await Agent.create(config, cwd=workspace, role="backend-dev")
5
+
6
+ async for ev in agent.run("Add rate limiting to the API"):
7
+ match ev:
8
+ case TextDelta(): out(ev.text)
9
+ case ToolStarted(): spinner(ev.name)
10
+ case ToolFinished(): done(ev.name)
11
+ case TurnFinished(): idle(ev.usage)
12
+ case AgentFailed(): error(ev.message)
13
+
14
+ `push()` + `events()` is the same machine with the door left open — see `agent.py`.
15
+ """
16
+
17
+ from stcode.core.agent.agent import DEFAULT_MAX_TURNS, Agent
18
+ from stcode.core.agent.events import (
19
+ AgentEvent,
20
+ AgentFailed,
21
+ ReasoningDelta,
22
+ TextDelta,
23
+ ToolFinished,
24
+ ToolStarted,
25
+ TurnFinished,
26
+ )
27
+ from stcode.core.agent.task import MAX_DEPTH, make_task_tool
28
+
29
+ __all__ = [
30
+ "DEFAULT_MAX_TURNS",
31
+ "MAX_DEPTH",
32
+ "Agent",
33
+ "AgentEvent",
34
+ "AgentFailed",
35
+ "ReasoningDelta",
36
+ "TextDelta",
37
+ "ToolFinished",
38
+ "ToolStarted",
39
+ "TurnFinished",
40
+ "make_task_tool",
41
+ ]