gemcode 0.3.74__tar.gz → 0.3.75__tar.gz
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.
- {gemcode-0.3.74/src/gemcode.egg-info → gemcode-0.3.75}/PKG-INFO +1 -1
- {gemcode-0.3.74 → gemcode-0.3.75}/pyproject.toml +1 -1
- {gemcode-0.3.74 → gemcode-0.3.75}/src/gemcode/cli.py +15 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/src/gemcode/config.py +6 -0
- gemcode-0.3.75/src/gemcode/ide_protocol.py +58 -0
- gemcode-0.3.75/src/gemcode/ide_stdio.py +164 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/src/gemcode/tools/bash.py +22 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/src/gemcode/tools/edit.py +86 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/src/gemcode/tools/shell.py +24 -0
- {gemcode-0.3.74 → gemcode-0.3.75/src/gemcode.egg-info}/PKG-INFO +1 -1
- {gemcode-0.3.74 → gemcode-0.3.75}/src/gemcode.egg-info/SOURCES.txt +2 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/LICENSE +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/MANIFEST.in +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/README.md +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/setup.cfg +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/src/gemcode/__init__.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/src/gemcode/__main__.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/src/gemcode/agent.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/src/gemcode/audit.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/src/gemcode/autocompact.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/src/gemcode/callbacks.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/src/gemcode/capability_routing.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/src/gemcode/compaction.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/src/gemcode/computer_use/__init__.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/src/gemcode/computer_use/browser_computer.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/src/gemcode/context_budget.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/src/gemcode/context_warning.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/src/gemcode/credentials.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/src/gemcode/dynamic_policy.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/src/gemcode/hitl_session.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/src/gemcode/hooks.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/src/gemcode/intent_classifier.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/src/gemcode/interactions.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/src/gemcode/invoke.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/src/gemcode/kairos_daemon.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/src/gemcode/limits.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/src/gemcode/live_audio_engine.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/src/gemcode/logging_config.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/src/gemcode/mcp_loader.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/src/gemcode/memory/__init__.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/src/gemcode/memory/embedding_memory_service.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/src/gemcode/memory/file_memory_service.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/src/gemcode/modality_tools.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/src/gemcode/model_errors.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/src/gemcode/model_routing.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/src/gemcode/openapi_loader.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/src/gemcode/paths.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/src/gemcode/permissions.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/src/gemcode/plugins/__init__.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/src/gemcode/plugins/terminal_hooks_plugin.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/src/gemcode/plugins/tool_recovery_plugin.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/src/gemcode/policy_profile.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/src/gemcode/pricing.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/src/gemcode/prompt_suggestions.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/src/gemcode/query/__init__.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/src/gemcode/query/config.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/src/gemcode/query/deps.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/src/gemcode/query/engine.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/src/gemcode/query/stop_hooks.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/src/gemcode/query/token_budget.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/src/gemcode/query/transitions.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/src/gemcode/refine.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/src/gemcode/repl_commands.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/src/gemcode/repl_slash.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/src/gemcode/review_agent.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/src/gemcode/session_runtime.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/src/gemcode/session_store.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/src/gemcode/slash_commands.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/src/gemcode/thinking.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/src/gemcode/tool_prompt_manifest.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/src/gemcode/tool_registry.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/src/gemcode/tool_result_store.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/src/gemcode/tools/__init__.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/src/gemcode/tools/browser.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/src/gemcode/tools/filesystem.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/src/gemcode/tools/notebook.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/src/gemcode/tools/notes.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/src/gemcode/tools/repo_map.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/src/gemcode/tools/search.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/src/gemcode/tools/shell_gate.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/src/gemcode/tools/subtask.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/src/gemcode/tools/tasks.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/src/gemcode/tools/think.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/src/gemcode/tools/todo.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/src/gemcode/tools/web.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/src/gemcode/tools/web_search.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/src/gemcode/tools_inspector.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/src/gemcode/trust.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/src/gemcode/tui/input_handler.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/src/gemcode/tui/scrollback.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/src/gemcode/tui/spinner.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/src/gemcode/tui/welcome_banner.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/src/gemcode/tui/welcome_rich.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/src/gemcode/version.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/src/gemcode/vertex.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/src/gemcode/web/__init__.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/src/gemcode/web/claude_sse_adapter.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/src/gemcode/web/terminal_repl.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/src/gemcode/workspace_hints.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/src/gemcode.egg-info/dependency_links.txt +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/src/gemcode.egg-info/entry_points.txt +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/src/gemcode.egg-info/requires.txt +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/src/gemcode.egg-info/top_level.txt +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/tests/test_agent_instruction.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/tests/test_autocompact.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/tests/test_capability_routing.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/tests/test_claude_web_adapter_sse.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/tests/test_cli_init.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/tests/test_computer_use_permissions.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/tests/test_context_budget.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/tests/test_context_warning.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/tests/test_credentials.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/tests/test_interactive_permission_ask.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/tests/test_kairos_scheduler.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/tests/test_modality_tools.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/tests/test_model_error_retry.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/tests/test_model_errors.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/tests/test_model_routing.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/tests/test_paths.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/tests/test_permissions.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/tests/test_prompt_suggestions.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/tests/test_repl_commands.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/tests/test_repl_slash.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/tests/test_slash_commands.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/tests/test_thinking_config.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/tests/test_token_budget.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/tests/test_tool_context_circulation.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/tests/test_tools.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/tests/test_tools_inspector.py +0 -0
- {gemcode-0.3.74 → gemcode-0.3.75}/tests/test_workspace_hints.py +0 -0
|
@@ -19,6 +19,7 @@ from gemcode.capability_routing import apply_capability_routing
|
|
|
19
19
|
from gemcode.session_runtime import create_runner
|
|
20
20
|
from gemcode.trust import is_trusted_root, trust_root
|
|
21
21
|
from gemcode.repl_slash import process_repl_slash
|
|
22
|
+
from gemcode.ide_stdio import main as ide_stdio_main
|
|
22
23
|
|
|
23
24
|
|
|
24
25
|
def _events_to_text(events) -> str:
|
|
@@ -339,6 +340,20 @@ def main() -> None:
|
|
|
339
340
|
"enable Terminal for Desktop Folder (or grant Full Disk Access)."
|
|
340
341
|
)
|
|
341
342
|
|
|
343
|
+
# Hidden IDE engine mode: `gemcode ide --stdio`
|
|
344
|
+
if len(sys.argv) >= 2 and sys.argv[1] == "ide":
|
|
345
|
+
ide_parser = argparse.ArgumentParser(prog="gemcode ide")
|
|
346
|
+
ide_parser.add_argument(
|
|
347
|
+
"--stdio",
|
|
348
|
+
action="store_true",
|
|
349
|
+
help="Run IDE engine over stdin/stdout (JSONL)",
|
|
350
|
+
)
|
|
351
|
+
ide_args = ide_parser.parse_args(sys.argv[2:])
|
|
352
|
+
if ide_args.stdio:
|
|
353
|
+
ide_stdio_main()
|
|
354
|
+
return
|
|
355
|
+
raise SystemExit("Usage: gemcode ide --stdio")
|
|
356
|
+
|
|
342
357
|
# Persist or rotate API key (Claude Code–style `claude login`).
|
|
343
358
|
if len(sys.argv) > 1 and sys.argv[1] == "login":
|
|
344
359
|
load_cli_environment()
|
|
@@ -340,6 +340,12 @@ class GemCodeConfig:
|
|
|
340
340
|
default_factory=lambda: _truthy_env("GEMCODE_ENABLE_WEB_SEARCH", default=False)
|
|
341
341
|
)
|
|
342
342
|
|
|
343
|
+
# IDE mode (VS Code extension): the engine should *propose* writes/commands,
|
|
344
|
+
# and the IDE applies them (WorkspaceEdit / terminal task) after user approval.
|
|
345
|
+
ide_proposal_mode: bool = False
|
|
346
|
+
ide_allow_write: bool = False
|
|
347
|
+
ide_allow_shell: bool = False
|
|
348
|
+
|
|
343
349
|
def __post_init__(self) -> None:
|
|
344
350
|
self.project_root = self.project_root.resolve()
|
|
345
351
|
# Default agentic depth when env omits GEMCODE_MAX_LLM_CALLS (was: None → SDK default).
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""
|
|
2
|
+
GemCode IDE stdio protocol (JSON Lines).
|
|
3
|
+
|
|
4
|
+
This module defines the *wire format* for `gemcode ide --stdio`, used by IDE
|
|
5
|
+
extensions (VS Code) to talk to a long-lived GemCode engine process.
|
|
6
|
+
|
|
7
|
+
Design goals:
|
|
8
|
+
- Human-readable JSONL (easy to debug)
|
|
9
|
+
- Streaming (token deltas + progress)
|
|
10
|
+
- Safe editing (engine proposes; IDE applies via WorkspaceEdit)
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import sys
|
|
17
|
+
import time
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
PROTOCOL_VERSION = 1
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _now_ms() -> int:
|
|
26
|
+
return int(time.time() * 1000)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def dumps(obj: Any) -> str:
|
|
30
|
+
return json.dumps(obj, ensure_ascii=False, separators=(",", ":"))
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class IdeEmitter:
|
|
35
|
+
"""Writes JSONL messages to stdout (flushes each line)."""
|
|
36
|
+
|
|
37
|
+
stream: Any = sys.stdout
|
|
38
|
+
|
|
39
|
+
def send(self, msg: dict) -> None:
|
|
40
|
+
msg = dict(msg or {})
|
|
41
|
+
msg.setdefault("v", PROTOCOL_VERSION)
|
|
42
|
+
msg.setdefault("ts_ms", _now_ms())
|
|
43
|
+
self.stream.write(dumps(msg) + "\n")
|
|
44
|
+
try:
|
|
45
|
+
self.stream.flush()
|
|
46
|
+
except Exception:
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def parse_json_line(line: str) -> dict[str, Any]:
|
|
51
|
+
try:
|
|
52
|
+
obj = json.loads(line)
|
|
53
|
+
except Exception as e:
|
|
54
|
+
return {"type": "invalid", "error": f"invalid_json: {e}"}
|
|
55
|
+
if not isinstance(obj, dict):
|
|
56
|
+
return {"type": "invalid", "error": "message must be a JSON object"}
|
|
57
|
+
return obj
|
|
58
|
+
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""
|
|
2
|
+
`gemcode ide --stdio`
|
|
3
|
+
|
|
4
|
+
Long-lived engine process that communicates over stdin/stdout using JSONL.
|
|
5
|
+
|
|
6
|
+
The IDE is responsible for:
|
|
7
|
+
- presenting UI
|
|
8
|
+
- previewing diffs
|
|
9
|
+
- applying changes (WorkspaceEdit)
|
|
10
|
+
|
|
11
|
+
GemCode is responsible for:
|
|
12
|
+
- planning + tool calls
|
|
13
|
+
- proposing edits/commands (when in proposal mode)
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import asyncio
|
|
19
|
+
import os
|
|
20
|
+
import sys
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
from gemcode.config import GemCodeConfig, load_cli_environment
|
|
24
|
+
from gemcode.ide_protocol import IdeEmitter, parse_json_line
|
|
25
|
+
from gemcode.invoke import run_turn
|
|
26
|
+
from gemcode.session_runtime import create_runner
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _truthy(v: Any, default: bool = False) -> bool:
|
|
30
|
+
if v is None:
|
|
31
|
+
return default
|
|
32
|
+
if isinstance(v, bool):
|
|
33
|
+
return v
|
|
34
|
+
if isinstance(v, (int, float)):
|
|
35
|
+
return bool(v)
|
|
36
|
+
if isinstance(v, str):
|
|
37
|
+
return v.strip().lower() in ("1", "true", "yes", "on")
|
|
38
|
+
return default
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _build_prompt(prompt: str, attachments: list[dict] | None) -> str:
|
|
42
|
+
# Keep it simple: attachments are appended as fenced blocks.
|
|
43
|
+
if not attachments:
|
|
44
|
+
return prompt
|
|
45
|
+
parts = [prompt.rstrip()]
|
|
46
|
+
for a in attachments:
|
|
47
|
+
if not isinstance(a, dict):
|
|
48
|
+
continue
|
|
49
|
+
at = (a.get("type") or "").strip().lower()
|
|
50
|
+
if at == "selection":
|
|
51
|
+
txt = a.get("text") or ""
|
|
52
|
+
path = a.get("path") or ""
|
|
53
|
+
rng = a.get("range") or ""
|
|
54
|
+
header = f"Selection from {path}{(' ' + rng) if rng else ''}".strip()
|
|
55
|
+
parts.append(f"\n\n```text\n{header}\n{txt}\n```")
|
|
56
|
+
elif at == "file":
|
|
57
|
+
path = a.get("path") or ""
|
|
58
|
+
snippet = a.get("text") or ""
|
|
59
|
+
header = f"File context: {path}".strip()
|
|
60
|
+
parts.append(f"\n\n```text\n{header}\n{snippet}\n```")
|
|
61
|
+
return "\n".join(parts).strip() + "\n"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
async def run_stdio_loop() -> int:
|
|
65
|
+
load_cli_environment()
|
|
66
|
+
# Keep stdout reserved for protocol JSONL. Redirect accidental prints to stderr.
|
|
67
|
+
proto_out = sys.stdout
|
|
68
|
+
try:
|
|
69
|
+
sys.stdout = sys.stderr # type: ignore[assignment]
|
|
70
|
+
except Exception:
|
|
71
|
+
pass
|
|
72
|
+
emitter = IdeEmitter(stream=proto_out)
|
|
73
|
+
emitter.send({"type": "hello", "protocol": 1})
|
|
74
|
+
|
|
75
|
+
runner = None
|
|
76
|
+
cfg: GemCodeConfig | None = None
|
|
77
|
+
session_id: str | None = None
|
|
78
|
+
|
|
79
|
+
try:
|
|
80
|
+
for raw in sys.stdin:
|
|
81
|
+
msg = parse_json_line(raw)
|
|
82
|
+
mtype = msg.get("type")
|
|
83
|
+
if mtype in ("invalid", None):
|
|
84
|
+
emitter.send({"type": "error", "error": msg.get("error") or "invalid"})
|
|
85
|
+
continue
|
|
86
|
+
|
|
87
|
+
if mtype == "shutdown":
|
|
88
|
+
emitter.send({"type": "bye"})
|
|
89
|
+
return 0
|
|
90
|
+
|
|
91
|
+
if mtype != "turn":
|
|
92
|
+
emitter.send({"type": "error", "error": f"unknown_type:{mtype}"})
|
|
93
|
+
continue
|
|
94
|
+
|
|
95
|
+
# Lazily initialize runner on first turn (needs project root).
|
|
96
|
+
if cfg is None:
|
|
97
|
+
root = msg.get("project_root") or os.getcwd()
|
|
98
|
+
model = msg.get("model") or os.environ.get("GEMCODE_MODEL") or ""
|
|
99
|
+
from pathlib import Path
|
|
100
|
+
cfg = GemCodeConfig(project_root=Path(str(root)), model=str(model))
|
|
101
|
+
# Attach emitter + proposal mode flags (used by tool wrappers).
|
|
102
|
+
object.__setattr__(cfg, "_ide_emitter", emitter)
|
|
103
|
+
object.__setattr__(cfg, "ide_proposal_mode", True)
|
|
104
|
+
runner = create_runner(cfg, extra_tools=None)
|
|
105
|
+
|
|
106
|
+
if session_id is None:
|
|
107
|
+
session_id = str(msg.get("session") or "vscode")
|
|
108
|
+
|
|
109
|
+
prompt = str(msg.get("prompt") or "")
|
|
110
|
+
attachments = msg.get("attachments") if isinstance(msg.get("attachments"), list) else None
|
|
111
|
+
full_prompt = _build_prompt(prompt, attachments)
|
|
112
|
+
|
|
113
|
+
# Per-turn allow flags (the engine still only proposes in IDE mode; the IDE applies).
|
|
114
|
+
allow_write = _truthy(msg.get("allowWrite"), default=False)
|
|
115
|
+
allow_shell = _truthy(msg.get("allowShell"), default=False)
|
|
116
|
+
object.__setattr__(cfg, "ide_allow_write", bool(allow_write))
|
|
117
|
+
object.__setattr__(cfg, "ide_allow_shell", bool(allow_shell))
|
|
118
|
+
|
|
119
|
+
emitter.send({"type": "turn_start", "session": session_id})
|
|
120
|
+
try:
|
|
121
|
+
events = await run_turn(
|
|
122
|
+
runner,
|
|
123
|
+
user_id="local",
|
|
124
|
+
session_id=session_id,
|
|
125
|
+
prompt=full_prompt,
|
|
126
|
+
max_llm_calls=cfg.max_llm_calls,
|
|
127
|
+
cfg=cfg,
|
|
128
|
+
)
|
|
129
|
+
except Exception as e:
|
|
130
|
+
emitter.send({"type": "error", "error": f"{type(e).__name__}: {e}"})
|
|
131
|
+
emitter.send({"type": "turn_done", "session": session_id, "ok": False})
|
|
132
|
+
continue
|
|
133
|
+
|
|
134
|
+
# Emit assistant text as a single message for now (delta streaming can be added later).
|
|
135
|
+
txt_parts: list[str] = []
|
|
136
|
+
for ev in events:
|
|
137
|
+
try:
|
|
138
|
+
if not getattr(ev, "content", None) or not ev.content.parts:
|
|
139
|
+
continue
|
|
140
|
+
if getattr(ev, "author", None) == "user":
|
|
141
|
+
continue
|
|
142
|
+
for p in ev.content.parts:
|
|
143
|
+
t = getattr(p, "text", None)
|
|
144
|
+
if t:
|
|
145
|
+
txt_parts.append(t)
|
|
146
|
+
except Exception:
|
|
147
|
+
continue
|
|
148
|
+
out_text = "".join(txt_parts).strip()
|
|
149
|
+
if out_text:
|
|
150
|
+
emitter.send({"type": "text", "text": out_text})
|
|
151
|
+
emitter.send({"type": "turn_done", "session": session_id, "ok": True})
|
|
152
|
+
|
|
153
|
+
finally:
|
|
154
|
+
if runner is not None:
|
|
155
|
+
try:
|
|
156
|
+
await runner.close()
|
|
157
|
+
except Exception:
|
|
158
|
+
pass
|
|
159
|
+
return 0
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def main() -> None:
|
|
163
|
+
raise SystemExit(asyncio.run(run_stdio_loop()))
|
|
164
|
+
|
|
@@ -30,6 +30,14 @@ def make_bash_tool(cfg: GemCodeConfig):
|
|
|
30
30
|
root = cfg.project_root
|
|
31
31
|
trusted = is_trusted_root(root)
|
|
32
32
|
|
|
33
|
+
def _emit(msg: dict) -> None:
|
|
34
|
+
em = getattr(cfg, "_ide_emitter", None)
|
|
35
|
+
if em is not None:
|
|
36
|
+
try:
|
|
37
|
+
em.send(msg)
|
|
38
|
+
except Exception:
|
|
39
|
+
pass
|
|
40
|
+
|
|
33
41
|
def bash(
|
|
34
42
|
command: str,
|
|
35
43
|
timeout_seconds: int = 120,
|
|
@@ -95,6 +103,20 @@ def make_bash_tool(cfg: GemCodeConfig):
|
|
|
95
103
|
explicit user approval. Quote file paths that contain spaces.
|
|
96
104
|
cwd_subdir is relative to the project root.
|
|
97
105
|
"""
|
|
106
|
+
if getattr(cfg, "ide_proposal_mode", False):
|
|
107
|
+
if not getattr(cfg, "ide_allow_shell", False):
|
|
108
|
+
_emit({"type": "permission_request", "kind": "shell", "detail": "bash(...)"} )
|
|
109
|
+
return {"error": "shell_not_allowed"}
|
|
110
|
+
_emit(
|
|
111
|
+
{
|
|
112
|
+
"type": "command_suggestion",
|
|
113
|
+
"cmd": command,
|
|
114
|
+
"cwd_subdir": cwd_subdir,
|
|
115
|
+
"background": bool(background),
|
|
116
|
+
"via": "bash",
|
|
117
|
+
}
|
|
118
|
+
)
|
|
119
|
+
return {"suggested": True, "via": "bash", "cmd": command, "cwd_subdir": cwd_subdir, "background": bool(background)}
|
|
98
120
|
if not trusted:
|
|
99
121
|
return {"error": "Project folder is not trusted. Re-run GemCode and approve folder trust."}
|
|
100
122
|
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import hashlib
|
|
6
|
+
import time
|
|
5
7
|
from pathlib import Path
|
|
6
8
|
|
|
7
9
|
from gemcode.config import GemCodeConfig
|
|
@@ -11,6 +13,20 @@ from gemcode.paths import PathEscapeError, resolve_under_root
|
|
|
11
13
|
def make_edit_tools(cfg: GemCodeConfig):
|
|
12
14
|
root = cfg.project_root
|
|
13
15
|
|
|
16
|
+
def _sha256_text(s: str) -> str:
|
|
17
|
+
return hashlib.sha256(s.encode("utf-8", errors="strict")).hexdigest()
|
|
18
|
+
|
|
19
|
+
def _emit(msg: dict) -> None:
|
|
20
|
+
em = getattr(cfg, "_ide_emitter", None)
|
|
21
|
+
if em is not None:
|
|
22
|
+
try:
|
|
23
|
+
em.send(msg)
|
|
24
|
+
except Exception:
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
def _proposal_id(prefix: str) -> str:
|
|
28
|
+
return f"{prefix}_{int(time.time()*1000)}"
|
|
29
|
+
|
|
14
30
|
def write_file(path: str, content: str) -> dict:
|
|
15
31
|
"""
|
|
16
32
|
Create or overwrite a file with the given content.
|
|
@@ -25,6 +41,47 @@ def make_edit_tools(cfg: GemCodeConfig):
|
|
|
25
41
|
Never write non-textual content (binary, base64 blobs) — those belong in
|
|
26
42
|
artifacts, not in source files.
|
|
27
43
|
"""
|
|
44
|
+
if getattr(cfg, "ide_proposal_mode", False):
|
|
45
|
+
if not getattr(cfg, "ide_allow_write", False):
|
|
46
|
+
_emit(
|
|
47
|
+
{
|
|
48
|
+
"type": "permission_request",
|
|
49
|
+
"kind": "write",
|
|
50
|
+
"detail": f"write_file({path})",
|
|
51
|
+
}
|
|
52
|
+
)
|
|
53
|
+
return {"error": "write_not_allowed"}
|
|
54
|
+
# Propose instead of executing.
|
|
55
|
+
try:
|
|
56
|
+
p = resolve_under_root(root, path)
|
|
57
|
+
except PathEscapeError as e:
|
|
58
|
+
return {"error": str(e)}
|
|
59
|
+
old_text = ""
|
|
60
|
+
existed = p.is_file()
|
|
61
|
+
if existed:
|
|
62
|
+
try:
|
|
63
|
+
old_text = p.read_text(encoding="utf-8", errors="strict")
|
|
64
|
+
except Exception:
|
|
65
|
+
old_text = ""
|
|
66
|
+
pid = _proposal_id("edit")
|
|
67
|
+
_emit(
|
|
68
|
+
{
|
|
69
|
+
"type": "edit_proposal",
|
|
70
|
+
"id": pid,
|
|
71
|
+
"files": [
|
|
72
|
+
{
|
|
73
|
+
"path": path,
|
|
74
|
+
"existed": existed,
|
|
75
|
+
"original_sha256": _sha256_text(old_text) if existed else None,
|
|
76
|
+
"new_sha256": _sha256_text(content),
|
|
77
|
+
"old_text": old_text,
|
|
78
|
+
"new_text": content,
|
|
79
|
+
}
|
|
80
|
+
],
|
|
81
|
+
}
|
|
82
|
+
)
|
|
83
|
+
return {"proposal_id": pid, "path": path, "bytes": len(content.encode("utf-8"))}
|
|
84
|
+
|
|
28
85
|
try:
|
|
29
86
|
p = resolve_under_root(root, path)
|
|
30
87
|
except PathEscapeError as e:
|
|
@@ -60,6 +117,16 @@ def make_edit_tools(cfg: GemCodeConfig):
|
|
|
60
117
|
comments that explain non-obvious intent or trade-offs.
|
|
61
118
|
- NEVER propose edits before reading. Read first. Edit second.
|
|
62
119
|
"""
|
|
120
|
+
if getattr(cfg, "ide_proposal_mode", False):
|
|
121
|
+
if not getattr(cfg, "ide_allow_write", False):
|
|
122
|
+
_emit(
|
|
123
|
+
{
|
|
124
|
+
"type": "permission_request",
|
|
125
|
+
"kind": "write",
|
|
126
|
+
"detail": f"search_replace({path})",
|
|
127
|
+
}
|
|
128
|
+
)
|
|
129
|
+
return {"error": "write_not_allowed"}
|
|
63
130
|
try:
|
|
64
131
|
p = resolve_under_root(root, path)
|
|
65
132
|
except PathEscapeError as e:
|
|
@@ -76,6 +143,25 @@ def make_edit_tools(cfg: GemCodeConfig):
|
|
|
76
143
|
new_text = text.replace(old_string, new_string)
|
|
77
144
|
else:
|
|
78
145
|
new_text = text.replace(old_string, new_string, 1)
|
|
146
|
+
if getattr(cfg, "ide_proposal_mode", False):
|
|
147
|
+
pid = _proposal_id("edit")
|
|
148
|
+
_emit(
|
|
149
|
+
{
|
|
150
|
+
"type": "edit_proposal",
|
|
151
|
+
"id": pid,
|
|
152
|
+
"files": [
|
|
153
|
+
{
|
|
154
|
+
"path": path,
|
|
155
|
+
"existed": True,
|
|
156
|
+
"original_sha256": _sha256_text(text),
|
|
157
|
+
"new_sha256": _sha256_text(new_text),
|
|
158
|
+
"old_text": text,
|
|
159
|
+
"new_text": new_text,
|
|
160
|
+
}
|
|
161
|
+
],
|
|
162
|
+
}
|
|
163
|
+
)
|
|
164
|
+
return {"proposal_id": pid, "path": path, "replacements": count if replace_all else 1}
|
|
79
165
|
p.write_text(new_text, encoding="utf-8")
|
|
80
166
|
return {"path": path, "replacements": count if replace_all else 1}
|
|
81
167
|
|
|
@@ -37,6 +37,14 @@ def make_run_command(cfg: GemCodeConfig):
|
|
|
37
37
|
root = cfg.project_root
|
|
38
38
|
trusted = is_trusted_root(root)
|
|
39
39
|
|
|
40
|
+
def _emit(msg: dict) -> None:
|
|
41
|
+
em = getattr(cfg, "_ide_emitter", None)
|
|
42
|
+
if em is not None:
|
|
43
|
+
try:
|
|
44
|
+
em.send(msg)
|
|
45
|
+
except Exception:
|
|
46
|
+
pass
|
|
47
|
+
|
|
40
48
|
def run_command(
|
|
41
49
|
command: str,
|
|
42
50
|
args: list[str] | None = None,
|
|
@@ -61,6 +69,22 @@ def make_run_command(cfg: GemCodeConfig):
|
|
|
61
69
|
merged into the child environment (e.g. keys ["CI"], values ["1"] for
|
|
62
70
|
non-interactive scaffolding tools). Omit both to use the default environment.
|
|
63
71
|
"""
|
|
72
|
+
if getattr(cfg, "ide_proposal_mode", False):
|
|
73
|
+
exe = str(command or "").strip()
|
|
74
|
+
args2 = list(args or [])
|
|
75
|
+
if not getattr(cfg, "ide_allow_shell", False):
|
|
76
|
+
_emit({"type": "permission_request", "kind": "shell", "detail": f"run_command({exe})"})
|
|
77
|
+
return {"error": "shell_not_allowed"}
|
|
78
|
+
_emit(
|
|
79
|
+
{
|
|
80
|
+
"type": "command_suggestion",
|
|
81
|
+
"cmd": " ".join([exe, *args2]).strip(),
|
|
82
|
+
"cwd_subdir": cwd_subdir,
|
|
83
|
+
"background": bool(background),
|
|
84
|
+
}
|
|
85
|
+
)
|
|
86
|
+
return {"suggested": True, "command": [exe, *args2], "cwd_subdir": cwd_subdir, "background": bool(background)}
|
|
87
|
+
|
|
64
88
|
if not trusted:
|
|
65
89
|
return {"error": "Project folder is not trusted. Re-run GemCode and approve folder trust."}
|
|
66
90
|
if not (cwd_subdir or "").strip():
|
|
@@ -18,6 +18,8 @@ src/gemcode/credentials.py
|
|
|
18
18
|
src/gemcode/dynamic_policy.py
|
|
19
19
|
src/gemcode/hitl_session.py
|
|
20
20
|
src/gemcode/hooks.py
|
|
21
|
+
src/gemcode/ide_protocol.py
|
|
22
|
+
src/gemcode/ide_stdio.py
|
|
21
23
|
src/gemcode/intent_classifier.py
|
|
22
24
|
src/gemcode/interactions.py
|
|
23
25
|
src/gemcode/invoke.py
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|