scootcli 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.
- scootcli/__init__.py +4 -0
- scootcli/__main__.py +9 -0
- scootcli/activity.py +26 -0
- scootcli/agent.py +350 -0
- scootcli/approvals.py +167 -0
- scootcli/auth.py +59 -0
- scootcli/cli.py +276 -0
- scootcli/clipboard.py +89 -0
- scootcli/commands/__init__.py +56 -0
- scootcli/commands/approve.py +38 -0
- scootcli/commands/auth.py +107 -0
- scootcli/commands/base.py +31 -0
- scootcli/commands/compact.py +40 -0
- scootcli/commands/copy.py +23 -0
- scootcli/commands/exit.py +14 -0
- scootcli/commands/forget.py +28 -0
- scootcli/commands/help.py +29 -0
- scootcli/commands/init.py +50 -0
- scootcli/commands/logo.py +51 -0
- scootcli/commands/model.py +61 -0
- scootcli/commands/panel.py +28 -0
- scootcli/commands/reset.py +20 -0
- scootcli/commands/resume.py +31 -0
- scootcli/commands/save.py +29 -0
- scootcli/commands/sessions.py +42 -0
- scootcli/commands/status.py +59 -0
- scootcli/commands/verbosity.py +57 -0
- scootcli/commands/worktree.py +64 -0
- scootcli/commands/yolo.py +20 -0
- scootcli/config.py +241 -0
- scootcli/context.py +82 -0
- scootcli/credentials.py +79 -0
- scootcli/errors.py +87 -0
- scootcli/images.py +169 -0
- scootcli/keys.py +119 -0
- scootcli/lineeditor.py +577 -0
- scootcli/logo.py +116 -0
- scootcli/models.py +120 -0
- scootcli/panel.py +263 -0
- scootcli/preferences.py +87 -0
- scootcli/presets.py +38 -0
- scootcli/project.py +94 -0
- scootcli/prompts.py +100 -0
- scootcli/providers/__init__.py +20 -0
- scootcli/providers/base.py +370 -0
- scootcli/providers/openai_chat.py +142 -0
- scootcli/providers/openai_responses.py +248 -0
- scootcli/providers/registry.py +173 -0
- scootcli/rendering.py +86 -0
- scootcli/repl.py +801 -0
- scootcli/sessions.py +186 -0
- scootcli/status.py +71 -0
- scootcli/tools/__init__.py +68 -0
- scootcli/tools/base.py +152 -0
- scootcli/tools/edit_file.py +72 -0
- scootcli/tools/list_dir.py +47 -0
- scootcli/tools/read_file.py +56 -0
- scootcli/tools/run_shell.py +73 -0
- scootcli/tools/search.py +170 -0
- scootcli/tools/update_plan.py +104 -0
- scootcli/tools/write_file.py +61 -0
- scootcli/transport.py +312 -0
- scootcli/vision.py +167 -0
- scootcli/workspace.py +105 -0
- scootcli/worktree.py +114 -0
- scootcli-0.1.0.dist-info/METADATA +238 -0
- scootcli-0.1.0.dist-info/RECORD +71 -0
- scootcli-0.1.0.dist-info/WHEEL +5 -0
- scootcli-0.1.0.dist-info/entry_points.txt +2 -0
- scootcli-0.1.0.dist-info/licenses/LICENSE +21 -0
- scootcli-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""/forget [<id>|all] — delete saved session(s) for this workspace root."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ..rendering import color, eprint
|
|
6
|
+
from . import register
|
|
7
|
+
from .base import SlashCommand
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _run(session, args: str):
|
|
11
|
+
from .. import sessions
|
|
12
|
+
|
|
13
|
+
arg = args.strip()
|
|
14
|
+
if arg == "all":
|
|
15
|
+
n = sessions.delete_all(str(session.config.root))
|
|
16
|
+
print(color(f"forgot {n} saved session(s) for this directory.", "gray"))
|
|
17
|
+
return
|
|
18
|
+
if not arg:
|
|
19
|
+
eprint(color("usage: /forget <id>|all (see /sessions).", "yellow"))
|
|
20
|
+
return
|
|
21
|
+
if sessions.delete(arg):
|
|
22
|
+
print(color(f"forgot session {arg}.", "gray"))
|
|
23
|
+
else:
|
|
24
|
+
eprint(color(f"no session with id '{arg}'.", "yellow"))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
register(SlashCommand("forget", "delete saved session(s)", _run, usage="<id>|all"))
|
|
28
|
+
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""/help — list available slash-commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ..rendering import color
|
|
6
|
+
from . import all_commands, register
|
|
7
|
+
from .base import SlashCommand
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _run(session, args: str):
|
|
11
|
+
if getattr(getattr(session, "config", None), "logo", True):
|
|
12
|
+
from ..logo import mascot
|
|
13
|
+
|
|
14
|
+
for row in mascot("idle", "micro"):
|
|
15
|
+
print(color(row, "cyan"))
|
|
16
|
+
print(color("slash-commands:", "bold"))
|
|
17
|
+
for name, cmd in all_commands().items():
|
|
18
|
+
usage = f" {cmd.usage}" if cmd.usage else ""
|
|
19
|
+
print(f" {color('/' + name, 'cyan')}{usage} {color('— ' + cmd.summary, 'gray')}")
|
|
20
|
+
print(color(" (or just type a prompt · esc to interrupt · ctrl-c to quit)", "gray"))
|
|
21
|
+
print(color(" keys: ↑/↓ recall history · ↓ on a draft clears it (↑ brings it back)", "gray"))
|
|
22
|
+
print(color(" feed detail: /verbosity full|compact|quiet (collapse tool calls / hide reasoning)",
|
|
23
|
+
"gray"))
|
|
24
|
+
print(color(" copy the last reply with /c or Ctrl-S (⌘C can't be captured by a terminal app)",
|
|
25
|
+
"gray"))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
register(SlashCommand("help", "show this help", _run))
|
|
29
|
+
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""/init — learn the project and write AGENTS.md (auto-loaded into the system prompt)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ..activity import activity
|
|
6
|
+
from ..errors import ScootError, Interrupted
|
|
7
|
+
from ..keys import read_key
|
|
8
|
+
from ..project import generate_agents_md
|
|
9
|
+
from ..rendering import color, eprint
|
|
10
|
+
from . import register
|
|
11
|
+
from .base import SlashCommand
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _run(session, args: str):
|
|
15
|
+
root = session.config.root
|
|
16
|
+
target = root / "AGENTS.md"
|
|
17
|
+
|
|
18
|
+
if target.exists():
|
|
19
|
+
print(color("AGENTS.md already exists — overwrite? [y/N] ", "yellow"), end="", flush=True)
|
|
20
|
+
key = read_key()
|
|
21
|
+
print(key)
|
|
22
|
+
if key != "y":
|
|
23
|
+
print(color("cancelled.", "gray"))
|
|
24
|
+
return
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
with activity("scanning project & writing AGENTS.md…") as cancel:
|
|
28
|
+
content = generate_agents_md(session, cancel)
|
|
29
|
+
except Interrupted:
|
|
30
|
+
print(color("⏹ interrupted", "yellow"))
|
|
31
|
+
return
|
|
32
|
+
except ScootError as exc:
|
|
33
|
+
eprint(color(f"/init failed: {exc}", "red"))
|
|
34
|
+
return
|
|
35
|
+
|
|
36
|
+
if not content:
|
|
37
|
+
eprint(color("model returned no content.", "yellow"))
|
|
38
|
+
return
|
|
39
|
+
try:
|
|
40
|
+
target.write_text(content, "utf-8")
|
|
41
|
+
except OSError as exc:
|
|
42
|
+
eprint(color(f"could not write AGENTS.md: {exc}", "red"))
|
|
43
|
+
return
|
|
44
|
+
print(color(
|
|
45
|
+
f"wrote {target} ({len(content.splitlines())} lines). "
|
|
46
|
+
"It will load into context automatically on future turns.", "gray"))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
register(SlashCommand("init", "learn the project → AGENTS.md", _run))
|
|
50
|
+
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""/logo — show the mascot, or turn it on/off (persisted across launches).
|
|
2
|
+
|
|
3
|
+
``off`` hides the launch banner art and the face in the status bar; the transcript label is unaffected
|
|
4
|
+
(see ``--no-emoji`` / ``SCOOT_EMOJI`` for that).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from .. import logo, preferences
|
|
10
|
+
from ..rendering import color, eprint
|
|
11
|
+
from . import register
|
|
12
|
+
from .base import SlashCommand
|
|
13
|
+
|
|
14
|
+
_ON = ("on", "true", "1", "yes", "show")
|
|
15
|
+
_OFF = ("off", "false", "0", "no", "hide")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _run(session, args: str):
|
|
19
|
+
want = (args or "").strip().lower()
|
|
20
|
+
cfg = session.config
|
|
21
|
+
if not want:
|
|
22
|
+
for row in logo.mascot("idle", "lean"):
|
|
23
|
+
print(color(row, "cyan"))
|
|
24
|
+
state = "on" if getattr(cfg, "logo", True) else "off"
|
|
25
|
+
print(color(f"logo: {state} (usage: /logo on|off · also --no-logo / SCOOT_LOGO=0)", "gray"))
|
|
26
|
+
return
|
|
27
|
+
if want in _ON:
|
|
28
|
+
on = True
|
|
29
|
+
elif want in _OFF:
|
|
30
|
+
on = False
|
|
31
|
+
else:
|
|
32
|
+
eprint(color(f"unknown option '{want}'. use: /logo on|off", "yellow"))
|
|
33
|
+
return
|
|
34
|
+
preferences.set_logo(on)
|
|
35
|
+
try:
|
|
36
|
+
session.config = cfg.override(logo=on) # Config is frozen: swap in a copy for the live session
|
|
37
|
+
except Exception:
|
|
38
|
+
pass
|
|
39
|
+
bar = getattr(session, "status_bar", None)
|
|
40
|
+
if bar is not None:
|
|
41
|
+
from ..auth import account_label, auth_source
|
|
42
|
+
from ..panel import build_status_text
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
bar.render(build_status_text(session, account_label() or auth_source()))
|
|
46
|
+
except Exception:
|
|
47
|
+
pass
|
|
48
|
+
print(color(f"logo {'on' if on else 'off'} (saved; the banner follows on the next launch).", "gray"))
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
register(SlashCommand("logo", "show the mascot, or /logo on|off to toggle it", _run, usage="[on|off]"))
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""/model — list available models or switch the active one ('auto' by default).
|
|
2
|
+
|
|
3
|
+
Models are ``provider/model`` ids (``openai/gpt-5.3-codex``, ``ollama/llama3.2``); a bare name means
|
|
4
|
+
the default provider. A switch made here is **persisted** (``~/.config/scoot/preferences.json``) so the
|
|
5
|
+
next launch reuses it. An explicit ``--model`` flag or ``SCOOT_MODEL`` still overrides it for that run.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from ..preferences import set_model
|
|
11
|
+
from ..providers.base import qualify, split_model_id
|
|
12
|
+
from ..rendering import color
|
|
13
|
+
from . import register
|
|
14
|
+
from .base import SlashCommand
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _run(session, args: str):
|
|
18
|
+
arg = args.strip()
|
|
19
|
+
available = session.available_models()
|
|
20
|
+
|
|
21
|
+
if not arg:
|
|
22
|
+
print(color(f"models (preference: {session.model} → active: {session.active_model})", "bold"))
|
|
23
|
+
current = ""
|
|
24
|
+
for mid in available:
|
|
25
|
+
head, _ = split_model_id(mid)
|
|
26
|
+
if head != current:
|
|
27
|
+
current = head
|
|
28
|
+
print(color(f" {head}", "bold"))
|
|
29
|
+
mark = color(" ← active", "green") if mid == session.active_model else ""
|
|
30
|
+
print(" " + color(mid, "cyan") + mark)
|
|
31
|
+
errors = getattr(getattr(session, "provider", None), "list_errors", None) or {}
|
|
32
|
+
for name, err in sorted(errors.items()):
|
|
33
|
+
print(color(f" {name}: unavailable ({err})", "yellow"))
|
|
34
|
+
print(color(" use: /model <provider/model> · /model default · /model auto (pick per task)", "gray"))
|
|
35
|
+
return
|
|
36
|
+
|
|
37
|
+
if arg in ("auto", "default"):
|
|
38
|
+
session.model = arg
|
|
39
|
+
session.active_model = session.resolved_model()
|
|
40
|
+
set_model(arg)
|
|
41
|
+
what = "picked per task from the live list" if arg == "auto" else f"the provider's preferred model, now {session.active_model}"
|
|
42
|
+
print(color(f"model preference set to {arg} ({what}); saved for next launch.", "gray"))
|
|
43
|
+
return
|
|
44
|
+
|
|
45
|
+
if arg not in available:
|
|
46
|
+
head, _ = split_model_id(arg)
|
|
47
|
+
if head is None:
|
|
48
|
+
try:
|
|
49
|
+
arg = qualify(session.provider.default_name, arg)
|
|
50
|
+
except Exception:
|
|
51
|
+
pass
|
|
52
|
+
if arg not in available:
|
|
53
|
+
print(color(f"warning: '{arg}' is not in the models list; setting it anyway.", "yellow"))
|
|
54
|
+
session.model = arg
|
|
55
|
+
session.active_model = arg
|
|
56
|
+
set_model(arg)
|
|
57
|
+
print(color(f"model set to {arg}; saved for next launch.", "gray"))
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
register(SlashCommand("model", "list or switch model (default|auto|provider/model)", _run, usage="[provider/model|default|auto]"))
|
|
61
|
+
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""/panel — toggle the persistent bottom status bar on/off."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ..rendering import color, eprint
|
|
6
|
+
from . import register
|
|
7
|
+
from .base import SlashCommand
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _run(session, args: str):
|
|
11
|
+
bar = getattr(session, "status_bar", None)
|
|
12
|
+
if bar is None:
|
|
13
|
+
eprint(color("status bar is not available in this context.", "yellow"))
|
|
14
|
+
return
|
|
15
|
+
on = bar.toggle()
|
|
16
|
+
if on:
|
|
17
|
+
from ..auth import account_label, auth_source
|
|
18
|
+
from ..panel import build_status_text
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
bar.render(build_status_text(session, account_label() or auth_source()))
|
|
22
|
+
except Exception:
|
|
23
|
+
pass
|
|
24
|
+
print(color(f"panel {'on' if on else 'off'}.", "gray"))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
register(SlashCommand("panel", "toggle the bottom status bar", _run))
|
|
28
|
+
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""/reset — clear the conversation history and context (keeps any loaded project context)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ..rendering import color
|
|
6
|
+
from . import register
|
|
7
|
+
from .base import SlashCommand
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _run(session, args: str):
|
|
11
|
+
session.reset()
|
|
12
|
+
redraw = getattr(session, "redraw_home", None)
|
|
13
|
+
if callable(redraw):
|
|
14
|
+
redraw("fresh session — conversation + context cleared.")
|
|
15
|
+
else:
|
|
16
|
+
print(color("conversation + context cleared — fresh session.", "gray"))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
register(SlashCommand("reset", "clear the conversation + context", _run))
|
|
20
|
+
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""/resume [<id>] — reload a saved conversation (defaults to the latest for this directory)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ..rendering import color, eprint
|
|
6
|
+
from . import register
|
|
7
|
+
from .base import SlashCommand
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _run(session, args: str):
|
|
11
|
+
from .. import sessions
|
|
12
|
+
|
|
13
|
+
ident = args.strip()
|
|
14
|
+
if ident:
|
|
15
|
+
record = sessions.load(ident)
|
|
16
|
+
if record is None:
|
|
17
|
+
eprint(color(f"no session with id '{ident}' (try /sessions).", "yellow"))
|
|
18
|
+
return
|
|
19
|
+
else:
|
|
20
|
+
record = sessions.latest_for_root(str(session.config.root))
|
|
21
|
+
if record is None:
|
|
22
|
+
print(color("no saved sessions for this directory.", "gray"))
|
|
23
|
+
return
|
|
24
|
+
|
|
25
|
+
session.apply_record(record)
|
|
26
|
+
print(color(f"resumed session {record.id} · {len(session.messages)} messages "
|
|
27
|
+
f"· model {session.active_model}.", "gray"))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
register(SlashCommand("resume", "resume a saved session", _run, usage="[id]"))
|
|
31
|
+
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""/save <file> — dump the current transcript to a file."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from ..rendering import color, eprint
|
|
8
|
+
from . import register
|
|
9
|
+
from .base import SlashCommand
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _run(session, args: str):
|
|
13
|
+
target = args.strip() or "scoot-transcript.md"
|
|
14
|
+
path = Path(target).expanduser()
|
|
15
|
+
lines = [f"# scoot transcript\n\n_model: {session.resolved_model()}_\n"]
|
|
16
|
+
for msg in session.messages:
|
|
17
|
+
role = msg.get("role", "?")
|
|
18
|
+
content = msg.get("content", "")
|
|
19
|
+
lines.append(f"\n## {role}\n\n{content}\n")
|
|
20
|
+
try:
|
|
21
|
+
path.write_text("".join(lines))
|
|
22
|
+
except OSError as exc:
|
|
23
|
+
eprint(color(f"could not write {path}: {exc}", "red"))
|
|
24
|
+
return
|
|
25
|
+
print(color(f"saved transcript → {path}", "gray"))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
register(SlashCommand("save", "save the transcript", _run, usage="<file>"))
|
|
29
|
+
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""/sessions — list saved sessions for this workspace root (resume with /resume <id>)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
|
|
7
|
+
from ..rendering import color
|
|
8
|
+
from . import register
|
|
9
|
+
from .base import SlashCommand
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _ago(ts: float) -> str:
|
|
13
|
+
if not ts:
|
|
14
|
+
return "?"
|
|
15
|
+
secs = max(0, int(time.time() - ts))
|
|
16
|
+
if secs < 60:
|
|
17
|
+
return f"{secs}s ago"
|
|
18
|
+
if secs < 3600:
|
|
19
|
+
return f"{secs // 60}m ago"
|
|
20
|
+
if secs < 86400:
|
|
21
|
+
return f"{secs // 3600}h ago"
|
|
22
|
+
return f"{secs // 86400}d ago"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _run(session, args: str):
|
|
26
|
+
from .. import sessions
|
|
27
|
+
|
|
28
|
+
records = sessions.list_sessions(str(session.config.root))
|
|
29
|
+
if not records:
|
|
30
|
+
print(color("no saved sessions for this directory.", "gray"))
|
|
31
|
+
return
|
|
32
|
+
print(color("saved sessions (this directory):", "bold"))
|
|
33
|
+
for rec in records:
|
|
34
|
+
marker = color(" ← current", "green") if rec.id == session.id else ""
|
|
35
|
+
print(f" {color(rec.id, 'cyan')} {color(_ago(rec.updated), 'gray'):<20} "
|
|
36
|
+
f"{rec.turns} turns · {rec.active_model or rec.model}{marker}")
|
|
37
|
+
print(f" {color(rec.first_prompt(), 'gray')}")
|
|
38
|
+
print(color("resume with: /resume <id>", "gray"))
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
register(SlashCommand("sessions", "list saved sessions", _run))
|
|
42
|
+
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""/status — show current model, token usage, root, and session info."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ..rendering import color
|
|
6
|
+
from . import register
|
|
7
|
+
from .base import SlashCommand
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _run(session, args: str):
|
|
11
|
+
cfg = session.config
|
|
12
|
+
try:
|
|
13
|
+
provider_row = session.provider.default_name
|
|
14
|
+
from ..auth import key_source
|
|
15
|
+
|
|
16
|
+
src = key_source(session.provider.spec)
|
|
17
|
+
provider_row += f" (key: {src})" if src else ("" if not session.provider.spec.key_required
|
|
18
|
+
else " (no key)")
|
|
19
|
+
except Exception:
|
|
20
|
+
provider_row = getattr(cfg, "provider", "") or "?"
|
|
21
|
+
rows = [
|
|
22
|
+
("provider", provider_row),
|
|
23
|
+
("model", session.active_model + (" (auto)" if session.model.lower() == "auto" else "")),
|
|
24
|
+
("effort", getattr(cfg, "effort", "medium")),
|
|
25
|
+
("env files", ", ".join(getattr(cfg, "env_files", ()) or ()) or "none"),
|
|
26
|
+
("root", str(cfg.root)),
|
|
27
|
+
("approval", session.approval_mode),
|
|
28
|
+
("streaming", "on" if getattr(cfg, "stream", True) else "off"),
|
|
29
|
+
("panel", "on" if getattr(session, "status_bar", None) and session.status_bar.enabled
|
|
30
|
+
else "off"),
|
|
31
|
+
("input dock", "on" if getattr(session, "dock", False) else "off"),
|
|
32
|
+
("resume", getattr(cfg, "resume", "hint")),
|
|
33
|
+
("workspace context", "on" if getattr(cfg, "workspace_context", True) else "off"),
|
|
34
|
+
("feed verbosity", getattr(getattr(session, "ui", None), "verbosity",
|
|
35
|
+
getattr(cfg, "verbosity", "full"))),
|
|
36
|
+
("max steps", str(cfg.max_steps)),
|
|
37
|
+
("turns", str(len(session.messages))),
|
|
38
|
+
("tokens (session)", f"prompt={session.total_prompt} completion={session.total_completion}"),
|
|
39
|
+
("tokens (last)", f"prompt={session.last_usage.get('prompt_tokens', '?')} "
|
|
40
|
+
f"completion={session.last_usage.get('completion_tokens', '?')}"),
|
|
41
|
+
]
|
|
42
|
+
print(color("status:", "bold"))
|
|
43
|
+
for label, value in rows:
|
|
44
|
+
print(f" {color(label + ':', 'gray'):<28} {value}")
|
|
45
|
+
trusted = getattr(session, "trusted_tools", None)
|
|
46
|
+
if trusted:
|
|
47
|
+
print(f" {color('trusted tools:', 'gray'):<28} {', '.join(sorted(trusted))}")
|
|
48
|
+
if getattr(session, "worktree", None) is not None:
|
|
49
|
+
print(f" {color('worktree:', 'gray'):<28} {session.worktree.branch}")
|
|
50
|
+
plan = getattr(session, "plan", None)
|
|
51
|
+
if plan:
|
|
52
|
+
done = sum(1 for s in plan if s.get("status") == "completed")
|
|
53
|
+
current = next((s.get("step", "") for s in plan if s.get("status") == "in_progress"), "")
|
|
54
|
+
detail = f"{done}/{len(plan)}" + (f" · now: {current}" if current else "")
|
|
55
|
+
print(f" {color('plan:', 'gray'):<28} {detail}")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
register(SlashCommand("status", "show model + token usage", _run))
|
|
59
|
+
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""/verbosity — regulate how much of a turn's "thinking" shows in the feed.
|
|
2
|
+
|
|
3
|
+
Levels (see :class:`scootcli.repl.ReplUI`):
|
|
4
|
+
* ``full`` — reasoning narration shown; every tool-in-use line kept in the feed (a run log).
|
|
5
|
+
* ``compact`` — reasoning narration shown; tool-in-use is one transient line above the spinner,
|
|
6
|
+
removed once the tool finishes (nothing accumulates).
|
|
7
|
+
* ``quiet`` — reasoning narration suppressed; tools render like ``compact``.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from ..rendering import color, eprint
|
|
13
|
+
from . import register
|
|
14
|
+
from .base import SlashCommand
|
|
15
|
+
|
|
16
|
+
_LEVELS = ("full", "compact", "quiet")
|
|
17
|
+
_ALIASES = {"default": "full", "collapse": "compact", "collapsed": "compact",
|
|
18
|
+
"off": "quiet", "min": "quiet", "minimal": "quiet"}
|
|
19
|
+
_HELP = {
|
|
20
|
+
"full": "show reasoning + keep every tool call in the feed",
|
|
21
|
+
"compact": "show reasoning; collapse tool calls to one transient line above Thinking…",
|
|
22
|
+
"quiet": "hide reasoning; collapse tool calls",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _run(session, args: str):
|
|
27
|
+
ui = getattr(session, "ui", None)
|
|
28
|
+
if ui is None:
|
|
29
|
+
eprint(color("verbosity is not available in this context.", "yellow"))
|
|
30
|
+
return
|
|
31
|
+
|
|
32
|
+
want = (args or "").strip().lower()
|
|
33
|
+
if not want:
|
|
34
|
+
cur = getattr(ui, "verbosity", "full")
|
|
35
|
+
print(color(f"feed verbosity: {cur}", "bold"))
|
|
36
|
+
for lvl in _LEVELS:
|
|
37
|
+
mark = color("●", "cyan") if lvl == cur else color("○", "gray")
|
|
38
|
+
print(f" {mark} {color(lvl, 'bold')} {color('— ' + _HELP[lvl], 'gray')}")
|
|
39
|
+
print(color(" usage: /verbosity full|compact|quiet", "gray"))
|
|
40
|
+
return
|
|
41
|
+
|
|
42
|
+
level = _ALIASES.get(want, want)
|
|
43
|
+
if level not in _LEVELS:
|
|
44
|
+
eprint(color(f"unknown level '{want}'. use: full | compact | quiet", "yellow"))
|
|
45
|
+
return
|
|
46
|
+
ui.verbosity = level
|
|
47
|
+
try:
|
|
48
|
+
session.config.verbosity = level # keep config in sync (surfaced in /status)
|
|
49
|
+
except Exception:
|
|
50
|
+
pass
|
|
51
|
+
print(color(f"feed verbosity → {level} ({_HELP[level]})", "gray"))
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
register(SlashCommand(
|
|
55
|
+
"verbosity", "regulate feed detail (full|compact|quiet)", _run,
|
|
56
|
+
usage="[full|compact|quiet]",
|
|
57
|
+
))
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""/worktree — isolate autonomous work in a throwaway git worktree, then review & merge (M5.1b)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ..rendering import color, eprint
|
|
6
|
+
from .. import worktree as wt
|
|
7
|
+
from . import register
|
|
8
|
+
from .base import SlashCommand
|
|
9
|
+
|
|
10
|
+
_ACTIONS = ("merge", "keep", "discard")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _start(session):
|
|
14
|
+
if session.worktree is not None:
|
|
15
|
+
print(color(f"already in worktree {session.worktree.branch}.", "yellow"))
|
|
16
|
+
return
|
|
17
|
+
root = session.config.root
|
|
18
|
+
if not wt.is_git_repo(root):
|
|
19
|
+
print(color("not a git repo — worktree isolation unavailable here.", "yellow"))
|
|
20
|
+
print(color(" tip: use /yolo in place, or `git init` first.", "gray"))
|
|
21
|
+
return
|
|
22
|
+
try:
|
|
23
|
+
w = wt.start(root)
|
|
24
|
+
except Exception as exc: # git failure
|
|
25
|
+
eprint(color(f"could not create worktree: {exc}", "red"))
|
|
26
|
+
return
|
|
27
|
+
session.worktree = w
|
|
28
|
+
session.config = session.config.override(root=str(w.path))
|
|
29
|
+
print(color(f"⚙ isolated in worktree {w.branch}", "cyan"))
|
|
30
|
+
print(color(f" root → {w.path}", "gray"))
|
|
31
|
+
print(color(" work freely (try /yolo); then /worktree merge|keep|discard", "gray"))
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _finish(session, action):
|
|
35
|
+
w = session.worktree
|
|
36
|
+
if w is None:
|
|
37
|
+
print(color("not in a worktree.", "yellow"))
|
|
38
|
+
return
|
|
39
|
+
msg = wt.finish(w, action)
|
|
40
|
+
session.config = session.config.override(root=str(w.original_root))
|
|
41
|
+
session.worktree = None
|
|
42
|
+
print(color(f"{msg}", "gray"))
|
|
43
|
+
print(color(f" root → {w.original_root}", "gray"))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _run(session, args: str):
|
|
47
|
+
sub = (args.strip() or "start").split()[0].lower()
|
|
48
|
+
if sub in ("start", "on"):
|
|
49
|
+
_start(session)
|
|
50
|
+
elif sub == "diff":
|
|
51
|
+
if session.worktree is None:
|
|
52
|
+
print(color("not in a worktree.", "yellow"))
|
|
53
|
+
return
|
|
54
|
+
d = wt.diff(session.worktree)
|
|
55
|
+
print(d or color("(no changes yet)", "gray"))
|
|
56
|
+
elif sub in _ACTIONS:
|
|
57
|
+
_finish(session, sub)
|
|
58
|
+
else:
|
|
59
|
+
print(color(f"usage: /worktree start|diff|{'|'.join(_ACTIONS)}", "gray"))
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
register(SlashCommand("worktree", "isolate work in a git worktree, then merge/keep/discard",
|
|
63
|
+
_run, usage="start|diff|merge|keep|discard"))
|
|
64
|
+
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""/yolo — shortcut to switch the session into yolo approval mode (auto-approve all)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ..rendering import color
|
|
6
|
+
from . import register
|
|
7
|
+
from .base import SlashCommand
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _run(session, args: str):
|
|
11
|
+
session.approval_mode = "yolo"
|
|
12
|
+
print(color("⚡ yolo mode ON — tool calls run without asking.", "yellow"))
|
|
13
|
+
print(color(" (destructive shell commands are still confirmed; /approve always to revert)", "gray"))
|
|
14
|
+
if args.strip().lower() in ("worktree", "wt", "--worktree"):
|
|
15
|
+
from . import get
|
|
16
|
+
get("worktree").handler(session, "start")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
register(SlashCommand("yolo", "auto-approve all tool calls this session", _run, usage="[worktree]"))
|
|
20
|
+
|