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/cli.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Seed Code command-line entry point (``seedcode``).
|
|
2
|
+
|
|
3
|
+
Thin wrapper: prepares the Windows console, builds the UI, hands off to the
|
|
4
|
+
application controller (:mod:`seedcode.app`), and provides the final safety
|
|
5
|
+
net so a stray exception is shown as a friendly message rather than a
|
|
6
|
+
traceback. Heavy imports are deferred into :func:`main` so ``--version``
|
|
7
|
+
(used by installers to verify the install) answers instantly.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import sys
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _prepare_console() -> None:
|
|
16
|
+
"""Make stdout/stderr UTF-8-safe, primarily for Windows.
|
|
17
|
+
|
|
18
|
+
Interactive Windows consoles accept Unicode natively (PEP 528), but a
|
|
19
|
+
redirected stream falls back to the legacy ANSI code page (e.g. cp1252),
|
|
20
|
+
where the banner's box-drawing characters would raise UnicodeEncodeError.
|
|
21
|
+
Reconfiguring to UTF-8 with ``errors="replace"`` makes output safe in
|
|
22
|
+
Windows Terminal, PowerShell, CMD, and when piped to files.
|
|
23
|
+
"""
|
|
24
|
+
for stream in (sys.stdout, sys.stderr):
|
|
25
|
+
try:
|
|
26
|
+
stream.reconfigure(encoding="utf-8", errors="replace") # type: ignore[union-attr]
|
|
27
|
+
except (AttributeError, OSError, ValueError):
|
|
28
|
+
pass # exotic streams (tests, embedders) — never fatal
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
_HELP = """\
|
|
32
|
+
Seed Code - a terminal-based AI coding assistant. Plant ideas. Grow code.
|
|
33
|
+
|
|
34
|
+
Usage:
|
|
35
|
+
seedcode Start the interactive app (chat + menu)
|
|
36
|
+
seedcode --version Print the version and exit
|
|
37
|
+
seedcode --help Show this help and exit
|
|
38
|
+
|
|
39
|
+
Inside the app, type /help for commands: /provider, /model, /agent,
|
|
40
|
+
/permission, /settings, /doctor, and more.
|
|
41
|
+
|
|
42
|
+
Docs: https://github.com/Alshahriar-07/seedcode-cli
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def main() -> None:
|
|
47
|
+
"""Console-script entry point (``seedcode``)."""
|
|
48
|
+
# --version/--help must work non-interactively and fast (installers and
|
|
49
|
+
# scripts verify with them), so handle both before any UI, config, or
|
|
50
|
+
# logging work happens.
|
|
51
|
+
if len(sys.argv) > 1 and sys.argv[1] in ("--version", "-V"):
|
|
52
|
+
from . import __version__
|
|
53
|
+
|
|
54
|
+
print(f"Seed Code v{__version__}")
|
|
55
|
+
return
|
|
56
|
+
if len(sys.argv) > 1 and sys.argv[1] in ("--help", "-h"):
|
|
57
|
+
print(_HELP)
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
_prepare_console()
|
|
61
|
+
|
|
62
|
+
from .utils.logger import get_logger, setup_logging
|
|
63
|
+
|
|
64
|
+
setup_logging()
|
|
65
|
+
log = get_logger("cli")
|
|
66
|
+
|
|
67
|
+
from . import __version__
|
|
68
|
+
|
|
69
|
+
log.info(
|
|
70
|
+
"Seed Code v%s starting (python %s on %s)",
|
|
71
|
+
__version__,
|
|
72
|
+
sys.version.split()[0],
|
|
73
|
+
sys.platform,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
from .app import run
|
|
77
|
+
from .ui import UI
|
|
78
|
+
|
|
79
|
+
ui = UI()
|
|
80
|
+
try:
|
|
81
|
+
run(ui)
|
|
82
|
+
log.info("Clean exit.")
|
|
83
|
+
except KeyboardInterrupt:
|
|
84
|
+
ui.blank()
|
|
85
|
+
ui.dim("Interrupted.")
|
|
86
|
+
log.info("Exited via Ctrl+C.")
|
|
87
|
+
except Exception as exc: # final safety net — never show a traceback
|
|
88
|
+
log.exception("Fatal error")
|
|
89
|
+
ui.error(f"Fatal error: {exc}")
|
|
90
|
+
ui.dim("Details were logged to ~/.seedcode/logs/seedcode.log")
|
|
91
|
+
sys.exit(1)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
if __name__ == "__main__":
|
|
95
|
+
main()
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Slash-command system for the Seed Code REPL.
|
|
2
|
+
|
|
3
|
+
Commands are registered in a table and dispatched by name. Each handler receives
|
|
4
|
+
the live :class:`CommandContext` and returns a :class:`CommandResult` telling the
|
|
5
|
+
REPL whether to keep looping or exit. Handlers live in the sibling modules
|
|
6
|
+
(:mod:`help`, :mod:`clear`, :mod:`history`, :mod:`about`) and are imported at the
|
|
7
|
+
bottom of this file so their ``@command`` decorators populate the registry.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from typing import Callable
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class CommandContext:
|
|
18
|
+
"""Everything a command handler might need to act on."""
|
|
19
|
+
|
|
20
|
+
ui: "object" # UI; typed loosely to avoid an import cycle.
|
|
21
|
+
config: "object" # AppConfig
|
|
22
|
+
engine: "object" # ChatEngine
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class CommandResult:
|
|
27
|
+
"""Signals control flow back to the REPL."""
|
|
28
|
+
|
|
29
|
+
should_exit: bool = False
|
|
30
|
+
handled: bool = True
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
Handler = Callable[[CommandContext, str], CommandResult]
|
|
34
|
+
|
|
35
|
+
# name -> (handler, help text). Populated via the @command decorator below.
|
|
36
|
+
_REGISTRY: dict[str, tuple[Handler, str]] = {}
|
|
37
|
+
_ALIASES: dict[str, str] = {}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def command(
|
|
41
|
+
name: str, help_text: str, aliases: tuple[str, ...] = ()
|
|
42
|
+
) -> Callable[[Handler], Handler]:
|
|
43
|
+
def wrap(func: Handler) -> Handler:
|
|
44
|
+
_REGISTRY[name] = (func, help_text)
|
|
45
|
+
for alias in aliases:
|
|
46
|
+
_ALIASES[alias] = name
|
|
47
|
+
return func
|
|
48
|
+
|
|
49
|
+
return wrap
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def is_command(text: str) -> bool:
|
|
53
|
+
return text.strip().startswith("/")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def dispatch(ctx: CommandContext, text: str) -> CommandResult:
|
|
57
|
+
"""Route ``text`` (a '/...' string) to its handler."""
|
|
58
|
+
parts = text.strip().split(maxsplit=1)
|
|
59
|
+
name = parts[0].lstrip("/").lower()
|
|
60
|
+
arg = parts[1] if len(parts) > 1 else ""
|
|
61
|
+
name = _ALIASES.get(name, name)
|
|
62
|
+
|
|
63
|
+
entry = _REGISTRY.get(name)
|
|
64
|
+
if entry is None:
|
|
65
|
+
ctx.ui.warning(f"Unknown command: /{name}. Type /help for the list.")
|
|
66
|
+
return CommandResult(handled=True)
|
|
67
|
+
return entry[0](ctx, arg)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# Import handler modules for their registration side effects. Deferred to the
|
|
71
|
+
# bottom so ``command`` / ``_REGISTRY`` already exist when the handlers load.
|
|
72
|
+
from . import about, agent, assist, clear, desktop, doctor, help, history, palette, provider, theme # noqa: E402,F401
|
|
73
|
+
|
|
74
|
+
__all__ = [
|
|
75
|
+
"CommandContext",
|
|
76
|
+
"CommandResult",
|
|
77
|
+
"command",
|
|
78
|
+
"dispatch",
|
|
79
|
+
"is_command",
|
|
80
|
+
"_REGISTRY",
|
|
81
|
+
]
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Meta commands: /about, /exit."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from rich.text import Text
|
|
6
|
+
|
|
7
|
+
from .. import TAGLINE, __author__, __version__
|
|
8
|
+
from ..core.providers import provider_label
|
|
9
|
+
from . import CommandContext, CommandResult, command
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def show_about(ui, config) -> None:
|
|
13
|
+
"""Render the About panel (shared by /about and the main menu)."""
|
|
14
|
+
body = Text()
|
|
15
|
+
body.append("Seed Code\n", style="seed.primary")
|
|
16
|
+
body.append(f"{TAGLINE}\n\n", style="seed.accent")
|
|
17
|
+
body.append(f"Version {__version__}\n", style="seed.text")
|
|
18
|
+
body.append(f"Author {__author__}\n", style="seed.text")
|
|
19
|
+
body.append(f"Provider {provider_label(config.provider)}\n", style="seed.text")
|
|
20
|
+
body.append(f"Model {config.model or '(none)'}\n\n", style="seed.text")
|
|
21
|
+
body.append("A premium terminal-based AI coding assistant.", style="seed.dim")
|
|
22
|
+
ui.panel(body, title="About")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@command("about", "About Seed Code")
|
|
26
|
+
def _about(ctx: CommandContext, arg: str) -> CommandResult:
|
|
27
|
+
show_about(ctx.ui, ctx.config)
|
|
28
|
+
return CommandResult()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@command("exit", "Leave the chat (back to the main menu)", aliases=("quit", "q", "menu"))
|
|
32
|
+
def _exit(ctx: CommandContext, arg: str) -> CommandResult:
|
|
33
|
+
ctx.ui.dim("Back to the menu.")
|
|
34
|
+
return CommandResult(should_exit=True)
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""Legacy commands: /agent and /permission.
|
|
2
|
+
|
|
3
|
+
The old Agent Mode was merged into Assist Mode — /agent now routes there.
|
|
4
|
+
/permission keeps its dedicated interactive picker; /index and /tools stay
|
|
5
|
+
as inspection commands.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from ..config import save_config
|
|
11
|
+
from ..tools import TOOL_REGISTRY, PermissionManager, PermissionMode
|
|
12
|
+
from ..tools.filesystem import build_index
|
|
13
|
+
from ..ui.selector import Option, select
|
|
14
|
+
from . import CommandContext, CommandResult, command
|
|
15
|
+
from .assist import disable_assist, enable_assist
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@command("agent", "Legacy alias for Assist Mode. Usage: /agent [on|off]")
|
|
19
|
+
def _agent(ctx: CommandContext, arg: str) -> CommandResult:
|
|
20
|
+
raw = arg.strip().lower()
|
|
21
|
+
if raw in ("on", "off"):
|
|
22
|
+
enable = raw == "on"
|
|
23
|
+
elif not raw:
|
|
24
|
+
enable = not ctx.config.agent_mode # bare /agent toggles
|
|
25
|
+
else:
|
|
26
|
+
ctx.ui.warning("Usage: /agent [on|off]")
|
|
27
|
+
return CommandResult()
|
|
28
|
+
|
|
29
|
+
# Agent Mode was merged into Assist Mode — route there transparently.
|
|
30
|
+
ctx.ui.dim("(/agent is now Assist Mode)")
|
|
31
|
+
if enable:
|
|
32
|
+
enable_assist(ctx.ui, ctx.config)
|
|
33
|
+
else:
|
|
34
|
+
disable_assist(ctx.ui, ctx.config)
|
|
35
|
+
return CommandResult()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@command("permission", "Show or set the Assist permission mode", aliases=("perm",))
|
|
39
|
+
def _permission(ctx: CommandContext, arg: str) -> CommandResult:
|
|
40
|
+
raw = arg.strip()
|
|
41
|
+
detail = {
|
|
42
|
+
PermissionMode.READ_ONLY: "inspect only — no writes, no commands",
|
|
43
|
+
PermissionMode.WORKSPACE: "edit and run inside this directory only",
|
|
44
|
+
PermissionMode.DESKTOP: "control this computer (mouse, keyboard, apps)",
|
|
45
|
+
PermissionMode.FULL_SYSTEM: "no path restriction + sensitive actions (use with care)",
|
|
46
|
+
}
|
|
47
|
+
if not raw:
|
|
48
|
+
current = PermissionMode.parse(ctx.config.permission_mode)
|
|
49
|
+
chosen = select(
|
|
50
|
+
[
|
|
51
|
+
Option(mode.label, mode.value_str, detail=detail[mode])
|
|
52
|
+
for mode in PermissionMode
|
|
53
|
+
],
|
|
54
|
+
title="Permission Level",
|
|
55
|
+
initial=current.value_str,
|
|
56
|
+
searchable=False,
|
|
57
|
+
hint="↑↓ move Enter select Esc keep current",
|
|
58
|
+
)
|
|
59
|
+
if chosen is None:
|
|
60
|
+
ctx.ui.dim(f"Permission unchanged ({current.label}).")
|
|
61
|
+
return CommandResult()
|
|
62
|
+
raw = str(chosen)
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
mode = PermissionMode.parse(raw)
|
|
66
|
+
except ValueError as exc:
|
|
67
|
+
ctx.ui.warning(str(exc))
|
|
68
|
+
return CommandResult()
|
|
69
|
+
ctx.config.permission_mode = mode.value_str
|
|
70
|
+
save_config(ctx.config)
|
|
71
|
+
ctx.ui.success(f"Permission mode set to {mode.label}.")
|
|
72
|
+
return CommandResult()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@command("index", "Show a compact tree of the current project")
|
|
76
|
+
def _index(ctx: CommandContext, arg: str) -> CommandResult:
|
|
77
|
+
perm = PermissionManager(mode=PermissionMode.READ_ONLY)
|
|
78
|
+
ctx.ui.panel(build_index(perm), title="Project Index")
|
|
79
|
+
return CommandResult()
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@command("tools", "List the tools available in Assist Mode")
|
|
83
|
+
def _tools(ctx: CommandContext, arg: str) -> CommandResult:
|
|
84
|
+
from ..ui.layout import columns_grid
|
|
85
|
+
|
|
86
|
+
rows = []
|
|
87
|
+
for name in sorted(TOOL_REGISTRY):
|
|
88
|
+
tool = TOOL_REGISTRY[name]
|
|
89
|
+
kind = "changes files/system" if tool.mutates else "read-only"
|
|
90
|
+
rows.append((name, f"{tool.description} ({kind})"))
|
|
91
|
+
ctx.ui.panel(
|
|
92
|
+
columns_grid(rows, ("seed.primary", "seed.text")), title="Assist Tools"
|
|
93
|
+
)
|
|
94
|
+
return CommandResult()
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""Assist Mode: unified AI + computer control.
|
|
2
|
+
|
|
3
|
+
/assist on — enables the full capability set (AI, filesystem, terminal,
|
|
4
|
+
git, browser, keyboard, mouse, windows, vision, OCR, desktop
|
|
5
|
+
automation).
|
|
6
|
+
/assist off — back to plain chat.
|
|
7
|
+
|
|
8
|
+
Assist Mode is the ONLY automation mode Seed Code exposes. The old Agent
|
|
9
|
+
and Desktop modes were merged into it; their commands now route here.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from rich.table import Table
|
|
15
|
+
|
|
16
|
+
from ..computer import is_available
|
|
17
|
+
from ..config import save_config
|
|
18
|
+
from ..tools import TOOL_REGISTRY, PermissionMode
|
|
19
|
+
from . import CommandContext, CommandResult, command
|
|
20
|
+
|
|
21
|
+
# The Assist capability set, in display order. Desktop-engine rows are
|
|
22
|
+
# marked so they can be dimmed when the Computer Engine is unavailable.
|
|
23
|
+
_CAPABILITIES: tuple[tuple[str, str, bool], ...] = (
|
|
24
|
+
("AI", "Chat, reasoning, and code generation", False),
|
|
25
|
+
("Filesystem", "Read, write, edit, search, and organise files", False),
|
|
26
|
+
("Terminal", "Run shell commands with live output", False),
|
|
27
|
+
("Git", "Status, diff, log, commit, push, pull", False),
|
|
28
|
+
("Browser", "Navigate, click, type, search", True),
|
|
29
|
+
("Keyboard", "Type, hotkeys, shortcuts", True),
|
|
30
|
+
("Mouse", "Move, click, drag, scroll", True),
|
|
31
|
+
("Windows", "List, focus, open, close", True),
|
|
32
|
+
("Vision", "See and understand the screen", True),
|
|
33
|
+
("OCR", "Read text from the screen", True),
|
|
34
|
+
("Desktop Automation", "Multi-step computer control", True),
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@command("assist", "Enable/disable Assist Mode (unified AI + computer control)")
|
|
39
|
+
def _assist(ctx: CommandContext, arg: str) -> CommandResult:
|
|
40
|
+
raw = arg.strip().lower()
|
|
41
|
+
if raw in ("on", "off"):
|
|
42
|
+
enable = raw == "on"
|
|
43
|
+
elif not raw:
|
|
44
|
+
# Bare /assist: show status
|
|
45
|
+
_show_status(ctx)
|
|
46
|
+
return CommandResult()
|
|
47
|
+
else:
|
|
48
|
+
ctx.ui.warning("Usage: /assist [on|off]")
|
|
49
|
+
return CommandResult()
|
|
50
|
+
|
|
51
|
+
if enable:
|
|
52
|
+
enable_assist(ctx.ui, ctx.config)
|
|
53
|
+
else:
|
|
54
|
+
disable_assist(ctx.ui, ctx.config)
|
|
55
|
+
return CommandResult()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def capability_table(desktop_ok: bool) -> Table:
|
|
59
|
+
"""The Assist capability list (✓ rows; unavailable rows dim).
|
|
60
|
+
|
|
61
|
+
OCR is probed independently of the rest of the desktop stack: it needs a
|
|
62
|
+
native engine the Python packages do not supply, so marking it available
|
|
63
|
+
purely because the Computer Engine imports would tell the user something
|
|
64
|
+
untrue.
|
|
65
|
+
"""
|
|
66
|
+
table = Table.grid(padding=(0, 2))
|
|
67
|
+
table.add_column(justify="right", no_wrap=True)
|
|
68
|
+
table.add_column()
|
|
69
|
+
ocr_ok = _ocr_available() if desktop_ok else False
|
|
70
|
+
for name, detail, needs_desktop in _CAPABILITIES:
|
|
71
|
+
available = desktop_ok if needs_desktop else True
|
|
72
|
+
if name == "OCR":
|
|
73
|
+
available = ocr_ok
|
|
74
|
+
if available:
|
|
75
|
+
table.add_row(f"[seed.success]✓ {name}[/seed.success]", f"[seed.text]{detail}[/seed.text]")
|
|
76
|
+
else:
|
|
77
|
+
table.add_row(f"[seed.dim]○ {name}[/seed.dim]", f"[seed.dim]{detail}[/seed.dim]")
|
|
78
|
+
return table
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _ocr_available() -> bool:
|
|
82
|
+
"""Whether the OCR tier can genuinely run (best-effort)."""
|
|
83
|
+
try:
|
|
84
|
+
from ..computer import ocr
|
|
85
|
+
|
|
86
|
+
return ocr.available()
|
|
87
|
+
except Exception:
|
|
88
|
+
return False
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def enable_assist(ui, config) -> None:
|
|
92
|
+
"""Enable Assist Mode: the full capability set in one switch."""
|
|
93
|
+
config.agent_mode = True
|
|
94
|
+
|
|
95
|
+
# Desktop capabilities require the Computer Engine. When available, Assist
|
|
96
|
+
# runs at the ``desktop`` level so the AI can drive the computer; otherwise
|
|
97
|
+
# it stays at ``workspace`` (AI + filesystem + terminal + git still work).
|
|
98
|
+
desktop_ok, desktop_reason = is_available()
|
|
99
|
+
config.permission_mode = (
|
|
100
|
+
PermissionMode.DESKTOP.value_str if desktop_ok
|
|
101
|
+
else PermissionMode.WORKSPACE.value_str
|
|
102
|
+
)
|
|
103
|
+
save_config(config)
|
|
104
|
+
|
|
105
|
+
ui.success("Assist Mode ON")
|
|
106
|
+
ui.blank()
|
|
107
|
+
ui.panel(capability_table(desktop_ok), title="Assist Mode")
|
|
108
|
+
if not desktop_ok:
|
|
109
|
+
ui.dim(f"Desktop capabilities unavailable: {desktop_reason}")
|
|
110
|
+
ui.blank()
|
|
111
|
+
|
|
112
|
+
level_label = "desktop" if desktop_ok else "workspace"
|
|
113
|
+
ui.dim(f"Permission level: {level_label} — change in Settings › Advanced or /permission.")
|
|
114
|
+
|
|
115
|
+
# Ask for routine desktop control ONCE, here, instead of interrupting every
|
|
116
|
+
# action later. Sensitive actions still confirm individually.
|
|
117
|
+
if desktop_ok:
|
|
118
|
+
request_session_permissions(ui)
|
|
119
|
+
ui.dim("The AI picks the right tools for each task automatically.")
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def request_session_permissions(ui) -> bool:
|
|
123
|
+
"""Request every routine desktop permission for the session, in one prompt.
|
|
124
|
+
|
|
125
|
+
Returns whether the session-wide grant was given. Declining is safe: the
|
|
126
|
+
engine simply falls back to confirming each action as it comes, which is
|
|
127
|
+
the old behaviour. Sensitive actions (registry writes, secrets, system
|
|
128
|
+
power, deletions, purchases) are never granted here — they always ask.
|
|
129
|
+
"""
|
|
130
|
+
from ..computer.permissions import (
|
|
131
|
+
CATEGORY_LABELS,
|
|
132
|
+
SESSION_GRANT_CATEGORIES,
|
|
133
|
+
session_permissions,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
session = session_permissions()
|
|
137
|
+
if session.requested:
|
|
138
|
+
return bool(session.granted)
|
|
139
|
+
|
|
140
|
+
wanted = "\n".join(
|
|
141
|
+
f" • {CATEGORY_LABELS.get(c, c)}" for c in SESSION_GRANT_CATEGORIES
|
|
142
|
+
)
|
|
143
|
+
description = (
|
|
144
|
+
"Grant these for this session so Assist doesn't interrupt every step:\n"
|
|
145
|
+
f"{wanted}\n"
|
|
146
|
+
"Sensitive actions (registry writes, passwords, system power, deletions, "
|
|
147
|
+
"purchases) will still ask each time."
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
try:
|
|
151
|
+
answer = ui.confirm_desktop("Assist Mode session permissions", description)
|
|
152
|
+
except Exception:
|
|
153
|
+
# No interactive prompt available (headless/non-TTY): leave the
|
|
154
|
+
# per-action flow in place rather than silently granting anything.
|
|
155
|
+
return False
|
|
156
|
+
|
|
157
|
+
allowed = answer in ("y", "a")
|
|
158
|
+
if allowed:
|
|
159
|
+
session.request(SESSION_GRANT_CATEGORIES, allow=True)
|
|
160
|
+
ui.dim("Desktop control granted for this session — you won't be asked again.")
|
|
161
|
+
else:
|
|
162
|
+
ui.dim("Declined: Assist will ask before each desktop action.")
|
|
163
|
+
return allowed
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def disable_assist(ui, config) -> None:
|
|
167
|
+
"""Disable Assist Mode: back to plain chat."""
|
|
168
|
+
from ..computer.permissions import session_permissions
|
|
169
|
+
|
|
170
|
+
config.agent_mode = False
|
|
171
|
+
# Drop back to the safe editing level (removes desktop capability).
|
|
172
|
+
config.permission_mode = PermissionMode.WORKSPACE.value_str
|
|
173
|
+
save_config(config)
|
|
174
|
+
# Forget the session-wide desktop grant: turning Assist back on asks again.
|
|
175
|
+
session_permissions().reset()
|
|
176
|
+
ui.success("Assist Mode OFF — back to plain chat")
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _show_status(ctx: CommandContext) -> None:
|
|
180
|
+
"""Show current Assist Mode status."""
|
|
181
|
+
on = ctx.config.agent_mode
|
|
182
|
+
desktop_ok, desktop_reason = is_available()
|
|
183
|
+
|
|
184
|
+
table = Table.grid(padding=(0, 3))
|
|
185
|
+
table.add_column(style="seed.dim", justify="right", no_wrap=True)
|
|
186
|
+
table.add_column()
|
|
187
|
+
|
|
188
|
+
state = "[seed.accent]ON[/seed.accent]" if on else "[seed.dim]OFF[/seed.dim]"
|
|
189
|
+
table.add_row("Assist Mode", state)
|
|
190
|
+
table.add_row("Mode", "Assist" if on else "Chat")
|
|
191
|
+
if not desktop_ok:
|
|
192
|
+
table.add_row("Desktop", f"Unavailable: {desktop_reason}")
|
|
193
|
+
table.add_row("Permission", ctx.config.permission_mode.replace("_", " ").title())
|
|
194
|
+
|
|
195
|
+
core_tools = [n for n, t in TOOL_REGISTRY.items() if t.group == "core"]
|
|
196
|
+
desktop_tools = [n for n, t in TOOL_REGISTRY.items() if t.group == "desktop"]
|
|
197
|
+
table.add_row("Available tools", f"{len(core_tools)} core, {len(desktop_tools)} desktop")
|
|
198
|
+
|
|
199
|
+
ctx.ui.panel(table, title="Assist Mode")
|
|
200
|
+
ctx.ui.blank()
|
|
201
|
+
ctx.ui.dim("Toggle with: /assist on | /assist off")
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Screen and context commands: /clear, /reset."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from . import CommandContext, CommandResult, command
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@command("clear", "Clear the screen")
|
|
9
|
+
def _clear(ctx: CommandContext, arg: str) -> CommandResult:
|
|
10
|
+
# The branded banner renders exactly once at startup, so /clear only
|
|
11
|
+
# wipes the screen.
|
|
12
|
+
ctx.ui.console.clear()
|
|
13
|
+
return CommandResult()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@command("reset", "Forget the current conversation context")
|
|
17
|
+
def _reset(ctx: CommandContext, arg: str) -> CommandResult:
|
|
18
|
+
ctx.engine.reset() # type: ignore[attr-defined]
|
|
19
|
+
ctx.ui.success("Conversation context cleared.")
|
|
20
|
+
return CommandResult()
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Desktop-related commands: /desktop (legacy → Assist), /computer,
|
|
2
|
+
/screenshot, /windows.
|
|
3
|
+
|
|
4
|
+
The old Desktop Mode was merged into Assist Mode — /desktop now routes
|
|
5
|
+
there. /computer shows engine status; /screenshot and /windows work
|
|
6
|
+
immediately (no Assist turn needed) so the user can sanity-check the
|
|
7
|
+
engine by hand.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from rich.table import Table
|
|
13
|
+
|
|
14
|
+
from ..computer import INSTALL_HINT, is_available, missing_packages
|
|
15
|
+
from . import CommandContext, CommandResult, command
|
|
16
|
+
from .assist import disable_assist, enable_assist
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@command("desktop", "Legacy alias for Assist Mode. Usage: /desktop [on|off]")
|
|
20
|
+
def _desktop(ctx: CommandContext, arg: str) -> CommandResult:
|
|
21
|
+
raw = arg.strip().lower()
|
|
22
|
+
if raw in ("on", "off"):
|
|
23
|
+
enable = raw == "on"
|
|
24
|
+
elif not raw:
|
|
25
|
+
enable = not ctx.config.agent_mode # bare /desktop toggles Assist
|
|
26
|
+
else:
|
|
27
|
+
ctx.ui.warning("Usage: /desktop [on|off]")
|
|
28
|
+
return CommandResult()
|
|
29
|
+
|
|
30
|
+
# Desktop Mode was merged into Assist Mode — route there transparently.
|
|
31
|
+
ctx.ui.dim("(/desktop is now Assist Mode)")
|
|
32
|
+
if enable:
|
|
33
|
+
enable_assist(ctx.ui, ctx.config)
|
|
34
|
+
else:
|
|
35
|
+
disable_assist(ctx.ui, ctx.config)
|
|
36
|
+
return CommandResult()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@command("computer", "Show Computer Engine status and desktop permissions")
|
|
40
|
+
def _computer(ctx: CommandContext, arg: str) -> CommandResult:
|
|
41
|
+
ok, reason = is_available()
|
|
42
|
+
|
|
43
|
+
table = Table.grid(padding=(0, 3))
|
|
44
|
+
table.add_column(style="seed.dim", justify="right", no_wrap=True)
|
|
45
|
+
table.add_column(style="seed.text")
|
|
46
|
+
table.add_row("Mode", "Assist" if ctx.config.agent_mode else "Chat (/assist on)")
|
|
47
|
+
table.add_row("Engine", reason if not ok else "Available")
|
|
48
|
+
missing = missing_packages()
|
|
49
|
+
if missing:
|
|
50
|
+
table.add_row("Missing", f"{', '.join(missing)} → {INSTALL_HINT}")
|
|
51
|
+
|
|
52
|
+
if ok:
|
|
53
|
+
try:
|
|
54
|
+
from ..tools.desktop import get_controller
|
|
55
|
+
|
|
56
|
+
table.add_row("Screen", get_controller().screen_info().split("\n")[0])
|
|
57
|
+
except Exception:
|
|
58
|
+
pass
|
|
59
|
+
from ..tools import TOOL_REGISTRY
|
|
60
|
+
|
|
61
|
+
names = sorted(n for n, t in TOOL_REGISTRY.items() if t.group == "desktop")
|
|
62
|
+
table.add_row("Tools", ", ".join(names))
|
|
63
|
+
|
|
64
|
+
ctx.ui.panel(table, title="Computer Engine")
|
|
65
|
+
return CommandResult()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@command("screenshot", "Capture a screenshot now. Usage: /screenshot [path]")
|
|
69
|
+
def _screenshot(ctx: CommandContext, arg: str) -> CommandResult:
|
|
70
|
+
ok, reason = is_available()
|
|
71
|
+
if not ok:
|
|
72
|
+
ctx.ui.error(reason)
|
|
73
|
+
return CommandResult()
|
|
74
|
+
from pathlib import Path
|
|
75
|
+
|
|
76
|
+
from ..tools.desktop import get_controller
|
|
77
|
+
from ..computer.controller import ComputerError
|
|
78
|
+
|
|
79
|
+
save_to = Path(arg.strip()).expanduser() if arg.strip() else None
|
|
80
|
+
try:
|
|
81
|
+
path = get_controller().screenshot(save_to=save_to)
|
|
82
|
+
except ComputerError as exc:
|
|
83
|
+
ctx.ui.error(str(exc))
|
|
84
|
+
return CommandResult()
|
|
85
|
+
ctx.ui.success(f"Screenshot saved: {path}")
|
|
86
|
+
return CommandResult()
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@command("windows", "List all open windows")
|
|
90
|
+
def _windows(ctx: CommandContext, arg: str) -> CommandResult:
|
|
91
|
+
ok, reason = is_available()
|
|
92
|
+
if not ok:
|
|
93
|
+
ctx.ui.error(reason)
|
|
94
|
+
return CommandResult()
|
|
95
|
+
from ..tools.desktop import get_controller
|
|
96
|
+
from ..computer.controller import ComputerError
|
|
97
|
+
|
|
98
|
+
try:
|
|
99
|
+
listing = get_controller().list_windows()
|
|
100
|
+
except ComputerError as exc:
|
|
101
|
+
ctx.ui.error(str(exc))
|
|
102
|
+
return CommandResult()
|
|
103
|
+
ctx.ui.panel(listing, title="Open Windows")
|
|
104
|
+
return CommandResult()
|