coderio 0.4.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.
- coderio/__init__.py +14 -0
- coderio/agent/__init__.py +5 -0
- coderio/agent/_deepagents_compat.py +139 -0
- coderio/agent/command_review.py +140 -0
- coderio/agent/deep_loop.py +758 -0
- coderio/agent/harness.py +768 -0
- coderio/agent/harness_middleware.py +293 -0
- coderio/agent/permission_middleware.py +52 -0
- coderio/agent/prompts.py +304 -0
- coderio/agent/skill_tool.py +70 -0
- coderio/agent/state.py +144 -0
- coderio/agent/stream.py +93 -0
- coderio/cli/__init__.py +0 -0
- coderio/cli/app.py +257 -0
- coderio/cli/commands.py +339 -0
- coderio/cli/credentials.py +70 -0
- coderio/cli/mcp_cmd.py +227 -0
- coderio/cli/multimodal.py +94 -0
- coderio/cli/onboarding.py +270 -0
- coderio/cli/providers.py +113 -0
- coderio/cli/render.py +28 -0
- coderio/cli/repl.py +189 -0
- coderio/cli/run_cmd.py +171 -0
- coderio/cli/skills_cmd.py +77 -0
- coderio/cli/stream.py +224 -0
- coderio/cli/tui.py +1584 -0
- coderio/cli/tui_onboarding.py +465 -0
- coderio/cli/tui_screens.py +334 -0
- coderio/cli/tui_widgets.py +453 -0
- coderio/config/__init__.py +21 -0
- coderio/config/bootstrap.py +38 -0
- coderio/config/loader.py +273 -0
- coderio/config/models.py +193 -0
- coderio/config/trust.py +136 -0
- coderio/llm/__init__.py +3 -0
- coderio/llm/factory.py +164 -0
- coderio/llm/probe.py +153 -0
- coderio/mcp_loader.py +274 -0
- coderio/session/__init__.py +3 -0
- coderio/session/message.py +79 -0
- coderio/session/store.py +244 -0
- coderio/skills/__init__.py +4 -0
- coderio/skills/bundled/executing-plans/SKILL.md +15 -0
- coderio/skills/lion-skills/LICENSE +21 -0
- coderio/skills/lion-skills/README.md +114 -0
- coderio/skills/lion-skills/skills/clarifying-questions/SKILL.md +119 -0
- coderio/skills/lion-skills/skills/clarifying-questions/evals/evals.json +40 -0
- coderio/skills/lion-skills/skills/code-review/SKILL.md +102 -0
- coderio/skills/lion-skills/skills/code-review/evals/evals.json +40 -0
- coderio/skills/lion-skills/skills/commit-message/SKILL.md +97 -0
- coderio/skills/lion-skills/skills/commit-message/evals/evals.json +20 -0
- coderio/skills/lion-skills/skills/debugging/SKILL.md +116 -0
- coderio/skills/lion-skills/skills/debugging/evals/evals.json +40 -0
- coderio/skills/lion-skills/skills/error-handling/SKILL.md +74 -0
- coderio/skills/lion-skills/skills/error-handling/evals/evals.json +20 -0
- coderio/skills/lion-skills/skills/lion-writing-skills/SKILL.md +85 -0
- coderio/skills/lion-skills/skills/lion-writing-skills/evals/evals.json +20 -0
- coderio/skills/lion-skills/skills/naming/SKILL.md +85 -0
- coderio/skills/lion-skills/skills/naming/evals/evals.json +20 -0
- coderio/skills/lion-skills/skills/onboarding-unknown-codebase/SKILL.md +95 -0
- coderio/skills/lion-skills/skills/onboarding-unknown-codebase/evals/evals.json +20 -0
- coderio/skills/lion-skills/skills/spec-writing/SKILL.md +154 -0
- coderio/skills/lion-skills/skills/spec-writing/evals/evals.json +40 -0
- coderio/skills/lion-skills/skills/task-breakdown/SKILL.md +134 -0
- coderio/skills/lion-skills/skills/task-breakdown/evals/evals.json +46 -0
- coderio/skills/lion-skills/skills/testing/SKILL.md +134 -0
- coderio/skills/lion-skills/skills/testing/evals/evals.json +40 -0
- coderio/skills/lion-skills/skills/verify-and-fix/SKILL.md +132 -0
- coderio/skills/lion-skills/skills/verify-and-fix/evals/evals.json +40 -0
- coderio/skills/models.py +66 -0
- coderio/skills/parser.py +50 -0
- coderio/skills/store.py +102 -0
- coderio/tools/__init__.py +94 -0
- coderio/tools/base.py +55 -0
- coderio/tools/bash.py +168 -0
- coderio/tools/command_policy.py +364 -0
- coderio/tools/edit_file.py +55 -0
- coderio/tools/glob_tool.py +21 -0
- coderio/tools/grep_tool.py +90 -0
- coderio/tools/linux_sandbox.py +196 -0
- coderio/tools/list_dir.py +58 -0
- coderio/tools/multi_edit.py +62 -0
- coderio/tools/note.py +72 -0
- coderio/tools/permission.py +214 -0
- coderio/tools/read_file.py +51 -0
- coderio/tools/sandbox_runner.py +113 -0
- coderio/tools/todo.py +71 -0
- coderio/tools/web_fetch.py +188 -0
- coderio/tools/web_search.py +49 -0
- coderio/tools/win_job.py +214 -0
- coderio/tools/win_sandbox.py +639 -0
- coderio/tools/write_file.py +25 -0
- coderio-0.4.0.dist-info/METADATA +490 -0
- coderio-0.4.0.dist-info/RECORD +97 -0
- coderio-0.4.0.dist-info/WHEEL +4 -0
- coderio-0.4.0.dist-info/entry_points.txt +2 -0
- coderio-0.4.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, Field
|
|
4
|
+
|
|
5
|
+
from coderio.agent.prompts import ActiveSkills
|
|
6
|
+
from coderio.skills.store import SkillStore
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ActivateSkillArgs(BaseModel):
|
|
10
|
+
name: str = Field(description="Name of the skill to activate.")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ActivateSkillTool:
|
|
14
|
+
"""Tool that activates a skill by name, loading its full playbook into context.
|
|
15
|
+
|
|
16
|
+
Spec §2.3 (3): keyword/explicit skill activation. Returns a status string;
|
|
17
|
+
the actual prompt refresh happens via the on_activate_skill callback in the
|
|
18
|
+
loop (so the system prompt picks up the newly-active skill body).
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
name = "activate_skill"
|
|
22
|
+
description = (
|
|
23
|
+
"Activate a skill by name to load its full playbook into context. "
|
|
24
|
+
"Use when a task matches a skill's domain and you need its detailed playbook."
|
|
25
|
+
)
|
|
26
|
+
args_schema = ActivateSkillArgs
|
|
27
|
+
|
|
28
|
+
def __init__(self, store: SkillStore, active: ActiveSkills) -> None:
|
|
29
|
+
self.store = store
|
|
30
|
+
self.active = active
|
|
31
|
+
|
|
32
|
+
def run(self, name: str) -> str:
|
|
33
|
+
if not self.store.has(name):
|
|
34
|
+
return f"Error: skill not found: {name}. Available: {', '.join(self.store.names())}"
|
|
35
|
+
skill = self.store.get(name)
|
|
36
|
+
newly = self.active.activate(skill)
|
|
37
|
+
return f"Activated skill: {name}" if newly else f"Skill already active: {name}"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class DeactivateSkillArgs(BaseModel):
|
|
41
|
+
name: str = Field(description="Name of the active skill to deactivate.")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class DeactivateSkillTool:
|
|
45
|
+
"""Tool that deactivates an active skill, dropping its body from context.
|
|
46
|
+
|
|
47
|
+
Mirrors ActivateSkillTool. The budget warning in the system prompt points the model at
|
|
48
|
+
`deactivate_skill` when active skill bodies exceed ~30% of the context budget;
|
|
49
|
+
without this tool that hint would reference a non-existent tool and the model
|
|
50
|
+
would get an 'unknown tool' error. After deactivation the prompt is refreshed
|
|
51
|
+
via the same on_activate_skill callback path (it rebuilds the system prompt
|
|
52
|
+
from whatever skills remain active).
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
name = "deactivate_skill"
|
|
56
|
+
description = (
|
|
57
|
+
"Deactivate an active skill to free context. Use when active skill bodies "
|
|
58
|
+
"are consuming too much budget and one is no longer needed for the task."
|
|
59
|
+
)
|
|
60
|
+
args_schema = DeactivateSkillArgs
|
|
61
|
+
|
|
62
|
+
def __init__(self, active: ActiveSkills) -> None:
|
|
63
|
+
self.active = active
|
|
64
|
+
|
|
65
|
+
def run(self, name: str) -> str:
|
|
66
|
+
removed = self.active.deactivate(name)
|
|
67
|
+
if not removed:
|
|
68
|
+
active_names = [s.name for s in self.active.all()]
|
|
69
|
+
return f"Error: skill not active: {name}. Active skills: {', '.join(active_names) or '(none)'}"
|
|
70
|
+
return f"Deactivated skill: {name}. Prompt will refresh on next turn."
|
coderio/agent/state.py
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""Explicit agent state machine — observability for execution phases.
|
|
2
|
+
|
|
3
|
+
Inspired by Step-Realtime-CLI's AgentStateMachine, but unlike theirs (which only
|
|
4
|
+
RECORDS transitions for telemetry), this tracker derives phase from the harness's
|
|
5
|
+
ground-truth signals (writes/verifications/todos) so the displayed phase reflects
|
|
6
|
+
what the agent is actually doing, not just what it claims.
|
|
7
|
+
|
|
8
|
+
The timeline persists to the session jsonl at turn end (via a system-role
|
|
9
|
+
Message) so a past turn's phase progression can be replayed for debugging.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import time
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from enum import StrEnum
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class AgentState(StrEnum):
|
|
20
|
+
"""Structural phases of a coding-agent turn.
|
|
21
|
+
|
|
22
|
+
Derived from harness ground truth (never from model self-report):
|
|
23
|
+
EXPLORE — reading code (read_file/grep/list_dir), no writes yet
|
|
24
|
+
PLAN — a write happened but no todo list exists (PlanGate signal)
|
|
25
|
+
IMPLEMENT — writes exist, not yet verified
|
|
26
|
+
VERIFY — bash ran a verifying command (test/build/lint)
|
|
27
|
+
COMPLETE — turn ended (cleanly or max-rounds)
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
EXPLORE = "explore"
|
|
31
|
+
PLAN = "plan"
|
|
32
|
+
IMPLEMENT = "implement"
|
|
33
|
+
VERIFY = "verify"
|
|
34
|
+
COMPLETE = "complete"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class StateSnapshot:
|
|
39
|
+
"""One point in the phase timeline."""
|
|
40
|
+
|
|
41
|
+
state: AgentState
|
|
42
|
+
timestamp: str = field(default_factory=lambda: time.strftime("%Y-%m-%dT%H:%M:%S"))
|
|
43
|
+
step: int = 0 # model round number when the transition fired
|
|
44
|
+
hint: str = "" # optional context (e.g. "wrote loader.py", "ran pytest")
|
|
45
|
+
|
|
46
|
+
def to_dict(self) -> dict:
|
|
47
|
+
return {
|
|
48
|
+
"state": str(self.state),
|
|
49
|
+
"timestamp": self.timestamp,
|
|
50
|
+
"step": self.step,
|
|
51
|
+
"hint": self.hint,
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
@classmethod
|
|
55
|
+
def from_dict(cls, d: dict) -> "StateSnapshot":
|
|
56
|
+
return cls(
|
|
57
|
+
state=AgentState(d.get("state", "explore")),
|
|
58
|
+
timestamp=d.get("timestamp", ""),
|
|
59
|
+
step=d.get("step", 0),
|
|
60
|
+
hint=d.get("hint", ""),
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class AgentStateTracker:
|
|
65
|
+
"""Records the phase timeline for one agent turn.
|
|
66
|
+
|
|
67
|
+
Phase is derived: ``derive_phase()`` maps harness state to an AgentState.
|
|
68
|
+
``transition()`` is called from Harness.observe / check_termination; if the
|
|
69
|
+
derived phase matches the current one, nothing is recorded (debounce — a
|
|
70
|
+
turn that does 10 read_files produces ONE explore snapshot, not ten).
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
def __init__(self) -> None:
|
|
74
|
+
self._current: AgentState = AgentState.EXPLORE
|
|
75
|
+
self.timeline: list[StateSnapshot] = []
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def current(self) -> AgentState:
|
|
79
|
+
return self._current
|
|
80
|
+
|
|
81
|
+
def derive_phase(
|
|
82
|
+
self,
|
|
83
|
+
writes_since_verify: list[str],
|
|
84
|
+
todos_exist: bool,
|
|
85
|
+
just_verified: bool,
|
|
86
|
+
) -> AgentState:
|
|
87
|
+
"""Map harness ground truth to a phase.
|
|
88
|
+
|
|
89
|
+
Called from inside Harness (which has the live state); the tracker does
|
|
90
|
+
NOT reach into Harness to avoid a circular dependency.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
writes_since_verify: files written but not yet verified (non-empty ⇒ IMPLEMENT)
|
|
94
|
+
todos_exist: whether a todo list exists (False + first write ⇒ PLAN)
|
|
95
|
+
just_verified: whether a verifying bash ran this tool call (⇒ VERIFY)
|
|
96
|
+
"""
|
|
97
|
+
if just_verified:
|
|
98
|
+
return AgentState.VERIFY
|
|
99
|
+
if writes_since_verify:
|
|
100
|
+
# Writes exist without a plan → still in the plan-implicit phase
|
|
101
|
+
# (PlanGate would nudge here). Once a todo list exists, it's full IMPLEMENT.
|
|
102
|
+
return AgentState.PLAN if not todos_exist else AgentState.IMPLEMENT
|
|
103
|
+
return AgentState.EXPLORE
|
|
104
|
+
|
|
105
|
+
def transition(self, state: AgentState, step: int = 0, hint: str = "") -> bool:
|
|
106
|
+
"""Record a phase change. No-op if ``state`` equals the current phase
|
|
107
|
+
(debounce: repeated same-phase observations don't bloat the timeline).
|
|
108
|
+
|
|
109
|
+
Returns True if a new snapshot was recorded, False if debounced. Callers
|
|
110
|
+
that also notify a stream should use this to avoid spamming on_phase_change
|
|
111
|
+
for repeated same-phase observations.
|
|
112
|
+
"""
|
|
113
|
+
if state == self._current and self.timeline:
|
|
114
|
+
return False # same phase, already recorded — skip
|
|
115
|
+
self._current = state
|
|
116
|
+
self.timeline.append(StateSnapshot(state=state, step=step, hint=hint))
|
|
117
|
+
return True
|
|
118
|
+
|
|
119
|
+
def finish(self, step: int = 0, hint: str = "") -> None:
|
|
120
|
+
"""Mark the turn as complete. Always records (even if already COMPLETE)
|
|
121
|
+
so the final step number is captured for timeline replay."""
|
|
122
|
+
self._current = AgentState.COMPLETE
|
|
123
|
+
self.timeline.append(StateSnapshot(state=AgentState.COMPLETE, step=step, hint=hint))
|
|
124
|
+
|
|
125
|
+
def to_payload(self) -> list[dict]:
|
|
126
|
+
"""Serialize the timeline for persistence (system-role Message content)."""
|
|
127
|
+
return [s.to_dict() for s in self.timeline]
|
|
128
|
+
|
|
129
|
+
@staticmethod
|
|
130
|
+
def from_payload(payload: list[dict]) -> "AgentStateTracker":
|
|
131
|
+
"""Reconstruct a timeline from persisted data (for session replay)."""
|
|
132
|
+
t = AgentStateTracker()
|
|
133
|
+
for d in payload:
|
|
134
|
+
snap = StateSnapshot.from_dict(d)
|
|
135
|
+
t.timeline.append(snap)
|
|
136
|
+
t._current = snap.state
|
|
137
|
+
return t
|
|
138
|
+
|
|
139
|
+
def summary(self) -> str:
|
|
140
|
+
"""One-line human-readable summary for UI display."""
|
|
141
|
+
if not self.timeline:
|
|
142
|
+
return str(self._current)
|
|
143
|
+
phases = " → ".join(str(s.state) for s in self.timeline)
|
|
144
|
+
return f"{phases} ({len(self.timeline)} transitions)"
|
coderio/agent/stream.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Protocol
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class StreamHandler(Protocol):
|
|
7
|
+
"""Abstract streaming UI. CLI/GUI implement these (spec §4.3)."""
|
|
8
|
+
|
|
9
|
+
def on_step_start(self, step: int = 1) -> None: ...
|
|
10
|
+
def on_token(self, text: str) -> None: ...
|
|
11
|
+
def on_tool_start(
|
|
12
|
+
self,
|
|
13
|
+
name: str,
|
|
14
|
+
args: dict[str, Any],
|
|
15
|
+
step: int = 1,
|
|
16
|
+
tool_index: int = 0,
|
|
17
|
+
tool_total: int = 0,
|
|
18
|
+
) -> None: ...
|
|
19
|
+
def on_tool_end(self, name: str, result: str) -> None: ...
|
|
20
|
+
def on_finish(self) -> None: ...
|
|
21
|
+
# Optional hooks (have attr guards in run_step; NullStream implements them too):
|
|
22
|
+
def on_thinking(self, text: str) -> None: ... # 'is thinking' indicator
|
|
23
|
+
def on_truncated(self, stop_reason: str) -> None: ... # output was cut off
|
|
24
|
+
# Harness escalation release: the agent claimed completion despite an
|
|
25
|
+
# unverified write / pending todos, and the gate exhausted its retries.
|
|
26
|
+
def on_harness_warn(self, message: str) -> None: ...
|
|
27
|
+
# Harness force-continue: the agent claimed done, but the harness found
|
|
28
|
+
# unfinished work (unverified writes, pending todos, ungrounded claims) and
|
|
29
|
+
# is injecting a follow-up demand instead of letting the turn end. Distinct
|
|
30
|
+
# from on_harness_warn (which fires on ESCALATION RELEASE, after retries
|
|
31
|
+
# are exhausted). This fires on every force-continue iteration, surfacing
|
|
32
|
+
# why the agent is still running after the model produced a "final" answer.
|
|
33
|
+
def on_harness_continue(self, reason: str) -> None: ...
|
|
34
|
+
# Agent phase change (explore/plan/implement/verify/complete). Fired by the
|
|
35
|
+
# AgentStateTracker from inside Harness.observe/check_termination. Lets the
|
|
36
|
+
# UI show the task-level phase alongside the model-activity micro-phase.
|
|
37
|
+
def on_phase_change(self, state: str, step: int, hint: str) -> None: ...
|
|
38
|
+
# Turn end summary: the list of files modified this turn (write_file /
|
|
39
|
+
# edit_file / multi_edit). Lets the UI show a "files changed" summary —
|
|
40
|
+
# matching the "always show what changed" UX of claude code / zcode.
|
|
41
|
+
def on_turn_end(self, writes: list[str]) -> None: ...
|
|
42
|
+
# Todo list updated (deepagents write_todos tool). `todos` is a list of
|
|
43
|
+
# {content: str, status: "pending"|"in_progress"|"completed"}. The whole
|
|
44
|
+
# list is replaced each time (not incremental). Lets the UI render a live
|
|
45
|
+
# todo progress panel.
|
|
46
|
+
def on_todos_update(self, todos: list[dict]) -> None: ...
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class NullStream:
|
|
50
|
+
"""Default no-op handler for tests/headless."""
|
|
51
|
+
|
|
52
|
+
def on_step_start(self, step: int = 1) -> None:
|
|
53
|
+
pass
|
|
54
|
+
|
|
55
|
+
def on_token(self, text: str) -> None:
|
|
56
|
+
pass
|
|
57
|
+
|
|
58
|
+
def on_tool_start(
|
|
59
|
+
self,
|
|
60
|
+
name: str,
|
|
61
|
+
args: dict[str, Any],
|
|
62
|
+
step: int = 1,
|
|
63
|
+
tool_index: int = 0,
|
|
64
|
+
tool_total: int = 0,
|
|
65
|
+
) -> None:
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
def on_tool_end(self, name: str, result: str) -> None:
|
|
69
|
+
pass
|
|
70
|
+
|
|
71
|
+
def on_finish(self) -> None:
|
|
72
|
+
pass
|
|
73
|
+
|
|
74
|
+
def on_thinking(self, text: str) -> None:
|
|
75
|
+
pass
|
|
76
|
+
|
|
77
|
+
def on_truncated(self, stop_reason: str) -> None:
|
|
78
|
+
pass
|
|
79
|
+
|
|
80
|
+
def on_harness_warn(self, message: str) -> None:
|
|
81
|
+
pass
|
|
82
|
+
|
|
83
|
+
def on_harness_continue(self, reason: str) -> None:
|
|
84
|
+
pass
|
|
85
|
+
|
|
86
|
+
def on_phase_change(self, state: str, step: int, hint: str) -> None:
|
|
87
|
+
pass
|
|
88
|
+
|
|
89
|
+
def on_turn_end(self, writes: list[str]) -> None:
|
|
90
|
+
pass
|
|
91
|
+
|
|
92
|
+
def on_todos_update(self, todos: list[dict]) -> None:
|
|
93
|
+
pass
|
coderio/cli/__init__.py
ADDED
|
File without changes
|
coderio/cli/app.py
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
from rich.panel import Panel
|
|
8
|
+
|
|
9
|
+
from coderio.config import load_config
|
|
10
|
+
from coderio.config.bootstrap import ensure_user_dirs
|
|
11
|
+
from coderio.skills.store import SkillStore, load_skill_store
|
|
12
|
+
|
|
13
|
+
app = typer.Typer(
|
|
14
|
+
add_completion=False,
|
|
15
|
+
no_args_is_help=False,
|
|
16
|
+
help="coderio — a skill-driven coding agent.",
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
skills_app = typer.Typer(help="Manage skills (install/list/update).")
|
|
20
|
+
app.add_typer(skills_app, name="skills")
|
|
21
|
+
|
|
22
|
+
mcp_app = typer.Typer(help="Manage MCP servers (add/list/remove).")
|
|
23
|
+
app.add_typer(mcp_app, name="mcp")
|
|
24
|
+
|
|
25
|
+
BUNDLED_SKILLS = Path(__file__).resolve().parents[1] / "skills"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _user_skills_dir() -> Path:
|
|
29
|
+
return Path.home() / ".coderio" / "skills"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _load_store() -> SkillStore:
|
|
33
|
+
return load_skill_store(BUNDLED_SKILLS, _user_skills_dir(), None)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@app.callback(invoke_without_command=True)
|
|
37
|
+
def main(
|
|
38
|
+
ctx: typer.Context,
|
|
39
|
+
resume: str = typer.Option(None, "--resume", help="Resume a session by id."),
|
|
40
|
+
continue_last: bool = typer.Option(False, "--continue", help="Resume most recent session."),
|
|
41
|
+
provider: str = typer.Option(None, "--provider", help="Override provider_id."),
|
|
42
|
+
model: str = typer.Option(None, "--model", help="Override model name."),
|
|
43
|
+
):
|
|
44
|
+
"""coderio — start the interactive TUI or run a subcommand.
|
|
45
|
+
|
|
46
|
+
Launches the Textual TUI: foldable thinking (Ctrl+O), scrollable history,
|
|
47
|
+
slash-command autocomplete. This is the only interactive entry point.
|
|
48
|
+
"""
|
|
49
|
+
if ctx.invoked_subcommand is not None:
|
|
50
|
+
return
|
|
51
|
+
ensure_user_dirs()
|
|
52
|
+
from coderio.cli.tui import run_tui
|
|
53
|
+
|
|
54
|
+
run_tui(
|
|
55
|
+
provider_override=provider,
|
|
56
|
+
model_override=model,
|
|
57
|
+
resume=resume,
|
|
58
|
+
continue_last=continue_last,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@skills_app.command("list")
|
|
63
|
+
def skills_list():
|
|
64
|
+
"""List installed skills (bundled + user layers)."""
|
|
65
|
+
store = _load_store()
|
|
66
|
+
console = Console()
|
|
67
|
+
names = store.names()
|
|
68
|
+
if not names:
|
|
69
|
+
console.print("No skills installed. Run `coderio skills install`.")
|
|
70
|
+
raise typer.Exit()
|
|
71
|
+
console.print("Installed skills:\n" + "\n".join(f" - {n}" for n in names))
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@skills_app.command("install")
|
|
75
|
+
def skills_install(
|
|
76
|
+
repo: str = typer.Option(None, "--repo", help="Git repo URL (default: Lion-Skills)."),
|
|
77
|
+
force: bool = typer.Option(False, "--force", help="Overwrite non-git target."),
|
|
78
|
+
):
|
|
79
|
+
"""Install/update skills from a git repo (default: Lion-Skills)."""
|
|
80
|
+
from coderio.cli.skills_cmd import install_skills
|
|
81
|
+
|
|
82
|
+
cfg = load_config()
|
|
83
|
+
repo_url = repo or cfg.skills.repo_url
|
|
84
|
+
result = install_skills(repo_url, _user_skills_dir(), force=force)
|
|
85
|
+
console = Console()
|
|
86
|
+
if result.success:
|
|
87
|
+
console.print(
|
|
88
|
+
Panel(
|
|
89
|
+
f"{result.action.capitalize()}: {len(result.skills)} skills\n"
|
|
90
|
+
+ "\n".join(f" - {s}" for s in result.skills),
|
|
91
|
+
title="skills",
|
|
92
|
+
border_style="green",
|
|
93
|
+
)
|
|
94
|
+
)
|
|
95
|
+
else:
|
|
96
|
+
console.print(f"[red]Error:[/red] {result.message}")
|
|
97
|
+
raise typer.Exit(1)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@skills_app.command("update")
|
|
101
|
+
def skills_update(
|
|
102
|
+
repo: str = typer.Option(None, "--repo", help="Git repo URL."),
|
|
103
|
+
):
|
|
104
|
+
"""Update installed skills (git pull)."""
|
|
105
|
+
skills_install(repo=repo, force=False)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
# ----------------------------------------------------------- coderio mcp
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@mcp_app.command("list")
|
|
112
|
+
def mcp_list_cmd():
|
|
113
|
+
"""List configured MCP servers (project + user scope)."""
|
|
114
|
+
from coderio.cli.mcp_cmd import mcp_list
|
|
115
|
+
|
|
116
|
+
console = Console()
|
|
117
|
+
entries = mcp_list()
|
|
118
|
+
if not entries:
|
|
119
|
+
console.print("No MCP servers configured. Add one with [cyan]coderio mcp add <name> --command ...[/cyan]")
|
|
120
|
+
return
|
|
121
|
+
# Group by scope for readability.
|
|
122
|
+
project = [(n, p) for n, s, p in entries if s == "project"]
|
|
123
|
+
user = [(n, p) for n, s, p in entries if s == "user"]
|
|
124
|
+
if project:
|
|
125
|
+
console.print("[bold green]project scope[/bold green] (.mcp.json):")
|
|
126
|
+
for name, path in project:
|
|
127
|
+
console.print(f" - {name} [dim]{path}[/dim]")
|
|
128
|
+
if user:
|
|
129
|
+
console.print("[bold blue]user scope[/bold blue] (~/.coderio/mcp.json):")
|
|
130
|
+
for name, path in user:
|
|
131
|
+
console.print(f" - {name} [dim]{path}[/dim]")
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@mcp_app.command("add")
|
|
135
|
+
def mcp_add_cmd(
|
|
136
|
+
name: str = typer.Argument(..., help="Server name (used as tool-name prefix)."),
|
|
137
|
+
type: str = typer.Option("stdio", "--type", help="Transport: stdio (default), http, or sse."),
|
|
138
|
+
command: str = typer.Option(None, "--command", help="stdio: executable to run (e.g. npx)."),
|
|
139
|
+
url: str = typer.Option(None, "--url", help="http/sse: server endpoint URL."),
|
|
140
|
+
scope: str = typer.Option("project", "--scope", help="Config scope: project (default) or user."),
|
|
141
|
+
arg: list[str] = typer.Option(
|
|
142
|
+
None, "--arg", help="stdio: argument (repeatable, in order). E.g. --arg -y --arg @mcp/server-fs"
|
|
143
|
+
),
|
|
144
|
+
):
|
|
145
|
+
"""Add an MCP server to the project (.mcp.json) or user (~/.coderio/mcp.json) config."""
|
|
146
|
+
from coderio.cli.mcp_cmd import mcp_add
|
|
147
|
+
|
|
148
|
+
result = mcp_add(
|
|
149
|
+
name,
|
|
150
|
+
server_type=type,
|
|
151
|
+
command=command,
|
|
152
|
+
url=url,
|
|
153
|
+
args=arg,
|
|
154
|
+
scope=scope,
|
|
155
|
+
)
|
|
156
|
+
console = Console()
|
|
157
|
+
if result.success:
|
|
158
|
+
console.print(f"[green]{result.action.capitalize()}[/green]: {result.message}")
|
|
159
|
+
else:
|
|
160
|
+
console.print(f"[red]Error:[/red] {result.message}")
|
|
161
|
+
raise typer.Exit(1)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@mcp_app.command("remove")
|
|
165
|
+
def mcp_remove_cmd(
|
|
166
|
+
name: str = typer.Argument(..., help="Server name to remove."),
|
|
167
|
+
scope: str = typer.Option("project", "--scope", help="Config scope: project (default) or user."),
|
|
168
|
+
):
|
|
169
|
+
"""Remove an MCP server entry (no-op if the name is absent)."""
|
|
170
|
+
from coderio.cli.mcp_cmd import mcp_remove
|
|
171
|
+
|
|
172
|
+
result = mcp_remove(name, scope=scope)
|
|
173
|
+
console = Console()
|
|
174
|
+
if result.action == "noop":
|
|
175
|
+
console.print(f"[yellow]No-op:[/yellow] {result.message}")
|
|
176
|
+
else:
|
|
177
|
+
console.print(f"[green]{result.action.capitalize()}[/green]: {result.message}")
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
@app.command("run")
|
|
181
|
+
def run_cmd(
|
|
182
|
+
task: str = typer.Argument(..., help="Task text for a one-shot headless agent run."),
|
|
183
|
+
provider: str = typer.Option(None, "--provider", help="Override provider_id."),
|
|
184
|
+
model: str = typer.Option(None, "--model", help="Override model name."),
|
|
185
|
+
permission: str = typer.Option(
|
|
186
|
+
"full",
|
|
187
|
+
"--permission",
|
|
188
|
+
"-p",
|
|
189
|
+
help="Permission tier: full (default, headless can't answer prompts), plan, confirm, auto_edit.",
|
|
190
|
+
),
|
|
191
|
+
session_id: str = typer.Option(None, "--session-id", help="Resume a prior session by id."),
|
|
192
|
+
quiet: bool = typer.Option(False, "--quiet", "-q", help="Print only the final result."),
|
|
193
|
+
):
|
|
194
|
+
"""One-shot headless agent run (CI / scripting / benchmarks).
|
|
195
|
+
|
|
196
|
+
Streams tokens to stdout by default; tool progress goes to stderr. Never
|
|
197
|
+
prompts interactively — untrusted repo config or missing credentials are
|
|
198
|
+
hard errors. The command blacklist still applies in full mode.
|
|
199
|
+
"""
|
|
200
|
+
from coderio.cli.run_cmd import run_headless
|
|
201
|
+
|
|
202
|
+
run_headless(
|
|
203
|
+
task,
|
|
204
|
+
provider=provider,
|
|
205
|
+
model=model,
|
|
206
|
+
permission=permission,
|
|
207
|
+
session_id=session_id,
|
|
208
|
+
quiet=quiet,
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
@app.command("config")
|
|
213
|
+
def config_cmd():
|
|
214
|
+
"""Print current configuration."""
|
|
215
|
+
cfg = load_config()
|
|
216
|
+
effective_base_url = cfg.model.base_url
|
|
217
|
+
if cfg.model.provider_id:
|
|
218
|
+
from coderio.cli.providers import get_provider
|
|
219
|
+
|
|
220
|
+
info = get_provider(cfg.model.provider_id)
|
|
221
|
+
if info is not None:
|
|
222
|
+
effective_base_url = info.base_url or "(user-supplied at runtime)"
|
|
223
|
+
console = Console()
|
|
224
|
+
console.print(
|
|
225
|
+
Panel(
|
|
226
|
+
f"provider_id: {cfg.model.provider_id or '(none)'}"
|
|
227
|
+
f"\nprovider: {cfg.model.provider}"
|
|
228
|
+
f"\nmodel: {cfg.model.default}"
|
|
229
|
+
f"\nbase_url: {effective_base_url}"
|
|
230
|
+
f"\npermission: {cfg.tools.permission_mode}"
|
|
231
|
+
f"\nskills repo: {cfg.skills.repo_url}",
|
|
232
|
+
title="coderio config",
|
|
233
|
+
border_style="blue",
|
|
234
|
+
)
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def main_entry() -> None:
|
|
239
|
+
# Ensure stdout/stderr use UTF-8 on Windows. The default Windows console
|
|
240
|
+
# encoding (cp1252) can't represent CJK characters in the Rich/Typer output
|
|
241
|
+
# (Chinese UI strings, skill names), causing UnicodeEncodeError. Reconfiguring
|
|
242
|
+
# here makes `coderio --help` and `coderio config` work in any locale —
|
|
243
|
+
# matching the behavior Linux/macOS users get by default.
|
|
244
|
+
import sys
|
|
245
|
+
|
|
246
|
+
for stream_name in ("stdout", "stderr"):
|
|
247
|
+
stream = getattr(sys, stream_name, None)
|
|
248
|
+
if stream is not None and hasattr(stream, "reconfigure"):
|
|
249
|
+
try:
|
|
250
|
+
stream.reconfigure(encoding="utf-8", errors="replace")
|
|
251
|
+
except Exception:
|
|
252
|
+
pass # already reconfigured or unsupported — ignore
|
|
253
|
+
app()
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
if __name__ == "__main__":
|
|
257
|
+
main_entry()
|