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.
Files changed (114) hide show
  1. seedcode/__init__.py +14 -0
  2. seedcode/__main__.py +12 -0
  3. seedcode/app.py +508 -0
  4. seedcode/apps/__init__.py +32 -0
  5. seedcode/apps/discovery.py +241 -0
  6. seedcode/apps/installer.py +164 -0
  7. seedcode/apps/launcher.py +156 -0
  8. seedcode/apps/verifier.py +119 -0
  9. seedcode/assets/logo.txt +15 -0
  10. seedcode/cli.py +95 -0
  11. seedcode/commands/__init__.py +81 -0
  12. seedcode/commands/about.py +34 -0
  13. seedcode/commands/agent.py +94 -0
  14. seedcode/commands/assist.py +201 -0
  15. seedcode/commands/clear.py +20 -0
  16. seedcode/commands/desktop.py +104 -0
  17. seedcode/commands/doctor.py +152 -0
  18. seedcode/commands/help.py +61 -0
  19. seedcode/commands/history.py +365 -0
  20. seedcode/commands/palette.py +100 -0
  21. seedcode/commands/provider.py +451 -0
  22. seedcode/commands/theme.py +76 -0
  23. seedcode/computer/__init__.py +98 -0
  24. seedcode/computer/browser.py +276 -0
  25. seedcode/computer/browser_cdp.py +567 -0
  26. seedcode/computer/browser_engine.py +546 -0
  27. seedcode/computer/browser_extract.py +301 -0
  28. seedcode/computer/browser_popups.py +329 -0
  29. seedcode/computer/browser_selenium.py +209 -0
  30. seedcode/computer/browser_skills.py +245 -0
  31. seedcode/computer/catalog.py +200 -0
  32. seedcode/computer/controller.py +324 -0
  33. seedcode/computer/dispatcher.py +272 -0
  34. seedcode/computer/dpi.py +185 -0
  35. seedcode/computer/engine.py +105 -0
  36. seedcode/computer/keyboard.py +101 -0
  37. seedcode/computer/logbook.py +104 -0
  38. seedcode/computer/mouse.py +48 -0
  39. seedcode/computer/ocr.py +213 -0
  40. seedcode/computer/operator_skills.py +577 -0
  41. seedcode/computer/permissions.py +203 -0
  42. seedcode/computer/recovery.py +115 -0
  43. seedcode/computer/registry.py +107 -0
  44. seedcode/computer/resolver.py +434 -0
  45. seedcode/computer/screen.py +130 -0
  46. seedcode/computer/screen_state.py +412 -0
  47. seedcode/computer/selfguard.py +197 -0
  48. seedcode/computer/semantic.py +100 -0
  49. seedcode/computer/skills.py +139 -0
  50. seedcode/computer/state.py +199 -0
  51. seedcode/computer/verifier.py +177 -0
  52. seedcode/computer/vision.py +327 -0
  53. seedcode/computer/windows.py +217 -0
  54. seedcode/config/__init__.py +8 -0
  55. seedcode/config/defaults.py +22 -0
  56. seedcode/config/manager.py +62 -0
  57. seedcode/core/__init__.py +31 -0
  58. seedcode/core/agent.py +534 -0
  59. seedcode/core/chat.py +128 -0
  60. seedcode/core/client.py +9 -0
  61. seedcode/core/errors.py +199 -0
  62. seedcode/core/identity.py +66 -0
  63. seedcode/core/identity_store.py +119 -0
  64. seedcode/core/lifecycle.py +240 -0
  65. seedcode/core/limits.py +35 -0
  66. seedcode/core/models.py +347 -0
  67. seedcode/core/project.py +96 -0
  68. seedcode/core/providers/__init__.py +58 -0
  69. seedcode/core/providers/aerolink.py +324 -0
  70. seedcode/core/providers/base.py +230 -0
  71. seedcode/core/providers/freemodel.py +931 -0
  72. seedcode/core/providers/ollama.py +262 -0
  73. seedcode/core/providers/openrouter.py +393 -0
  74. seedcode/core/streaming.py +21 -0
  75. seedcode/memory/__init__.py +8 -0
  76. seedcode/memory/manager.py +47 -0
  77. seedcode/memory/storage.py +38 -0
  78. seedcode/memory/store.py +257 -0
  79. seedcode/tools/__init__.py +35 -0
  80. seedcode/tools/base.py +179 -0
  81. seedcode/tools/desktop.py +371 -0
  82. seedcode/tools/filesystem.py +309 -0
  83. seedcode/tools/git.py +72 -0
  84. seedcode/tools/patch.py +170 -0
  85. seedcode/tools/permissions.py +288 -0
  86. seedcode/tools/search.py +137 -0
  87. seedcode/tools/terminal.py +200 -0
  88. seedcode/tools/textio.py +59 -0
  89. seedcode/ui/__init__.py +164 -0
  90. seedcode/ui/badges.py +64 -0
  91. seedcode/ui/banner.py +78 -0
  92. seedcode/ui/dashboard.py +197 -0
  93. seedcode/ui/dialog.py +62 -0
  94. seedcode/ui/fuzzy.py +128 -0
  95. seedcode/ui/layout.py +54 -0
  96. seedcode/ui/menu.py +61 -0
  97. seedcode/ui/palette.py +40 -0
  98. seedcode/ui/progress.py +41 -0
  99. seedcode/ui/prompts.py +16 -0
  100. seedcode/ui/renderer.py +36 -0
  101. seedcode/ui/searchbox.py +70 -0
  102. seedcode/ui/selector.py +514 -0
  103. seedcode/ui/statusbar.py +38 -0
  104. seedcode/ui/textbox.py +61 -0
  105. seedcode/ui/theme.py +204 -0
  106. seedcode/ui/tree.py +91 -0
  107. seedcode/utils/__init__.py +22 -0
  108. seedcode/utils/helpers.py +97 -0
  109. seedcode/utils/logger.py +65 -0
  110. seedcode_cli-6.1.5.dist-info/METADATA +368 -0
  111. seedcode_cli-6.1.5.dist-info/RECORD +114 -0
  112. seedcode_cli-6.1.5.dist-info/WHEEL +4 -0
  113. seedcode_cli-6.1.5.dist-info/entry_points.txt +2 -0
  114. 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())
@@ -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)