mycode-cli 0.13.0__py3-none-any.whl → 0.13.2__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.
- mycode_cli/main.py +7 -5
- mycode_cli/permissions.py +6 -7
- mycode_cli/runtime.py +3 -2
- mycode_cli/server/deps.py +1 -1
- mycode_cli/server/routers/chat.py +13 -14
- mycode_cli/server/routers/sessions.py +2 -2
- mycode_cli/server/run_manager.py +9 -1
- mycode_cli/server/static/assets/{EditDiff-DKu81Ilp.js → EditDiff-DOjxGJl-.js} +1 -1
- mycode_cli/server/static/assets/{index-DwIE3pX3.js → index-BUaXGcoz.js} +63 -63
- mycode_cli/server/static/assets/index-g5_LkRCI.css +1 -0
- mycode_cli/server/static/index.html +2 -2
- mycode_cli/sessions.py +200 -0
- mycode_cli/system_prompt.py +6 -4
- mycode_cli/tools.py +12 -12
- mycode_cli/tui/chat.py +16 -13
- mycode_cli/web_tools.py +7 -4
- mycode_cli/workspace.py +30 -0
- {mycode_cli-0.13.0.dist-info → mycode_cli-0.13.2.dist-info}/METADATA +2 -2
- {mycode_cli-0.13.0.dist-info → mycode_cli-0.13.2.dist-info}/RECORD +22 -20
- mycode_cli/server/static/assets/index-CxbWFIm9.css +0 -1
- {mycode_cli-0.13.0.dist-info → mycode_cli-0.13.2.dist-info}/WHEEL +0 -0
- {mycode_cli-0.13.0.dist-info → mycode_cli-0.13.2.dist-info}/entry_points.txt +0 -0
- {mycode_cli-0.13.0.dist-info → mycode_cli-0.13.2.dist-info}/licenses/LICENSE +0 -0
mycode_cli/main.py
CHANGED
|
@@ -14,7 +14,6 @@ import typer
|
|
|
14
14
|
|
|
15
15
|
from mycode.agent import Agent
|
|
16
16
|
from mycode.messages import ConversationMessage
|
|
17
|
-
from mycode.session import SessionStore
|
|
18
17
|
from mycode_cli import __version__
|
|
19
18
|
from mycode_cli.config import (
|
|
20
19
|
ResolvedProvider,
|
|
@@ -26,6 +25,7 @@ from mycode_cli.config import (
|
|
|
26
25
|
resolve_sessions_dir,
|
|
27
26
|
)
|
|
28
27
|
from mycode_cli.permissions import PERMISSION_DENIED_BY_USER_OUTPUT, PERMISSION_DENIED_OUTPUT
|
|
28
|
+
from mycode_cli.sessions import SessionStore
|
|
29
29
|
|
|
30
30
|
from .runtime import build_agent
|
|
31
31
|
from .tui.chat import TerminalChat
|
|
@@ -83,8 +83,7 @@ async def resolve_session(
|
|
|
83
83
|
"resumed",
|
|
84
84
|
)
|
|
85
85
|
|
|
86
|
-
#
|
|
87
|
-
# lazily by Agent.achat on the first persist.
|
|
86
|
+
# The catalog and timeline are both created lazily on the first user turn.
|
|
88
87
|
return ResolvedSession(uuid4().hex, {}, [], "new")
|
|
89
88
|
|
|
90
89
|
|
|
@@ -95,9 +94,10 @@ def _show_version(value: bool) -> None:
|
|
|
95
94
|
raise typer.Exit()
|
|
96
95
|
|
|
97
96
|
|
|
98
|
-
async def run_noninteractive(agent: Agent, message: str) -> int:
|
|
97
|
+
async def run_noninteractive(agent: Agent, message: str, *, store: SessionStore, cwd: str) -> int:
|
|
99
98
|
"""Run one message non-interactively and print only the final assistant reply."""
|
|
100
99
|
|
|
100
|
+
await store.record_user_turn(agent.session_id, cwd=cwd, text=message)
|
|
101
101
|
latest_assistant: ConversationMessage | None = None
|
|
102
102
|
|
|
103
103
|
async def track(payload: ConversationMessage) -> None:
|
|
@@ -304,7 +304,9 @@ def run(
|
|
|
304
304
|
reasoning_effort=reasoning_effort,
|
|
305
305
|
)
|
|
306
306
|
|
|
307
|
-
code = asyncio.run(
|
|
307
|
+
code = asyncio.run(
|
|
308
|
+
run_noninteractive(setup.agent, " ".join(message).strip(), store=setup.store, cwd=setup.settings.cwd)
|
|
309
|
+
)
|
|
308
310
|
raise SystemExit(code)
|
|
309
311
|
|
|
310
312
|
|
mycode_cli/permissions.py
CHANGED
|
@@ -9,9 +9,9 @@ from pathlib import Path
|
|
|
9
9
|
from typing import Literal, NamedTuple
|
|
10
10
|
|
|
11
11
|
from mycode import Hooks, ToolExecutionResult, ToolHookContext
|
|
12
|
-
from mycode.utils import resolve_path
|
|
13
12
|
from mycode_cli.config import PermissionConfig, PermissionLevel, Settings
|
|
14
13
|
from mycode_cli.system_prompt import discover_skills
|
|
14
|
+
from mycode_cli.workspace import CliDeps, resolve_path
|
|
15
15
|
|
|
16
16
|
PermissionTier = Literal["readonly", "safe", "standard", "yolo"]
|
|
17
17
|
PermissionDecision = Literal["allow", "ask", "deny"]
|
|
@@ -78,8 +78,8 @@ def build_permission_hooks(
|
|
|
78
78
|
skill_roots = [Path(s.path).parent.resolve(strict=False) for s in discover_skills(settings.cwd)]
|
|
79
79
|
|
|
80
80
|
@hooks.before_tool
|
|
81
|
-
async def check_permission(ctx: ToolHookContext) -> ToolExecutionResult | None:
|
|
82
|
-
check = classify_tool(ctx,
|
|
81
|
+
async def check_permission(ctx: ToolHookContext[CliDeps]) -> ToolExecutionResult | None:
|
|
82
|
+
check = classify_tool(ctx, project=settings.project, skill_roots=skill_roots)
|
|
83
83
|
decision = permission_decision(settings.permission, check.tier)
|
|
84
84
|
if decision == "allow":
|
|
85
85
|
return None
|
|
@@ -111,9 +111,8 @@ def permission_decision(permission: PermissionConfig, tier: PermissionTier) -> P
|
|
|
111
111
|
|
|
112
112
|
|
|
113
113
|
def classify_tool(
|
|
114
|
-
ctx: ToolHookContext,
|
|
114
|
+
ctx: ToolHookContext[CliDeps],
|
|
115
115
|
*,
|
|
116
|
-
cwd: str,
|
|
117
116
|
project: str,
|
|
118
117
|
skill_roots: list[Path],
|
|
119
118
|
) -> PermissionCheck:
|
|
@@ -133,11 +132,11 @@ def classify_tool(
|
|
|
133
132
|
|
|
134
133
|
if name in {"read", "write", "edit"}:
|
|
135
134
|
raw = str(ctx.tool_input.get("path") or "")
|
|
136
|
-
path = resolve_path(raw, cwd=cwd)
|
|
135
|
+
path = resolve_path(raw, cwd=ctx.deps.cwd)
|
|
137
136
|
project_path = Path(project).resolve(strict=False)
|
|
138
137
|
preview = raw or str(path)
|
|
139
138
|
if name == "read" and (
|
|
140
|
-
path.is_relative_to(ctx.tool_output_dir.resolve(strict=False))
|
|
139
|
+
path.is_relative_to(ctx.deps.tool_output_dir.resolve(strict=False))
|
|
141
140
|
or any(path.is_relative_to(root) for root in skill_roots)
|
|
142
141
|
):
|
|
143
142
|
return PermissionCheck("readonly", preview)
|
mycode_cli/runtime.py
CHANGED
|
@@ -3,12 +3,13 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
from mycode.agent import Agent
|
|
6
|
-
from mycode.session import SessionStore
|
|
7
6
|
from mycode_cli.config import ResolvedProvider, Settings
|
|
8
7
|
from mycode_cli.permissions import ToolReviewCallback, build_permission_hooks
|
|
8
|
+
from mycode_cli.sessions import SessionStore
|
|
9
9
|
from mycode_cli.system_prompt import build_system_prompt
|
|
10
10
|
from mycode_cli.tools import DEFAULT_TOOLS
|
|
11
11
|
from mycode_cli.web_tools import build_web_tools
|
|
12
|
+
from mycode_cli.workspace import CliDeps
|
|
12
13
|
|
|
13
14
|
|
|
14
15
|
def sum_known_costs(*costs: float | None) -> float | None:
|
|
@@ -58,7 +59,6 @@ def build_agent(
|
|
|
58
59
|
return Agent(
|
|
59
60
|
model=resolved_provider.model,
|
|
60
61
|
provider=resolved_provider.provider,
|
|
61
|
-
cwd=cwd,
|
|
62
62
|
session_dir=store.data_dir,
|
|
63
63
|
session_id=session_id,
|
|
64
64
|
api_key=resolved_provider.api_key,
|
|
@@ -74,4 +74,5 @@ def build_agent(
|
|
|
74
74
|
system=build_system_prompt(cwd, settings),
|
|
75
75
|
tools=[*DEFAULT_TOOLS, *build_web_tools(settings.web)],
|
|
76
76
|
hooks=hooks,
|
|
77
|
+
deps=CliDeps.for_session(cwd=cwd, data_dir=store.data_dir, session_id=session_id),
|
|
77
78
|
)
|
mycode_cli/server/deps.py
CHANGED
|
@@ -8,9 +8,9 @@ from typing import Annotated
|
|
|
8
8
|
|
|
9
9
|
from fastapi import Depends, HTTPException
|
|
10
10
|
|
|
11
|
-
from mycode.session import SessionStore
|
|
12
11
|
from mycode_cli.config import resolve_sessions_dir
|
|
13
12
|
from mycode_cli.server.run_manager import RunManager
|
|
13
|
+
from mycode_cli.sessions import SessionStore
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
def resolve_workspace_cwd(raw: str | None) -> str:
|
|
@@ -26,11 +26,11 @@ from mycode.messages import (
|
|
|
26
26
|
ConversationMessage,
|
|
27
27
|
build_message,
|
|
28
28
|
document_block,
|
|
29
|
+
flatten_message_text,
|
|
29
30
|
image_block,
|
|
30
31
|
text_block,
|
|
31
32
|
)
|
|
32
33
|
from mycode.providers import provider_default_models
|
|
33
|
-
from mycode.utils import resolve_path
|
|
34
34
|
from mycode_cli.config import (
|
|
35
35
|
ResolvedProvider,
|
|
36
36
|
get_settings,
|
|
@@ -55,6 +55,7 @@ from mycode_cli.server.schemas import (
|
|
|
55
55
|
StreamEvent,
|
|
56
56
|
)
|
|
57
57
|
from mycode_cli.system_prompt import build_skill_snapshot_blocks, discover_slash_skills
|
|
58
|
+
from mycode_cli.workspace import resolve_path
|
|
58
59
|
|
|
59
60
|
router = APIRouter()
|
|
60
61
|
|
|
@@ -80,7 +81,7 @@ def _read_workspace_text_attachment(rel_path: str, *, name: str | None, cwd: str
|
|
|
80
81
|
if detect_image_mime_type(path) or detect_document_mime_type(path):
|
|
81
82
|
raise HTTPException(status_code=400, detail=f"not a text file: {rel_path}")
|
|
82
83
|
try:
|
|
83
|
-
return build_attachment_blocks([Attachment.path(path, name=name or rel_path)]
|
|
84
|
+
return build_attachment_blocks([Attachment.path(path, name=name or rel_path)])
|
|
84
85
|
except ValueError as exc:
|
|
85
86
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
86
87
|
|
|
@@ -100,12 +101,7 @@ async def _build_user_message(chat: ChatRequest, cwd: str) -> ConversationMessag
|
|
|
100
101
|
continue
|
|
101
102
|
text = block.text or ""
|
|
102
103
|
if block.is_attachment:
|
|
103
|
-
blocks.extend(
|
|
104
|
-
build_attachment_blocks(
|
|
105
|
-
[Attachment.text(text, name=str(block.name or "attached-file"))],
|
|
106
|
-
cwd=cwd,
|
|
107
|
-
)
|
|
108
|
-
)
|
|
104
|
+
blocks.extend(build_attachment_blocks([Attachment.text(text, name=str(block.name or "attached-file"))]))
|
|
109
105
|
elif text:
|
|
110
106
|
visible_text.append(text)
|
|
111
107
|
blocks.append(text_block(text))
|
|
@@ -245,14 +241,16 @@ async def chat(chat: ChatRequest, store: StoreDep, runs: RunManagerDep) -> ChatR
|
|
|
245
241
|
existing_messages = data["messages"] if data else []
|
|
246
242
|
_validate_rewind_request(session=session, messages=existing_messages, rewind_to=chat.rewind_to)
|
|
247
243
|
|
|
248
|
-
# All validation passed. Land the rewind marker (if any) and
|
|
249
|
-
#
|
|
250
|
-
#
|
|
244
|
+
# All validation passed. Land the rewind marker (if any) and register
|
|
245
|
+
# the user turn in the catalog: first turn creates the entry and sets
|
|
246
|
+
# the title, later turns bump updated_at.
|
|
251
247
|
if chat.rewind_to is not None:
|
|
252
248
|
await store.append_rewind(session_id, chat.rewind_to)
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
249
|
+
session = await store.record_user_turn(
|
|
250
|
+
session_id,
|
|
251
|
+
cwd=cwd,
|
|
252
|
+
text=flatten_message_text(user_message, include_thinking=False),
|
|
253
|
+
)
|
|
256
254
|
|
|
257
255
|
async def review(request: ToolReviewRequest) -> ToolReviewDecision:
|
|
258
256
|
# Bridge before_tool review waits to SSE events on the active run.
|
|
@@ -376,6 +374,7 @@ async def compact_session(
|
|
|
376
374
|
session_id=session_id,
|
|
377
375
|
base_messages=agent.messages,
|
|
378
376
|
agent=agent,
|
|
377
|
+
on_complete=store.touch,
|
|
379
378
|
)
|
|
380
379
|
except ActiveRunError as exc:
|
|
381
380
|
existing = await runs.get_run(exc.run_id)
|
|
@@ -34,8 +34,8 @@ def _redact_document_data(messages: list[ConversationMessage]) -> list[dict[str,
|
|
|
34
34
|
async def create_session(req: SessionCreateRequest, store: StoreDep) -> dict[str, Any]:
|
|
35
35
|
cwd = resolve_workspace_cwd(req.cwd)
|
|
36
36
|
session_id = uuid4().hex
|
|
37
|
-
|
|
38
|
-
return {"session":
|
|
37
|
+
session = await store.create_session(session_id, cwd=cwd)
|
|
38
|
+
return {"session": session, "messages": []}
|
|
39
39
|
|
|
40
40
|
|
|
41
41
|
@router.get("")
|
mycode_cli/server/run_manager.py
CHANGED
|
@@ -5,7 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
import asyncio
|
|
6
6
|
import copy
|
|
7
7
|
import time
|
|
8
|
-
from collections.abc import AsyncGenerator, AsyncIterator, Awaitable
|
|
8
|
+
from collections.abc import AsyncGenerator, AsyncIterator, Awaitable, Callable
|
|
9
9
|
from contextlib import asynccontextmanager, suppress
|
|
10
10
|
from dataclasses import dataclass, field
|
|
11
11
|
from typing import Any, Literal, Protocol
|
|
@@ -18,6 +18,7 @@ from mycode_cli.runtime import sum_known_costs
|
|
|
18
18
|
|
|
19
19
|
RunStatus = Literal["running", "completed", "failed", "cancelled"]
|
|
20
20
|
RunKind = Literal["chat", "compact"]
|
|
21
|
+
RunCompletionCallback = Callable[[str], Awaitable[None]]
|
|
21
22
|
FINISHED_RUN_TTL_SECONDS = 300
|
|
22
23
|
RUN_EVENT_BUFFER_SIZE = 2000
|
|
23
24
|
RUN_TOOL_OUTPUT_BUFFER_BYTES = 1_000_000
|
|
@@ -62,6 +63,7 @@ class RunState:
|
|
|
62
63
|
# Session cost before this run (folded from the session JSONL by the chat
|
|
63
64
|
# router); None means unknown. Composed with each usage event's turn cost.
|
|
64
65
|
session_cost_base: float | None = None
|
|
66
|
+
on_complete: RunCompletionCallback | None = None
|
|
65
67
|
status: RunStatus = "running"
|
|
66
68
|
error: str | None = None
|
|
67
69
|
events: list[dict[str, Any]] = field(default_factory=list)
|
|
@@ -121,6 +123,7 @@ class RunManager:
|
|
|
121
123
|
session_id: str,
|
|
122
124
|
base_messages: list[ConversationMessage],
|
|
123
125
|
agent: RunAgent,
|
|
126
|
+
on_complete: RunCompletionCallback | None = None,
|
|
124
127
|
) -> dict[str, Any]:
|
|
125
128
|
return await self._start(
|
|
126
129
|
session_id=session_id,
|
|
@@ -128,6 +131,7 @@ class RunManager:
|
|
|
128
131
|
user_message=None,
|
|
129
132
|
base_messages=base_messages,
|
|
130
133
|
agent=agent,
|
|
134
|
+
on_complete=on_complete,
|
|
131
135
|
)
|
|
132
136
|
|
|
133
137
|
async def _start(
|
|
@@ -139,6 +143,7 @@ class RunManager:
|
|
|
139
143
|
base_messages: list[ConversationMessage],
|
|
140
144
|
agent: RunAgent,
|
|
141
145
|
session_cost_base: float | None = None,
|
|
146
|
+
on_complete: RunCompletionCallback | None = None,
|
|
142
147
|
) -> dict[str, Any]:
|
|
143
148
|
await self._prune_finished_runs()
|
|
144
149
|
|
|
@@ -155,6 +160,7 @@ class RunManager:
|
|
|
155
160
|
session_cost_base=session_cost_base,
|
|
156
161
|
base_messages=copy.deepcopy(base_messages),
|
|
157
162
|
agent=agent,
|
|
163
|
+
on_complete=on_complete,
|
|
158
164
|
)
|
|
159
165
|
state.task = asyncio.create_task(self._run(state), name=f"mycode-run-{state.id}")
|
|
160
166
|
self._active_by_session[session_id] = state
|
|
@@ -323,6 +329,8 @@ class RunManager:
|
|
|
323
329
|
if state.kind == "compact":
|
|
324
330
|
# The agent persists the compact marker before this returns.
|
|
325
331
|
await state.agent.acompact()
|
|
332
|
+
if state.on_complete is not None:
|
|
333
|
+
await state.on_complete(state.session_id)
|
|
326
334
|
await self._append_event(state, Event("compact", {}))
|
|
327
335
|
else:
|
|
328
336
|
assert state.user_message is not None
|
|
@@ -111,4 +111,4 @@ diff --git`)}function xa(e,t){if(e.length===0)return[``];let n=`\n${t}`,r=e.star
|
|
|
111
111
|
),
|
|
112
112
|
var(--diffs-bg);
|
|
113
113
|
}
|
|
114
|
-
`;function Oo({patch:e}){let{resolvedTheme:t}=r();return(0,N.jsx)(`div`,{className:`rounded-md bg-code overflow-hidden`,children:(0,N.jsx)(Co,{patch:e,style:{...Eo,colorScheme:t},disableWorkerPool:!0,options:{theme:To,themeType:t,diffStyle:`unified`,diffIndicators:`classic`,disableFileHeader:!0,hunkSeparators:`simple`,overflow:`scroll`,unsafeCSS:Do}})})}export{Oo as default};
|
|
114
|
+
`;function Oo({patch:e}){let{resolvedTheme:t}=r();return(0,N.jsx)(`div`,{className:`rounded-md bg-code shadow-hairline overflow-hidden`,children:(0,N.jsx)(Co,{patch:e,style:{...Eo,colorScheme:t},disableWorkerPool:!0,options:{theme:To,themeType:t,diffStyle:`unified`,diffIndicators:`classic`,disableFileHeader:!0,hunkSeparators:`simple`,overflow:`scroll`,unsafeCSS:Do}})})}export{Oo as default};
|