seedcode-cli 6.1.5__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.
- seedcode/__init__.py +14 -0
- seedcode/__main__.py +12 -0
- seedcode/app.py +508 -0
- seedcode/apps/__init__.py +32 -0
- seedcode/apps/discovery.py +241 -0
- seedcode/apps/installer.py +164 -0
- seedcode/apps/launcher.py +156 -0
- seedcode/apps/verifier.py +119 -0
- seedcode/assets/logo.txt +15 -0
- seedcode/cli.py +95 -0
- seedcode/commands/__init__.py +81 -0
- seedcode/commands/about.py +34 -0
- seedcode/commands/agent.py +94 -0
- seedcode/commands/assist.py +201 -0
- seedcode/commands/clear.py +20 -0
- seedcode/commands/desktop.py +104 -0
- seedcode/commands/doctor.py +152 -0
- seedcode/commands/help.py +61 -0
- seedcode/commands/history.py +365 -0
- seedcode/commands/palette.py +100 -0
- seedcode/commands/provider.py +451 -0
- seedcode/commands/theme.py +76 -0
- seedcode/computer/__init__.py +98 -0
- seedcode/computer/browser.py +276 -0
- seedcode/computer/browser_cdp.py +567 -0
- seedcode/computer/browser_engine.py +546 -0
- seedcode/computer/browser_extract.py +301 -0
- seedcode/computer/browser_popups.py +329 -0
- seedcode/computer/browser_selenium.py +209 -0
- seedcode/computer/browser_skills.py +245 -0
- seedcode/computer/catalog.py +200 -0
- seedcode/computer/controller.py +324 -0
- seedcode/computer/dispatcher.py +272 -0
- seedcode/computer/dpi.py +185 -0
- seedcode/computer/engine.py +105 -0
- seedcode/computer/keyboard.py +101 -0
- seedcode/computer/logbook.py +104 -0
- seedcode/computer/mouse.py +48 -0
- seedcode/computer/ocr.py +213 -0
- seedcode/computer/operator_skills.py +577 -0
- seedcode/computer/permissions.py +203 -0
- seedcode/computer/recovery.py +115 -0
- seedcode/computer/registry.py +107 -0
- seedcode/computer/resolver.py +434 -0
- seedcode/computer/screen.py +130 -0
- seedcode/computer/screen_state.py +412 -0
- seedcode/computer/selfguard.py +197 -0
- seedcode/computer/semantic.py +100 -0
- seedcode/computer/skills.py +139 -0
- seedcode/computer/state.py +199 -0
- seedcode/computer/verifier.py +177 -0
- seedcode/computer/vision.py +327 -0
- seedcode/computer/windows.py +217 -0
- seedcode/config/__init__.py +8 -0
- seedcode/config/defaults.py +22 -0
- seedcode/config/manager.py +62 -0
- seedcode/core/__init__.py +31 -0
- seedcode/core/agent.py +534 -0
- seedcode/core/chat.py +128 -0
- seedcode/core/client.py +9 -0
- seedcode/core/errors.py +199 -0
- seedcode/core/identity.py +66 -0
- seedcode/core/identity_store.py +119 -0
- seedcode/core/lifecycle.py +240 -0
- seedcode/core/limits.py +35 -0
- seedcode/core/models.py +347 -0
- seedcode/core/project.py +96 -0
- seedcode/core/providers/__init__.py +58 -0
- seedcode/core/providers/aerolink.py +324 -0
- seedcode/core/providers/base.py +230 -0
- seedcode/core/providers/freemodel.py +931 -0
- seedcode/core/providers/ollama.py +262 -0
- seedcode/core/providers/openrouter.py +393 -0
- seedcode/core/streaming.py +21 -0
- seedcode/memory/__init__.py +8 -0
- seedcode/memory/manager.py +47 -0
- seedcode/memory/storage.py +38 -0
- seedcode/memory/store.py +257 -0
- seedcode/tools/__init__.py +35 -0
- seedcode/tools/base.py +179 -0
- seedcode/tools/desktop.py +371 -0
- seedcode/tools/filesystem.py +309 -0
- seedcode/tools/git.py +72 -0
- seedcode/tools/patch.py +170 -0
- seedcode/tools/permissions.py +288 -0
- seedcode/tools/search.py +137 -0
- seedcode/tools/terminal.py +200 -0
- seedcode/tools/textio.py +59 -0
- seedcode/ui/__init__.py +164 -0
- seedcode/ui/badges.py +64 -0
- seedcode/ui/banner.py +78 -0
- seedcode/ui/dashboard.py +197 -0
- seedcode/ui/dialog.py +62 -0
- seedcode/ui/fuzzy.py +128 -0
- seedcode/ui/layout.py +54 -0
- seedcode/ui/menu.py +61 -0
- seedcode/ui/palette.py +40 -0
- seedcode/ui/progress.py +41 -0
- seedcode/ui/prompts.py +16 -0
- seedcode/ui/renderer.py +36 -0
- seedcode/ui/searchbox.py +70 -0
- seedcode/ui/selector.py +514 -0
- seedcode/ui/statusbar.py +38 -0
- seedcode/ui/textbox.py +61 -0
- seedcode/ui/theme.py +204 -0
- seedcode/ui/tree.py +91 -0
- seedcode/utils/__init__.py +22 -0
- seedcode/utils/helpers.py +97 -0
- seedcode/utils/logger.py +65 -0
- seedcode_cli-6.1.5.dist-info/METADATA +368 -0
- seedcode_cli-6.1.5.dist-info/RECORD +114 -0
- seedcode_cli-6.1.5.dist-info/WHEEL +4 -0
- seedcode_cli-6.1.5.dist-info/entry_points.txt +2 -0
- seedcode_cli-6.1.5.dist-info/licenses/LICENSE +21 -0
seedcode/ui/theme.py
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""Seed Code theme system.
|
|
2
|
+
|
|
3
|
+
Multiple named palettes share one branding contract: a primary tone, an
|
|
4
|
+
accent, dim/text/warning/error/success roles, and a cursor-row background
|
|
5
|
+
for the interactive selector. The default "seed" palette is the classic
|
|
6
|
+
Seed Green identity; every other theme keeps the same structure so all
|
|
7
|
+
components render correctly under any of them.
|
|
8
|
+
|
|
9
|
+
Both rendering stacks read from here:
|
|
10
|
+
|
|
11
|
+
* Rich — :func:`rich_theme` builds a :class:`rich.theme.Theme` with the
|
|
12
|
+
``seed.*`` style names used across the app.
|
|
13
|
+
* prompt_toolkit — :func:`pt_style` builds the ``sel.*`` style classes used
|
|
14
|
+
by the interactive components in :mod:`seedcode.ui.selector` and friends.
|
|
15
|
+
|
|
16
|
+
The active theme is module-level state set from config at startup and by
|
|
17
|
+
the theme picker; components query it at render time so a theme switch is
|
|
18
|
+
instant everywhere.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from dataclasses import dataclass
|
|
24
|
+
|
|
25
|
+
from prompt_toolkit.styles import Style
|
|
26
|
+
from rich.theme import Theme
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True)
|
|
30
|
+
class Palette:
|
|
31
|
+
"""One named colour palette (all values are hex strings)."""
|
|
32
|
+
|
|
33
|
+
id: str
|
|
34
|
+
label: str
|
|
35
|
+
description: str
|
|
36
|
+
primary: str
|
|
37
|
+
accent: str
|
|
38
|
+
text: str
|
|
39
|
+
dim: str
|
|
40
|
+
warning: str
|
|
41
|
+
error: str
|
|
42
|
+
success: str
|
|
43
|
+
cursor_bg: str # selector highlighted-row background
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
PALETTES: dict[str, Palette] = {
|
|
47
|
+
p.id: p
|
|
48
|
+
for p in (
|
|
49
|
+
Palette(
|
|
50
|
+
id="seed",
|
|
51
|
+
label="Seed Green",
|
|
52
|
+
description="The classic Seed Code identity",
|
|
53
|
+
primary="#2ecc71",
|
|
54
|
+
accent="#7bed9f",
|
|
55
|
+
text="#ffffff",
|
|
56
|
+
dim="#9e9e9e",
|
|
57
|
+
warning="#f1c40f",
|
|
58
|
+
error="#e74c3c",
|
|
59
|
+
success="#2ecc71",
|
|
60
|
+
cursor_bg="#1d3b2a",
|
|
61
|
+
),
|
|
62
|
+
Palette(
|
|
63
|
+
id="forest",
|
|
64
|
+
label="Forest",
|
|
65
|
+
description="Deep greens and moss",
|
|
66
|
+
primary="#27ae60",
|
|
67
|
+
accent="#a3e4b8",
|
|
68
|
+
text="#e8f5e9",
|
|
69
|
+
dim="#7d8f84",
|
|
70
|
+
warning="#e6c229",
|
|
71
|
+
error="#e74c3c",
|
|
72
|
+
success="#27ae60",
|
|
73
|
+
cursor_bg="#16301f",
|
|
74
|
+
),
|
|
75
|
+
Palette(
|
|
76
|
+
id="ocean",
|
|
77
|
+
label="Ocean",
|
|
78
|
+
description="Calm blues and cyan",
|
|
79
|
+
primary="#3498db",
|
|
80
|
+
accent="#7fd6f2",
|
|
81
|
+
text="#eaf6fb",
|
|
82
|
+
dim="#8496a3",
|
|
83
|
+
warning="#f1c40f",
|
|
84
|
+
error="#e74c3c",
|
|
85
|
+
success="#2ecc71",
|
|
86
|
+
cursor_bg="#14303f",
|
|
87
|
+
),
|
|
88
|
+
Palette(
|
|
89
|
+
id="dusk",
|
|
90
|
+
label="Dusk",
|
|
91
|
+
description="Violet evening tones",
|
|
92
|
+
primary="#9b59b6",
|
|
93
|
+
accent="#d2a8e0",
|
|
94
|
+
text="#f5eefa",
|
|
95
|
+
dim="#988fa3",
|
|
96
|
+
warning="#f1c40f",
|
|
97
|
+
error="#e74c3c",
|
|
98
|
+
success="#2ecc71",
|
|
99
|
+
cursor_bg="#2d1f38",
|
|
100
|
+
),
|
|
101
|
+
Palette(
|
|
102
|
+
id="ember",
|
|
103
|
+
label="Ember",
|
|
104
|
+
description="Warm amber and orange",
|
|
105
|
+
primary="#e67e22",
|
|
106
|
+
accent="#f5b971",
|
|
107
|
+
text="#fdf3e7",
|
|
108
|
+
dim="#a3937f",
|
|
109
|
+
warning="#f1c40f",
|
|
110
|
+
error="#e74c3c",
|
|
111
|
+
success="#2ecc71",
|
|
112
|
+
cursor_bg="#3a2712",
|
|
113
|
+
),
|
|
114
|
+
Palette(
|
|
115
|
+
id="mono",
|
|
116
|
+
label="Monochrome",
|
|
117
|
+
description="Plain white on black",
|
|
118
|
+
primary="#ffffff",
|
|
119
|
+
accent="#c8c8c8",
|
|
120
|
+
text="#ffffff",
|
|
121
|
+
dim="#808080",
|
|
122
|
+
warning="#f1c40f",
|
|
123
|
+
error="#e74c3c",
|
|
124
|
+
success="#ffffff",
|
|
125
|
+
cursor_bg="#333333",
|
|
126
|
+
),
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
DEFAULT_THEME = "seed"
|
|
131
|
+
|
|
132
|
+
# Module-level active theme (set from config at startup, and live by the
|
|
133
|
+
# theme picker). Components read it at render time.
|
|
134
|
+
_active: str = DEFAULT_THEME
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def set_active_theme(name: str) -> Palette:
|
|
138
|
+
"""Set the active theme (unknown names fall back to the default)."""
|
|
139
|
+
global _active
|
|
140
|
+
_active = name if name in PALETTES else DEFAULT_THEME
|
|
141
|
+
return PALETTES[_active]
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def active_theme_name() -> str:
|
|
145
|
+
return _active
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def active_palette() -> Palette:
|
|
149
|
+
return PALETTES.get(_active, PALETTES[DEFAULT_THEME])
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def rich_theme(name: str | None = None) -> Theme:
|
|
153
|
+
"""Rich theme with the ``seed.*`` styles used across the app."""
|
|
154
|
+
p = PALETTES.get(name or _active, PALETTES[DEFAULT_THEME])
|
|
155
|
+
return Theme(
|
|
156
|
+
{
|
|
157
|
+
"seed.primary": f"bold {p.primary}",
|
|
158
|
+
"seed.accent": p.accent,
|
|
159
|
+
"seed.text": p.text,
|
|
160
|
+
"seed.dim": p.dim,
|
|
161
|
+
"seed.warning": p.warning,
|
|
162
|
+
"seed.error": f"bold {p.error}",
|
|
163
|
+
"seed.success": p.success,
|
|
164
|
+
"seed.prompt": f"bold {p.primary}",
|
|
165
|
+
"seed.assistant": p.accent,
|
|
166
|
+
"markdown.code": p.accent,
|
|
167
|
+
}
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def pt_style(name: str | None = None) -> Style:
|
|
172
|
+
"""prompt_toolkit style with the ``sel.*`` classes the components use."""
|
|
173
|
+
p = PALETTES.get(name or _active, PALETTES[DEFAULT_THEME])
|
|
174
|
+
return Style.from_dict(
|
|
175
|
+
{
|
|
176
|
+
"prompt": f"bold {p.primary}",
|
|
177
|
+
"sel.title": f"bold {p.primary}",
|
|
178
|
+
"sel.breadcrumb": p.dim,
|
|
179
|
+
"sel.breadcrumb.here": f"bold {p.accent}",
|
|
180
|
+
"sel.searchlabel": f"bold {p.primary}",
|
|
181
|
+
"sel.query": p.text,
|
|
182
|
+
"sel.placeholder": f"italic {p.dim}",
|
|
183
|
+
"sel.pointer": f"bold {p.primary}",
|
|
184
|
+
"sel.cursorline": f"bg:{p.cursor_bg}",
|
|
185
|
+
"sel.text": p.text,
|
|
186
|
+
"sel.dim": p.dim,
|
|
187
|
+
"sel.match": f"bold underline {p.accent}",
|
|
188
|
+
"sel.group": f"bold {p.accent}",
|
|
189
|
+
"sel.hint": p.dim,
|
|
190
|
+
"sel.counter": p.dim,
|
|
191
|
+
"sel.ok": p.success,
|
|
192
|
+
"sel.warn": p.warning,
|
|
193
|
+
"sel.err": f"bold {p.error}",
|
|
194
|
+
"sel.off": p.dim,
|
|
195
|
+
"sel.scroll": p.dim,
|
|
196
|
+
"sel.swatch.primary": f"bg:{p.primary}",
|
|
197
|
+
"sel.swatch.accent": f"bg:{p.accent}",
|
|
198
|
+
"sel.swatch.dim": f"bg:{p.dim}",
|
|
199
|
+
}
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
# Backwards-compatible export: the classic Seed Green Rich theme.
|
|
204
|
+
SEED_THEME = rich_theme(DEFAULT_THEME)
|
seedcode/ui/tree.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Nested navigation trees: settings screens with breadcrumbs.
|
|
2
|
+
|
|
3
|
+
A :class:`TreeNode` is either a branch (children) or a leaf (an action
|
|
4
|
+
callback). :func:`navigate` walks the tree with the interactive selector,
|
|
5
|
+
showing a ``Settings › Providers › FreeModel Claude`` breadcrumb at every
|
|
6
|
+
level. Esc goes up one level; Esc at the root exits. Leaf callbacks return
|
|
7
|
+
True to stay on the current level (so a value edit refreshes in place) or
|
|
8
|
+
False/None to exit the whole tree.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from typing import Callable, Sequence
|
|
15
|
+
|
|
16
|
+
from .selector import Option, select
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class TreeNode:
|
|
21
|
+
"""One navigable node.
|
|
22
|
+
|
|
23
|
+
Branches set ``children`` (static) or ``build`` (computed per visit so
|
|
24
|
+
live values stay fresh). Leaves set ``action``. ``status`` is the dimmed
|
|
25
|
+
current-value column shown next to the label.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
label: str
|
|
29
|
+
children: Sequence["TreeNode"] = field(default_factory=tuple)
|
|
30
|
+
build: Callable[[], Sequence["TreeNode"]] | None = None
|
|
31
|
+
action: Callable[[], object] | None = None
|
|
32
|
+
status: str = ""
|
|
33
|
+
status_fn: Callable[[], str] | None = None
|
|
34
|
+
badge: str = ""
|
|
35
|
+
|
|
36
|
+
def resolve_children(self) -> Sequence["TreeNode"]:
|
|
37
|
+
if self.build is not None:
|
|
38
|
+
return self.build()
|
|
39
|
+
return self.children
|
|
40
|
+
|
|
41
|
+
def resolve_status(self) -> str:
|
|
42
|
+
if self.status_fn is not None:
|
|
43
|
+
try:
|
|
44
|
+
return self.status_fn()
|
|
45
|
+
except Exception:
|
|
46
|
+
return ""
|
|
47
|
+
return self.status
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def navigate(root: TreeNode, *, breadcrumbs: Sequence[str] = ()) -> None:
|
|
51
|
+
"""Walk ``root`` interactively until the user backs out of the top level."""
|
|
52
|
+
trail = list(breadcrumbs) or [root.label]
|
|
53
|
+
_navigate_level(root, trail)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _navigate_level(node: TreeNode, trail: list[str]) -> bool:
|
|
57
|
+
"""Show one level; returns False when the whole tree should exit."""
|
|
58
|
+
last = None
|
|
59
|
+
while True:
|
|
60
|
+
children = list(node.resolve_children())
|
|
61
|
+
if not children:
|
|
62
|
+
return True
|
|
63
|
+
options = [
|
|
64
|
+
Option(
|
|
65
|
+
child.label,
|
|
66
|
+
value=i,
|
|
67
|
+
columns=(child.resolve_status(),) if child.resolve_status() else (),
|
|
68
|
+
badge=child.badge,
|
|
69
|
+
)
|
|
70
|
+
for i, child in enumerate(children)
|
|
71
|
+
]
|
|
72
|
+
chosen = select(
|
|
73
|
+
options,
|
|
74
|
+
breadcrumbs=trail,
|
|
75
|
+
hint="↑↓ move Enter open Esc back",
|
|
76
|
+
initial=last,
|
|
77
|
+
)
|
|
78
|
+
if chosen is None:
|
|
79
|
+
return True # Esc: up one level
|
|
80
|
+
last = chosen
|
|
81
|
+
child = children[int(chosen)]
|
|
82
|
+
if child.action is not None:
|
|
83
|
+
try:
|
|
84
|
+
keep = child.action()
|
|
85
|
+
except (KeyboardInterrupt, EOFError):
|
|
86
|
+
keep = True
|
|
87
|
+
if keep is False:
|
|
88
|
+
return False
|
|
89
|
+
continue
|
|
90
|
+
if not _navigate_level(child, trail + [child.label]):
|
|
91
|
+
return False
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Utility helpers: filesystem paths, timestamps, and logging."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .helpers import (
|
|
6
|
+
app_dir,
|
|
7
|
+
config_path,
|
|
8
|
+
history_dir,
|
|
9
|
+
restrict_permissions,
|
|
10
|
+
session_id,
|
|
11
|
+
)
|
|
12
|
+
from .logger import get_logger, setup_logging
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"app_dir",
|
|
16
|
+
"config_path",
|
|
17
|
+
"get_logger",
|
|
18
|
+
"history_dir",
|
|
19
|
+
"restrict_permissions",
|
|
20
|
+
"session_id",
|
|
21
|
+
"setup_logging",
|
|
22
|
+
]
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Small shared helpers for Seed Code.
|
|
2
|
+
|
|
3
|
+
Kept dependency-light so importing this module stays cheap during startup.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import sys
|
|
9
|
+
import tempfile
|
|
10
|
+
import time
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def app_dir() -> Path:
|
|
15
|
+
"""Return the per-user Seed Code directory, creating it if needed.
|
|
16
|
+
|
|
17
|
+
Uses ``~/.seedcode`` on every platform for predictable, cross-platform
|
|
18
|
+
behaviour (Windows PowerShell, Linux, macOS). If the home directory is
|
|
19
|
+
unwritable (locked-down corporate machines), falls back to a temp
|
|
20
|
+
location so the app still starts.
|
|
21
|
+
"""
|
|
22
|
+
path = Path.home() / ".seedcode"
|
|
23
|
+
try:
|
|
24
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
25
|
+
except OSError:
|
|
26
|
+
path = Path(tempfile.gettempdir()) / "seedcode"
|
|
27
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
28
|
+
return path
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def bundled_path(*parts: str) -> Path:
|
|
32
|
+
"""Resolve a read-only asset that ships with Seed Code.
|
|
33
|
+
|
|
34
|
+
Works in both modes the app runs in:
|
|
35
|
+
|
|
36
|
+
* **frozen** (the PyInstaller one-file exe) — data files are unpacked to a
|
|
37
|
+
temp directory exposed as ``sys._MEIPASS``;
|
|
38
|
+
* **source / pip** — assets sit inside the installed ``seedcode`` package.
|
|
39
|
+
|
|
40
|
+
The path is returned whether or not it exists; callers decide what a
|
|
41
|
+
missing asset means.
|
|
42
|
+
"""
|
|
43
|
+
if getattr(sys, "frozen", False):
|
|
44
|
+
base = Path(getattr(sys, "_MEIPASS", "") or Path(sys.executable).parent)
|
|
45
|
+
else:
|
|
46
|
+
base = Path(__file__).resolve().parents[1] # -> seedcode/
|
|
47
|
+
return base.joinpath(*parts)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def install_dir() -> Path:
|
|
51
|
+
"""The directory Seed Code is installed in (next to the exe when frozen).
|
|
52
|
+
|
|
53
|
+
Distinct from :func:`bundled_path`: the Windows installer places large
|
|
54
|
+
payloads beside ``seedcode.exe`` rather than inside it, so they survive as
|
|
55
|
+
real files instead of being unpacked on every launch.
|
|
56
|
+
"""
|
|
57
|
+
if getattr(sys, "frozen", False):
|
|
58
|
+
return Path(sys.executable).resolve().parent
|
|
59
|
+
return Path(__file__).resolve().parents[2]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def config_path() -> Path:
|
|
63
|
+
"""Path to the JSON configuration file."""
|
|
64
|
+
return app_dir() / "config.json"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def history_dir(provider_id: str = "") -> Path:
|
|
68
|
+
"""Directory holding saved conversation transcripts.
|
|
69
|
+
|
|
70
|
+
Each provider keeps its own history under ``history/<provider_id>/`` so
|
|
71
|
+
switching backends never mixes conversations.
|
|
72
|
+
"""
|
|
73
|
+
path = app_dir() / "history"
|
|
74
|
+
if provider_id:
|
|
75
|
+
path = path / provider_id
|
|
76
|
+
try:
|
|
77
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
78
|
+
except OSError:
|
|
79
|
+
pass # history is best-effort; callers already tolerate write failures
|
|
80
|
+
return path
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def restrict_permissions(path: Path) -> None:
|
|
84
|
+
"""Best-effort: make a file readable/writable by the owner only.
|
|
85
|
+
|
|
86
|
+
On Windows this is a no-op (POSIX perms are ignored) but it never raises,
|
|
87
|
+
so callers can invoke it unconditionally.
|
|
88
|
+
"""
|
|
89
|
+
try:
|
|
90
|
+
path.chmod(0o600)
|
|
91
|
+
except (OSError, NotImplementedError):
|
|
92
|
+
pass
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def session_id() -> str:
|
|
96
|
+
"""Generate a filesystem-safe id for a chat session."""
|
|
97
|
+
return time.strftime("%Y%m%d-%H%M%S", time.localtime())
|
seedcode/utils/logger.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Logging for Seed Code.
|
|
2
|
+
|
|
3
|
+
Seed Code is a quiet CLI: nothing is ever logged to the terminal. Instead a
|
|
4
|
+
rotating file under ``~/.seedcode/logs/seedcode.log`` records startup, config
|
|
5
|
+
loading, API request metadata and errors so problems can be diagnosed after
|
|
6
|
+
the fact. API keys and message content are never logged.
|
|
7
|
+
|
|
8
|
+
Set ``SEEDCODE_DEBUG=1`` to raise the file log level from INFO to DEBUG.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import logging
|
|
14
|
+
import os
|
|
15
|
+
from logging.handlers import RotatingFileHandler
|
|
16
|
+
|
|
17
|
+
_ROOT = "seedcode"
|
|
18
|
+
_configured = False
|
|
19
|
+
|
|
20
|
+
# Keep the log small: two 512 KB files at most.
|
|
21
|
+
_MAX_BYTES = 512 * 1024
|
|
22
|
+
_BACKUP_COUNT = 1
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def setup_logging() -> None:
|
|
26
|
+
"""Attach the rotating file handler once. Never raises, never prints."""
|
|
27
|
+
global _configured
|
|
28
|
+
if _configured:
|
|
29
|
+
return
|
|
30
|
+
_configured = True
|
|
31
|
+
|
|
32
|
+
root = logging.getLogger(_ROOT)
|
|
33
|
+
root.setLevel(logging.DEBUG)
|
|
34
|
+
# Terminal stays silent even if no file handler could be attached.
|
|
35
|
+
root.addHandler(logging.NullHandler())
|
|
36
|
+
root.propagate = False
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
# Imported here so importing this module stays dependency-light.
|
|
40
|
+
from .helpers import app_dir
|
|
41
|
+
|
|
42
|
+
log_dir = app_dir() / "logs"
|
|
43
|
+
log_dir.mkdir(parents=True, exist_ok=True)
|
|
44
|
+
handler = RotatingFileHandler(
|
|
45
|
+
log_dir / "seedcode.log",
|
|
46
|
+
maxBytes=_MAX_BYTES,
|
|
47
|
+
backupCount=_BACKUP_COUNT,
|
|
48
|
+
encoding="utf-8",
|
|
49
|
+
)
|
|
50
|
+
debug = os.environ.get("SEEDCODE_DEBUG", "").strip() not in ("", "0")
|
|
51
|
+
handler.setLevel(logging.DEBUG if debug else logging.INFO)
|
|
52
|
+
handler.setFormatter(
|
|
53
|
+
logging.Formatter("%(asctime)s %(levelname)-7s %(name)s: %(message)s")
|
|
54
|
+
)
|
|
55
|
+
root.addHandler(handler)
|
|
56
|
+
except OSError:
|
|
57
|
+
# A read-only or full disk must never stop the app from starting.
|
|
58
|
+
pass
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def get_logger(name: str = _ROOT) -> logging.Logger:
|
|
62
|
+
"""Return a namespaced logger (silent until :func:`setup_logging` runs)."""
|
|
63
|
+
if not name.startswith(_ROOT):
|
|
64
|
+
name = f"{_ROOT}.{name}"
|
|
65
|
+
return logging.getLogger(name)
|