zettcode 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.
- zettcode/__init__.py +6 -0
- zettcode/cli.py +78 -0
- zettcode/config.py +72 -0
- zettcode/runtime.py +121 -0
- zettcode/tui.py +924 -0
- zettcode/tui_framework/__init__.py +29 -0
- zettcode/tui_framework/application.py +91 -0
- zettcode/tui_framework/components.py +821 -0
- zettcode/tui_framework/events.py +37 -0
- zettcode/tui_framework/input.py +163 -0
- zettcode/tui_framework/screen.py +214 -0
- zettcode/tui_framework/terminal.py +118 -0
- zettcode-0.1.0.dist-info/METADATA +109 -0
- zettcode-0.1.0.dist-info/RECORD +17 -0
- zettcode-0.1.0.dist-info/WHEEL +4 -0
- zettcode-0.1.0.dist-info/entry_points.txt +2 -0
- zettcode-0.1.0.dist-info/licenses/LICENSE +21 -0
zettcode/__init__.py
ADDED
zettcode/cli.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Command-line entry point for the ZettCode TUI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import asyncio
|
|
7
|
+
import os
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from zett_agent import ReasoningEffort
|
|
11
|
+
|
|
12
|
+
from .config import ProviderName, ZettCodeConfig, default_model, provider_api_key
|
|
13
|
+
from .runtime import ZettCodeRuntime
|
|
14
|
+
from .tui import ZettCodeTUI
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def parse_args(argv: list[str] | None = None) -> ZettCodeConfig:
|
|
18
|
+
"""Parse CLI arguments into a validated immutable configuration."""
|
|
19
|
+
default_provider = os.getenv("ZETTCODE_PROVIDER") or ProviderName.DEEPSEEK.value
|
|
20
|
+
parser = argparse.ArgumentParser(description="Run the ZettCode terminal coding agent")
|
|
21
|
+
parser.add_argument("workspace", nargs="?", type=Path, default=Path.cwd())
|
|
22
|
+
parser.add_argument("--provider", choices=[item.value for item in ProviderName], default=default_provider)
|
|
23
|
+
parser.add_argument("--model")
|
|
24
|
+
parser.add_argument("--api-key", help="Prefer provider environment variables to avoid shell history exposure")
|
|
25
|
+
parser.add_argument("--base-url")
|
|
26
|
+
parser.add_argument("--responses-api", action="store_true")
|
|
27
|
+
parser.add_argument("--session")
|
|
28
|
+
parser.add_argument("--database", type=Path, default=Path.home() / ".zettcode" / "sessions.sqlite3")
|
|
29
|
+
parser.add_argument(
|
|
30
|
+
"--reasoning-effort",
|
|
31
|
+
choices=[item.value for item in ReasoningEffort],
|
|
32
|
+
default=ReasoningEffort.MEDIUM.value,
|
|
33
|
+
)
|
|
34
|
+
parser.add_argument("--serial-tools", action="store_true")
|
|
35
|
+
parser.add_argument("--max-iterations", type=int, default=36)
|
|
36
|
+
parser.add_argument("--compaction-max-tokens", type=int, default=128_000)
|
|
37
|
+
parser.add_argument("--compaction-keep-tokens", type=int, default=32_000)
|
|
38
|
+
args = parser.parse_args(argv)
|
|
39
|
+
try:
|
|
40
|
+
provider = ProviderName(args.provider)
|
|
41
|
+
except ValueError:
|
|
42
|
+
parser.error(f"Unsupported provider: {args.provider!r} (check ZETTCODE_PROVIDER)")
|
|
43
|
+
try:
|
|
44
|
+
return ZettCodeConfig(
|
|
45
|
+
workspace=args.workspace,
|
|
46
|
+
provider=provider,
|
|
47
|
+
model=args.model or default_model(provider),
|
|
48
|
+
api_key=args.api_key or provider_api_key(provider),
|
|
49
|
+
database=args.database,
|
|
50
|
+
session_id=args.session,
|
|
51
|
+
base_url=args.base_url,
|
|
52
|
+
responses_api=args.responses_api,
|
|
53
|
+
reasoning_effort=ReasoningEffort(args.reasoning_effort),
|
|
54
|
+
parallel_tool_call=not args.serial_tools,
|
|
55
|
+
max_iterations=args.max_iterations,
|
|
56
|
+
compaction_max_tokens=args.compaction_max_tokens,
|
|
57
|
+
compaction_keep_tokens=args.compaction_keep_tokens,
|
|
58
|
+
)
|
|
59
|
+
except ValueError as error:
|
|
60
|
+
parser.error(str(error))
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
async def async_main(config: ZettCodeConfig) -> None:
|
|
64
|
+
"""Own runtime lifecycle around the full-screen application."""
|
|
65
|
+
runtime = await ZettCodeRuntime.create(config)
|
|
66
|
+
try:
|
|
67
|
+
await ZettCodeTUI(runtime).run()
|
|
68
|
+
finally:
|
|
69
|
+
await runtime.aclose()
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def main() -> None:
|
|
73
|
+
"""Installed console-script entry point."""
|
|
74
|
+
asyncio.run(async_main(parse_args()))
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
if __name__ == "__main__":
|
|
78
|
+
main()
|
zettcode/config.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Validated construction settings for the ZettCode runtime."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from enum import StrEnum
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from zett_agent import ReasoningEffort
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ProviderName(StrEnum):
|
|
14
|
+
"""Provider adapters exposed by the initial CLI."""
|
|
15
|
+
|
|
16
|
+
DEEPSEEK = "deepseek"
|
|
17
|
+
OPENAI = "openai"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True, slots=True)
|
|
21
|
+
class ZettCodeConfig:
|
|
22
|
+
"""All process-level settings needed to construct one coding runtime."""
|
|
23
|
+
|
|
24
|
+
workspace: Path
|
|
25
|
+
provider: ProviderName
|
|
26
|
+
model: str
|
|
27
|
+
api_key: str
|
|
28
|
+
database: Path
|
|
29
|
+
session_id: str | None = None
|
|
30
|
+
base_url: str | None = None
|
|
31
|
+
responses_api: bool = False
|
|
32
|
+
reasoning_effort: ReasoningEffort = ReasoningEffort.MEDIUM
|
|
33
|
+
parallel_tool_call: bool = True
|
|
34
|
+
max_iterations: int = 36
|
|
35
|
+
compaction_max_tokens: int = 128_000
|
|
36
|
+
compaction_keep_tokens: int = 32_000
|
|
37
|
+
|
|
38
|
+
def __post_init__(self) -> None:
|
|
39
|
+
workspace = self.workspace.expanduser().resolve()
|
|
40
|
+
database = self.database.expanduser().resolve()
|
|
41
|
+
if not workspace.is_dir():
|
|
42
|
+
raise ValueError(f"Workspace is not a directory: {workspace}")
|
|
43
|
+
if not self.model.strip():
|
|
44
|
+
raise ValueError("Model cannot be empty")
|
|
45
|
+
if not self.api_key.strip():
|
|
46
|
+
raise ValueError(f"Missing API key for {self.provider.value}")
|
|
47
|
+
if self.max_iterations < 1:
|
|
48
|
+
raise ValueError("max_iterations must be positive")
|
|
49
|
+
if self.compaction_keep_tokens < 1:
|
|
50
|
+
raise ValueError("compaction_keep_tokens must be positive")
|
|
51
|
+
if self.compaction_max_tokens <= self.compaction_keep_tokens:
|
|
52
|
+
raise ValueError("compaction_max_tokens must be greater than compaction_keep_tokens")
|
|
53
|
+
object.__setattr__(self, "workspace", workspace)
|
|
54
|
+
object.__setattr__(self, "database", database)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def provider_api_key(provider: ProviderName) -> str:
|
|
58
|
+
"""Read the selected provider credential without logging it."""
|
|
59
|
+
match provider:
|
|
60
|
+
case ProviderName.DEEPSEEK:
|
|
61
|
+
return os.getenv("DEEPSEEK_API_KEY") or os.getenv("DEEPSEEK_API", "")
|
|
62
|
+
case ProviderName.OPENAI:
|
|
63
|
+
return os.getenv("OPENAI_API_KEY", "")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def default_model(provider: ProviderName) -> str:
|
|
67
|
+
"""Return a conservative default model for one provider."""
|
|
68
|
+
match provider:
|
|
69
|
+
case ProviderName.DEEPSEEK:
|
|
70
|
+
return "deepseek-chat"
|
|
71
|
+
case ProviderName.OPENAI:
|
|
72
|
+
return "gpt-5-mini"
|
zettcode/runtime.py
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""Application runtime that composes AgentClient with coding extensions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import platform
|
|
7
|
+
import sys
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
|
|
11
|
+
from zett_agent import (
|
|
12
|
+
AgentClient,
|
|
13
|
+
AgentRunConfig,
|
|
14
|
+
CodingExtension,
|
|
15
|
+
CompactionExtension,
|
|
16
|
+
DeepSeekProvider,
|
|
17
|
+
OpenAIProvider,
|
|
18
|
+
SQLiteSessionExtension,
|
|
19
|
+
ToolGuidelinesExtension,
|
|
20
|
+
create_agent,
|
|
21
|
+
new_uuid7,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
from .config import ProviderName, ZettCodeConfig
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def build_system_prompt(config: ZettCodeConfig) -> str:
|
|
28
|
+
"""Build request instructions with an explicit workspace boundary."""
|
|
29
|
+
now = datetime.now().astimezone()
|
|
30
|
+
return f"""You are ZettCode, a focused coding agent working with the user in one local workspace.
|
|
31
|
+
|
|
32
|
+
Inspect relevant files before editing. Make focused changes, preserve unrelated work,
|
|
33
|
+
and run checks proportional to risk. Explain blockers concretely. Do not perform
|
|
34
|
+
destructive actions unless the user explicitly requests them.
|
|
35
|
+
|
|
36
|
+
Runtime environment:
|
|
37
|
+
- Workspace: {config.workspace}
|
|
38
|
+
- Local time: {now.isoformat(timespec="seconds")}
|
|
39
|
+
- Operating system: {platform.platform()}
|
|
40
|
+
- Architecture: {platform.machine() or "unknown"}
|
|
41
|
+
- Python: {platform.python_implementation()} {platform.python_version()}
|
|
42
|
+
- Executable: {sys.executable}
|
|
43
|
+
- Shell: {os.getenv("SHELL", "unknown")}
|
|
44
|
+
|
|
45
|
+
Treat the workspace as the current working directory. Prefer relative paths in tool
|
|
46
|
+
calls. Verify mutable facts with tools instead of assuming this startup snapshot is current."""
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass(slots=True)
|
|
50
|
+
class ZettCodeRuntime:
|
|
51
|
+
"""Own the model, persistence extension, AgentClient, and active session."""
|
|
52
|
+
|
|
53
|
+
config: ZettCodeConfig
|
|
54
|
+
client: AgentClient
|
|
55
|
+
persistence: SQLiteSessionExtension
|
|
56
|
+
model: OpenAIProvider | DeepSeekProvider
|
|
57
|
+
session_id: str
|
|
58
|
+
|
|
59
|
+
@classmethod
|
|
60
|
+
async def create(cls, config: ZettCodeConfig) -> ZettCodeRuntime:
|
|
61
|
+
"""Create all owned resources after changing into the chosen workspace."""
|
|
62
|
+
os.chdir(config.workspace)
|
|
63
|
+
config.database.parent.mkdir(parents=True, exist_ok=True)
|
|
64
|
+
persistence = SQLiteSessionExtension(config.database)
|
|
65
|
+
recent = await persistence.list_sessions(limit=1)
|
|
66
|
+
session_id = config.session_id or (recent[0].session_id if recent else new_uuid7())
|
|
67
|
+
match config.provider:
|
|
68
|
+
case ProviderName.DEEPSEEK:
|
|
69
|
+
model = DeepSeekProvider(
|
|
70
|
+
config.model,
|
|
71
|
+
config.api_key,
|
|
72
|
+
base_url=config.base_url,
|
|
73
|
+
response=config.responses_api,
|
|
74
|
+
)
|
|
75
|
+
case ProviderName.OPENAI:
|
|
76
|
+
model = OpenAIProvider(
|
|
77
|
+
config.model,
|
|
78
|
+
config.api_key,
|
|
79
|
+
base_url=config.base_url,
|
|
80
|
+
response=config.responses_api,
|
|
81
|
+
)
|
|
82
|
+
try:
|
|
83
|
+
client = await create_agent(
|
|
84
|
+
model,
|
|
85
|
+
config=AgentRunConfig(session_id=session_id),
|
|
86
|
+
system_prompt=build_system_prompt(config),
|
|
87
|
+
extensions=[
|
|
88
|
+
CodingExtension(),
|
|
89
|
+
persistence,
|
|
90
|
+
ToolGuidelinesExtension(),
|
|
91
|
+
CompactionExtension(
|
|
92
|
+
model,
|
|
93
|
+
max_tokens=config.compaction_max_tokens,
|
|
94
|
+
keep_recent_tokens=config.compaction_keep_tokens,
|
|
95
|
+
),
|
|
96
|
+
],
|
|
97
|
+
reasoning_effort=config.reasoning_effort,
|
|
98
|
+
parallel_tool_call=config.parallel_tool_call,
|
|
99
|
+
max_iterations=config.max_iterations,
|
|
100
|
+
)
|
|
101
|
+
except BaseException:
|
|
102
|
+
await model.aclose()
|
|
103
|
+
await persistence.close()
|
|
104
|
+
raise
|
|
105
|
+
return cls(config, client, persistence, model, session_id)
|
|
106
|
+
|
|
107
|
+
def new_session(self) -> str:
|
|
108
|
+
"""Switch future requests to a fresh session identity."""
|
|
109
|
+
self.session_id = new_uuid7()
|
|
110
|
+
return self.session_id
|
|
111
|
+
|
|
112
|
+
def use_session(self, session_id: str) -> None:
|
|
113
|
+
"""Select a persisted or new explicit session for the next request."""
|
|
114
|
+
if not session_id.strip():
|
|
115
|
+
raise ValueError("Session ID cannot be empty")
|
|
116
|
+
self.session_id = session_id.strip()
|
|
117
|
+
|
|
118
|
+
async def aclose(self) -> None:
|
|
119
|
+
"""Release all resources owned by this runtime."""
|
|
120
|
+
await self.model.aclose()
|
|
121
|
+
await self.persistence.close()
|