gemcode 0.2.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.
- gemcode/__init__.py +3 -0
- gemcode/__main__.py +3 -0
- gemcode/agent.py +146 -0
- gemcode/audit.py +16 -0
- gemcode/callbacks.py +473 -0
- gemcode/capability_routing.py +137 -0
- gemcode/cli.py +658 -0
- gemcode/compaction.py +35 -0
- gemcode/computer_use/__init__.py +0 -0
- gemcode/computer_use/browser_computer.py +275 -0
- gemcode/config.py +247 -0
- gemcode/interactions.py +15 -0
- gemcode/invoke.py +151 -0
- gemcode/kairos_daemon.py +221 -0
- gemcode/limits.py +83 -0
- gemcode/live_audio_engine.py +124 -0
- gemcode/mcp_loader.py +57 -0
- gemcode/memory/__init__.py +0 -0
- gemcode/memory/embedding_memory_service.py +292 -0
- gemcode/memory/file_memory_service.py +176 -0
- gemcode/modality_tools.py +216 -0
- gemcode/model_routing.py +179 -0
- gemcode/paths.py +29 -0
- gemcode/permissions.py +5 -0
- gemcode/plugins/__init__.py +0 -0
- gemcode/plugins/terminal_hooks_plugin.py +168 -0
- gemcode/plugins/tool_recovery_plugin.py +135 -0
- gemcode/prompt_suggestions.py +80 -0
- gemcode/query/__init__.py +36 -0
- gemcode/query/config.py +35 -0
- gemcode/query/deps.py +20 -0
- gemcode/query/engine.py +55 -0
- gemcode/query/stop_hooks.py +63 -0
- gemcode/query/token_budget.py +109 -0
- gemcode/query/transitions.py +41 -0
- gemcode/session_runtime.py +81 -0
- gemcode/thinking.py +136 -0
- gemcode/tool_prompt_manifest.py +118 -0
- gemcode/tool_registry.py +50 -0
- gemcode/tools/__init__.py +25 -0
- gemcode/tools/edit.py +53 -0
- gemcode/tools/filesystem.py +73 -0
- gemcode/tools/search.py +85 -0
- gemcode/tools/shell.py +73 -0
- gemcode/tools_inspector.py +132 -0
- gemcode/trust.py +54 -0
- gemcode/tui/app.py +697 -0
- gemcode/tui/scrollback.py +312 -0
- gemcode/vertex.py +22 -0
- gemcode/web/__init__.py +2 -0
- gemcode/web/claude_sse_adapter.py +282 -0
- gemcode/web/terminal_repl.py +147 -0
- gemcode-0.2.2.dist-info/METADATA +440 -0
- gemcode-0.2.2.dist-info/RECORD +58 -0
- gemcode-0.2.2.dist-info/WHEEL +5 -0
- gemcode-0.2.2.dist-info/entry_points.txt +2 -0
- gemcode-0.2.2.dist-info/licenses/LICENSE +151 -0
- gemcode-0.2.2.dist-info/top_level.txt +1 -0
gemcode/tools/edit.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Write and search_replace tools."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from gemcode.config import GemCodeConfig
|
|
8
|
+
from gemcode.paths import PathEscapeError, resolve_under_root
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def make_edit_tools(cfg: GemCodeConfig):
|
|
12
|
+
root = cfg.project_root
|
|
13
|
+
|
|
14
|
+
def write_file(path: str, content: str) -> dict:
|
|
15
|
+
"""Create or overwrite a file relative to the project root."""
|
|
16
|
+
try:
|
|
17
|
+
p = resolve_under_root(root, path)
|
|
18
|
+
except PathEscapeError as e:
|
|
19
|
+
return {"error": str(e)}
|
|
20
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
21
|
+
p.write_text(content, encoding="utf-8")
|
|
22
|
+
return {"path": path, "bytes_written": len(content.encode("utf-8"))}
|
|
23
|
+
|
|
24
|
+
def search_replace(
|
|
25
|
+
path: str,
|
|
26
|
+
old_string: str,
|
|
27
|
+
new_string: str,
|
|
28
|
+
replace_all: bool = False,
|
|
29
|
+
) -> dict:
|
|
30
|
+
"""
|
|
31
|
+
Replace old_string with new_string in a text file. Fails if old_string
|
|
32
|
+
is missing or duplicate (unless replace_all=True).
|
|
33
|
+
"""
|
|
34
|
+
try:
|
|
35
|
+
p = resolve_under_root(root, path)
|
|
36
|
+
except PathEscapeError as e:
|
|
37
|
+
return {"error": str(e)}
|
|
38
|
+
if not p.is_file():
|
|
39
|
+
return {"error": f"Not a file: {path}"}
|
|
40
|
+
text = p.read_text(encoding="utf-8", errors="strict")
|
|
41
|
+
count = text.count(old_string)
|
|
42
|
+
if count == 0:
|
|
43
|
+
return {"error": "old_string not found"}
|
|
44
|
+
if count > 1 and not replace_all:
|
|
45
|
+
return {"error": f"old_string appears {count} times; set replace_all=true or narrow snippet"}
|
|
46
|
+
if replace_all:
|
|
47
|
+
new_text = text.replace(old_string, new_string)
|
|
48
|
+
else:
|
|
49
|
+
new_text = text.replace(old_string, new_string, 1)
|
|
50
|
+
p.write_text(new_text, encoding="utf-8")
|
|
51
|
+
return {"path": path, "replacements": count if replace_all else 1}
|
|
52
|
+
|
|
53
|
+
return write_file, search_replace
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Read and list files under project root."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from gemcode.config import GemCodeConfig
|
|
8
|
+
from gemcode.paths import PathEscapeError, resolve_under_root
|
|
9
|
+
from gemcode.trust import is_trusted_root
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def make_filesystem_tools(cfg: GemCodeConfig):
|
|
13
|
+
root = cfg.project_root
|
|
14
|
+
trusted = is_trusted_root(root)
|
|
15
|
+
|
|
16
|
+
def read_file(path: str, max_bytes: int = 200_000) -> dict:
|
|
17
|
+
"""Read a text file relative to the project root. Large files are truncated."""
|
|
18
|
+
if not trusted:
|
|
19
|
+
return {"error": "Project folder is not trusted. Re-run GemCode and approve folder trust."}
|
|
20
|
+
try:
|
|
21
|
+
p = resolve_under_root(root, path)
|
|
22
|
+
except PathEscapeError as e:
|
|
23
|
+
return {"error": str(e)}
|
|
24
|
+
if not p.is_file():
|
|
25
|
+
return {"error": f"Not a file: {path}"}
|
|
26
|
+
data = p.read_bytes()
|
|
27
|
+
truncated = len(data) > max_bytes
|
|
28
|
+
text = data[:max_bytes].decode("utf-8", errors="replace")
|
|
29
|
+
return {
|
|
30
|
+
"path": path,
|
|
31
|
+
"content": text,
|
|
32
|
+
"truncated": truncated,
|
|
33
|
+
"total_bytes": len(data),
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
def list_directory(path: str = ".") -> dict:
|
|
37
|
+
"""List files and directories under path (relative to project root)."""
|
|
38
|
+
if not trusted:
|
|
39
|
+
return {"error": "Project folder is not trusted. Re-run GemCode and approve folder trust."}
|
|
40
|
+
try:
|
|
41
|
+
p = resolve_under_root(root, path)
|
|
42
|
+
except PathEscapeError as e:
|
|
43
|
+
return {"error": str(e)}
|
|
44
|
+
if not p.is_dir():
|
|
45
|
+
return {"error": f"Not a directory: {path}"}
|
|
46
|
+
entries: list[dict] = []
|
|
47
|
+
for child in sorted(p.iterdir(), key=lambda x: (not x.is_dir(), x.name.lower())):
|
|
48
|
+
entries.append(
|
|
49
|
+
{
|
|
50
|
+
"name": child.name,
|
|
51
|
+
"type": "dir" if child.is_dir() else "file",
|
|
52
|
+
}
|
|
53
|
+
)
|
|
54
|
+
return {"path": path, "entries": entries[:500]}
|
|
55
|
+
|
|
56
|
+
def glob_files(pattern: str) -> dict:
|
|
57
|
+
"""Glob file paths relative to project root (e.g. 'src/**/*.py')."""
|
|
58
|
+
if not trusted:
|
|
59
|
+
return {"error": "Project folder is not trusted. Re-run GemCode and approve folder trust."}
|
|
60
|
+
if ".." in pattern or pattern.startswith("/"):
|
|
61
|
+
return {"error": "Invalid pattern"}
|
|
62
|
+
matches: list[str] = []
|
|
63
|
+
for m in root.glob(pattern):
|
|
64
|
+
try:
|
|
65
|
+
rel = m.resolve().relative_to(root)
|
|
66
|
+
except ValueError:
|
|
67
|
+
continue
|
|
68
|
+
matches.append(str(rel))
|
|
69
|
+
if len(matches) >= 200:
|
|
70
|
+
break
|
|
71
|
+
return {"pattern": pattern, "matches": matches}
|
|
72
|
+
|
|
73
|
+
return read_file, list_directory, glob_files
|
gemcode/tools/search.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Grep content with regex (MVP: Python scan, optional rg)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
import subprocess
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from gemcode.config import GemCodeConfig
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def make_grep_tool(cfg: GemCodeConfig):
|
|
13
|
+
root = cfg.project_root
|
|
14
|
+
|
|
15
|
+
def grep_content(
|
|
16
|
+
pattern: str,
|
|
17
|
+
path_glob: str = "**/*",
|
|
18
|
+
max_matches: int = 80,
|
|
19
|
+
) -> dict:
|
|
20
|
+
"""
|
|
21
|
+
Search file contents with a regex pattern. Scans files under path_glob
|
|
22
|
+
(glob relative to project root). Binary files skipped.
|
|
23
|
+
"""
|
|
24
|
+
if max_matches < 1:
|
|
25
|
+
max_matches = 1
|
|
26
|
+
if max_matches > 500:
|
|
27
|
+
max_matches = 500
|
|
28
|
+
try:
|
|
29
|
+
re.compile(pattern)
|
|
30
|
+
except re.error as e:
|
|
31
|
+
return {"error": f"Invalid regex: {e}"}
|
|
32
|
+
|
|
33
|
+
# Prefer ripgrep if available (faster)
|
|
34
|
+
rg = Path("/usr/bin/rg") if Path("/usr/bin/rg").is_file() else None
|
|
35
|
+
if rg is None:
|
|
36
|
+
rg = Path("/opt/homebrew/bin/rg") if Path("/opt/homebrew/bin/rg").is_file() else None
|
|
37
|
+
if rg and rg.is_file():
|
|
38
|
+
try:
|
|
39
|
+
proc = subprocess.run(
|
|
40
|
+
[
|
|
41
|
+
str(rg),
|
|
42
|
+
"-n",
|
|
43
|
+
"--glob",
|
|
44
|
+
path_glob,
|
|
45
|
+
"--glob",
|
|
46
|
+
"!.git/*",
|
|
47
|
+
pattern,
|
|
48
|
+
".",
|
|
49
|
+
],
|
|
50
|
+
cwd=root,
|
|
51
|
+
capture_output=True,
|
|
52
|
+
text=True,
|
|
53
|
+
timeout=60,
|
|
54
|
+
check=False,
|
|
55
|
+
)
|
|
56
|
+
lines = proc.stdout.splitlines()[:max_matches]
|
|
57
|
+
return {"pattern": pattern, "matches": lines, "backend": "rg"}
|
|
58
|
+
except (subprocess.TimeoutExpired, OSError):
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
rx = re.compile(pattern)
|
|
62
|
+
matches: list[str] = []
|
|
63
|
+
for fp in root.glob(path_glob):
|
|
64
|
+
if not fp.is_file():
|
|
65
|
+
continue
|
|
66
|
+
if fp.stat().st_size > 2_000_000:
|
|
67
|
+
continue
|
|
68
|
+
try:
|
|
69
|
+
text = fp.read_text(encoding="utf-8", errors="ignore")
|
|
70
|
+
except OSError:
|
|
71
|
+
continue
|
|
72
|
+
for i, line in enumerate(text.splitlines(), 1):
|
|
73
|
+
if rx.search(line):
|
|
74
|
+
rel = fp.resolve().relative_to(root)
|
|
75
|
+
matches.append(f"{rel}:{i}:{line[:500]}")
|
|
76
|
+
if len(matches) >= max_matches:
|
|
77
|
+
return {
|
|
78
|
+
"pattern": pattern,
|
|
79
|
+
"matches": matches,
|
|
80
|
+
"truncated": True,
|
|
81
|
+
"backend": "python",
|
|
82
|
+
}
|
|
83
|
+
return {"pattern": pattern, "matches": matches, "backend": "python"}
|
|
84
|
+
|
|
85
|
+
return grep_content
|
gemcode/tools/shell.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Allowlisted subprocess execution."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import shutil
|
|
7
|
+
import subprocess
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from gemcode.config import GemCodeConfig
|
|
11
|
+
from gemcode.trust import is_trusted_root
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def make_run_command(cfg: GemCodeConfig):
|
|
15
|
+
root = cfg.project_root
|
|
16
|
+
trusted = is_trusted_root(root)
|
|
17
|
+
|
|
18
|
+
def run_command(
|
|
19
|
+
command: str,
|
|
20
|
+
args: list[str] | None = None,
|
|
21
|
+
timeout_seconds: int = 120,
|
|
22
|
+
) -> dict:
|
|
23
|
+
"""
|
|
24
|
+
Run an allowlisted executable with arguments under the project root cwd.
|
|
25
|
+
|
|
26
|
+
The executable must be a basename (no shell metacharacters) and appear in
|
|
27
|
+
GEMCODE_ALLOW_COMMANDS / default allowlist.
|
|
28
|
+
"""
|
|
29
|
+
if not trusted:
|
|
30
|
+
return {"error": "Project folder is not trusted. Re-run GemCode and approve folder trust."}
|
|
31
|
+
args = args or []
|
|
32
|
+
if timeout_seconds < 1:
|
|
33
|
+
timeout_seconds = 1
|
|
34
|
+
if timeout_seconds > 600:
|
|
35
|
+
timeout_seconds = 600
|
|
36
|
+
if any(c in command for c in ";|&$`"):
|
|
37
|
+
return {"error": "Command must be a single executable name, not a shell snippet"}
|
|
38
|
+
exe = Path(command).name
|
|
39
|
+
if exe != command:
|
|
40
|
+
return {"error": "Use basename only for command (e.g. pytest, not /usr/bin/pytest)"}
|
|
41
|
+
|
|
42
|
+
allowed = cfg.allow_commands
|
|
43
|
+
if exe not in allowed:
|
|
44
|
+
return {
|
|
45
|
+
"error": (
|
|
46
|
+
f"Command {exe!r} not in allowlist. Add it to GEMCODE_ALLOW_COMMANDS "
|
|
47
|
+
f"(comma-separated)."
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
resolved = shutil.which(exe)
|
|
52
|
+
if not resolved:
|
|
53
|
+
return {"error": f"Executable not found on PATH: {exe}"}
|
|
54
|
+
try:
|
|
55
|
+
proc = subprocess.run(
|
|
56
|
+
[resolved, *args],
|
|
57
|
+
cwd=root,
|
|
58
|
+
capture_output=True,
|
|
59
|
+
text=True,
|
|
60
|
+
timeout=timeout_seconds,
|
|
61
|
+
env={**os.environ},
|
|
62
|
+
check=False,
|
|
63
|
+
)
|
|
64
|
+
return {
|
|
65
|
+
"command": [exe, *args],
|
|
66
|
+
"exit_code": proc.returncode,
|
|
67
|
+
"stdout": proc.stdout[:50_000],
|
|
68
|
+
"stderr": proc.stderr[:50_000],
|
|
69
|
+
}
|
|
70
|
+
except subprocess.TimeoutExpired:
|
|
71
|
+
return {"error": f"Timeout after {timeout_seconds}s"}
|
|
72
|
+
|
|
73
|
+
return run_command
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""
|
|
2
|
+
GemCode tool inventory + declaration smoke testing.
|
|
3
|
+
|
|
4
|
+
Claude Code's tool-system prompt emphasizes:
|
|
5
|
+
- inventory of available tools (feature-gated vs always-on)
|
|
6
|
+
- schema/declaration compilation smoke tests per tool
|
|
7
|
+
- permission/category metadata
|
|
8
|
+
|
|
9
|
+
In GemCode (ADK-based), tool declarations are produced by ADK via:
|
|
10
|
+
- wrapping callables in `google.adk.tools.function_tool.FunctionTool`
|
|
11
|
+
- calling `_get_declaration()` on BaseTool implementations
|
|
12
|
+
|
|
13
|
+
This module provides a deterministic way to:
|
|
14
|
+
- enumerate tools for a given `GemCodeConfig`
|
|
15
|
+
- validate each tool can build its declaration (when supported)
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from dataclasses import dataclass
|
|
21
|
+
from typing import Any, Iterable
|
|
22
|
+
|
|
23
|
+
from google.adk.tools.base_tool import BaseTool
|
|
24
|
+
from google.adk.tools.function_tool import FunctionTool
|
|
25
|
+
|
|
26
|
+
from gemcode.config import GemCodeConfig
|
|
27
|
+
from gemcode.modality_tools import build_extra_tools as build_modality_tools
|
|
28
|
+
from gemcode.tools import build_function_tools
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True)
|
|
32
|
+
class ToolInspection:
|
|
33
|
+
name: str
|
|
34
|
+
category: str
|
|
35
|
+
declaration_present: bool
|
|
36
|
+
declaration_error: str | None = None
|
|
37
|
+
tool_type: str = "callable" # "callable" | "builtin"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _tool_name(tool_union: Any) -> str:
|
|
41
|
+
if isinstance(tool_union, BaseTool):
|
|
42
|
+
return getattr(tool_union, "name", "") or ""
|
|
43
|
+
if callable(tool_union):
|
|
44
|
+
return getattr(tool_union, "__name__", "") or ""
|
|
45
|
+
return ""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _classify_tool(name: str) -> str:
|
|
49
|
+
# Keep this consistent with gemcode/src/gemcode/tool_registry.py
|
|
50
|
+
from gemcode.tool_registry import READ_ONLY_TOOLS, MUTATING_TOOLS, SHELL_TOOLS
|
|
51
|
+
|
|
52
|
+
if name in READ_ONLY_TOOLS:
|
|
53
|
+
return "read_only"
|
|
54
|
+
if name in MUTATING_TOOLS:
|
|
55
|
+
return "mutating"
|
|
56
|
+
if name in SHELL_TOOLS:
|
|
57
|
+
return "shell"
|
|
58
|
+
return "other"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _iter_tool_unions(cfg: GemCodeConfig, *, extra_tools: Iterable[Any] | None = None):
|
|
62
|
+
# Core function tools (always available for the agent).
|
|
63
|
+
tools: list[Any] = list(build_function_tools(cfg))
|
|
64
|
+
|
|
65
|
+
# Deep research built-in tools (Search/URL/Maps).
|
|
66
|
+
tools.extend(build_modality_tools(cfg))
|
|
67
|
+
|
|
68
|
+
# Optional MCP toolsets are provided as `extra_tools` by the caller (CLI).
|
|
69
|
+
if extra_tools:
|
|
70
|
+
tools.extend(list(extra_tools))
|
|
71
|
+
|
|
72
|
+
# Note: ComputerUseToolset requires launching Playwright via BrowserComputer.
|
|
73
|
+
# Tool declaration smoke tests can be slow and brittle, so we intentionally
|
|
74
|
+
# omit it from inventory unless the caller constructs it already.
|
|
75
|
+
#
|
|
76
|
+
# If you want it included, pass it via `extra_tools` explicitly.
|
|
77
|
+
return tools
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def inspect_tools(
|
|
81
|
+
cfg: GemCodeConfig,
|
|
82
|
+
*,
|
|
83
|
+
extra_tools: Iterable[Any] | None = None,
|
|
84
|
+
) -> list[ToolInspection]:
|
|
85
|
+
"""
|
|
86
|
+
Enumerate tools for this config and attempt to compile each tool's
|
|
87
|
+
declaration (schema) without executing tool logic.
|
|
88
|
+
"""
|
|
89
|
+
out: list[ToolInspection] = []
|
|
90
|
+
|
|
91
|
+
for tool_union in _iter_tool_unions(cfg, extra_tools=extra_tools):
|
|
92
|
+
name = _tool_name(tool_union) or "<unknown>"
|
|
93
|
+
category = _classify_tool(name)
|
|
94
|
+
tool_type = "builtin" if isinstance(tool_union, BaseTool) else "callable"
|
|
95
|
+
|
|
96
|
+
declaration_present = False
|
|
97
|
+
declaration_error: str | None = None
|
|
98
|
+
|
|
99
|
+
try:
|
|
100
|
+
if isinstance(tool_union, BaseTool):
|
|
101
|
+
decl = tool_union._get_declaration()
|
|
102
|
+
else:
|
|
103
|
+
# For pure callables, ADK uses FunctionTool to build FunctionDeclaration.
|
|
104
|
+
decl_tool = FunctionTool(func=tool_union) # type: ignore[arg-type]
|
|
105
|
+
decl = decl_tool._get_declaration()
|
|
106
|
+
|
|
107
|
+
declaration_present = decl is not None
|
|
108
|
+
except Exception as e:
|
|
109
|
+
declaration_present = False
|
|
110
|
+
declaration_error = f"{type(e).__name__}: {e}"
|
|
111
|
+
|
|
112
|
+
out.append(
|
|
113
|
+
ToolInspection(
|
|
114
|
+
name=name,
|
|
115
|
+
category=category,
|
|
116
|
+
declaration_present=declaration_present,
|
|
117
|
+
declaration_error=declaration_error,
|
|
118
|
+
tool_type=tool_type,
|
|
119
|
+
)
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
# Stable output ordering.
|
|
123
|
+
out.sort(key=lambda x: (x.category, x.name.lower()))
|
|
124
|
+
return out
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def smoke_tools(
|
|
128
|
+
inspections: list[ToolInspection],
|
|
129
|
+
) -> list[ToolInspection]:
|
|
130
|
+
"""Return only the tools that failed declaration compilation."""
|
|
131
|
+
return [i for i in inspections if i.declaration_error is not None]
|
|
132
|
+
|
gemcode/trust.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _trust_file_path() -> Path:
|
|
9
|
+
base = Path(os.environ.get("GEMCODE_HOME") or (Path.home() / ".gemcode"))
|
|
10
|
+
return base / "trust.json"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def load_trusted_roots() -> set[str]:
|
|
14
|
+
"""
|
|
15
|
+
Returns a set of resolved absolute paths (as strings) that are trusted.
|
|
16
|
+
"""
|
|
17
|
+
p = _trust_file_path()
|
|
18
|
+
try:
|
|
19
|
+
data = json.loads(p.read_text("utf-8"))
|
|
20
|
+
except FileNotFoundError:
|
|
21
|
+
return set()
|
|
22
|
+
except Exception:
|
|
23
|
+
return set()
|
|
24
|
+
roots = data.get("trusted_roots") if isinstance(data, dict) else None
|
|
25
|
+
if not isinstance(roots, list):
|
|
26
|
+
return set()
|
|
27
|
+
out: set[str] = set()
|
|
28
|
+
for r in roots:
|
|
29
|
+
if isinstance(r, str) and r:
|
|
30
|
+
out.add(str(Path(r).resolve()))
|
|
31
|
+
return out
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def save_trusted_roots(roots: set[str]) -> None:
|
|
35
|
+
p = _trust_file_path()
|
|
36
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
37
|
+
payload = {"trusted_roots": sorted(set(str(Path(r).resolve()) for r in roots))}
|
|
38
|
+
p.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def is_trusted_root(root: Path) -> bool:
|
|
42
|
+
r = str(root.resolve())
|
|
43
|
+
return r in load_trusted_roots()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def trust_root(root: Path, *, trusted: bool) -> None:
|
|
47
|
+
roots = load_trusted_roots()
|
|
48
|
+
r = str(root.resolve())
|
|
49
|
+
if trusted:
|
|
50
|
+
roots.add(r)
|
|
51
|
+
else:
|
|
52
|
+
roots.discard(r)
|
|
53
|
+
save_trusted_roots(roots)
|
|
54
|
+
|