Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 99 additions & 47 deletions src/context_engine/cli.py

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions src/context_engine/dashboard/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ async def csrf_and_auth(request: Request, call_next):
def _read_json(path: Path) -> dict:
if path.exists():
try:
return json.loads(path.read_text())
return json.loads(path.read_text(encoding="utf-8"))
except (json.JSONDecodeError, OSError):
pass
return {}
Expand Down Expand Up @@ -119,7 +119,7 @@ def _read_sessions(limit: int = 20) -> list[dict]:
result = []
for f in files[:limit]:
try:
result.append(json.loads(f.read_text()))
result.append(json.loads(f.read_text(encoding="utf-8")))
except (json.JSONDecodeError, OSError):
pass
return result
Expand Down Expand Up @@ -420,7 +420,7 @@ async def delete_file(file_path: str) -> dict | JSONResponse:
async def set_compression(req: CompressionRequest) -> dict:
state = _read_state()
state["output_level"] = req.level
(storage_base / "state.json").write_text(json.dumps(state))
(storage_base / "state.json").write_text(json.dumps(state), encoding="utf-8")
return {"level": req.level}

@app.get("/api/export")
Expand Down
22 changes: 11 additions & 11 deletions src/context_engine/editors.py
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,7 @@ def configure_mcp(project_dir: Path, editor_key: str) -> bool | None:

if config_path.exists():
try:
data = json.loads(config_path.read_text())
data = json.loads(config_path.read_text(encoding="utf-8"))
except (json.JSONDecodeError, OSError):
data = {}
else:
Expand Down Expand Up @@ -334,7 +334,7 @@ def _configure_opencode(config_path: Path, command: str, project_dir: str) -> bo

if config_path.exists():
try:
content = config_path.read_text()
content = config_path.read_text(encoding="utf-8")
# Strip JSONC comments for parsing
data = json.loads(_strip_jsonc_comments(content))
except (json.JSONDecodeError, OSError):
Expand Down Expand Up @@ -393,7 +393,7 @@ def _configure_toml(
atomic_write_text(config_path, block)
return True

original = config_path.read_text()
original = config_path.read_text(encoding="utf-8")
except OSError:
return None

Expand Down Expand Up @@ -533,13 +533,13 @@ def remove_mcp(project_dir: Path, editor_key: str) -> str | None:

servers_key = editor["servers_key"]
try:
data = json.loads(config_path.read_text())
data = json.loads(config_path.read_text(encoding="utf-8"))
servers = data.get(servers_key, {})
if "context-engine" not in servers:
return None
del servers["context-engine"]
if servers:
config_path.write_text(json.dumps(data, indent=2) + "\n")
config_path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
return f"Removed context-engine from {editor['config_path']}"
else:
config_path.unlink()
Expand All @@ -557,7 +557,7 @@ def _remove_toml(config_path: Path, display_path: str, *, section: str) -> str |
so it can never accidentally match a longer section that shares a prefix
(e.g. removing `cce-api` won't touch `cce-api-staging`)."""
try:
content = config_path.read_text()
content = config_path.read_text(encoding="utf-8")
except OSError:
return None

Expand Down Expand Up @@ -593,13 +593,13 @@ def write_instruction_file(
instructions = _build_instructions(output_level)

if path.exists():
content = path.read_text()
content = path.read_text(encoding="utf-8")
if marker in content:
return False # already has CCE block
# Append
path.write_text(content.rstrip() + "\n\n" + instructions)
path.write_text(content.rstrip() + "\n\n" + instructions, encoding="utf-8")
else:
path.write_text(instructions)
path.write_text(instructions, encoding="utf-8")
return True


Expand All @@ -612,7 +612,7 @@ def remove_instruction_file(project_dir: Path, file_key: str) -> str | None:
if not path.exists():
return None

content = path.read_text()
content = path.read_text(encoding="utf-8")
if marker not in content:
return None

Expand All @@ -628,7 +628,7 @@ def remove_instruction_file(project_dir: Path, file_key: str) -> str | None:

new_content = (content[:start] + content[end:]).strip()
if new_content:
path.write_text(new_content + "\n")
path.write_text(new_content + "\n", encoding="utf-8")
return f"Removed CCE block from {info['name']}"
else:
path.unlink()
Expand Down
4 changes: 2 additions & 2 deletions src/context_engine/indexer/git_hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,13 +67,13 @@ def install_hooks(project_dir: str) -> list[str]:
def _install_single_hook(hook_path: Path) -> None:
script = _hook_script()
if hook_path.exists():
existing = hook_path.read_text()
existing = hook_path.read_text(encoding="utf-8")
if HOOK_MARKER in existing:
return
new_content = existing.rstrip() + "\n\n" + script
else:
new_content = "#!/bin/sh\n\n" + script
hook_path.write_text(new_content)
hook_path.write_text(new_content, encoding="utf-8")
hook_path.chmod(hook_path.stat().st_mode | stat.S_IEXEC)


Expand Down
6 changes: 3 additions & 3 deletions src/context_engine/integration/mcp_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -477,7 +477,7 @@ def _load_stats(self) -> dict:
}
if self._stats_path.exists():
try:
data = json.loads(self._stats_path.read_text())
data = json.loads(self._stats_path.read_text(encoding="utf-8"))
# Backfill new keys for stats files written by older versions.
data.setdefault("queries", 0)
data.setdefault("raw_tokens", 0)
Expand Down Expand Up @@ -515,7 +515,7 @@ def _append_query_log(self) -> None:
import datetime
try:
# Verify the write actually landed
on_disk = self._stats_path.read_text() if self._stats_path.exists() else "missing"
on_disk = self._stats_path.read_text(encoding="utf-8") if self._stats_path.exists() else "missing"
log_path = self._storage_base / "query.log"
q = self._stats["queries"]
entry = (
Expand Down Expand Up @@ -580,7 +580,7 @@ def _append_error_log(self, msg: str) -> None:
def _load_state(self) -> dict:
if self._state_path.exists():
try:
return json.loads(self._state_path.read_text())
return json.loads(self._state_path.read_text(encoding="utf-8"))
except (json.JSONDecodeError, OSError):
pass
return {}
Expand Down
6 changes: 3 additions & 3 deletions src/context_engine/integration/session_capture.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ def _prune_locked(
existing: list[dict] = []
if log_path.exists():
try:
existing = json.loads(log_path.read_text())
existing = json.loads(log_path.read_text(encoding="utf-8"))
if not isinstance(existing, list):
existing = []
except (json.JSONDecodeError, OSError):
Expand All @@ -201,7 +201,7 @@ def _prune_locked(
if f == log_path:
continue
try:
data = json.loads(f.read_text())
data = json.loads(f.read_text(encoding="utf-8"))
except (json.JSONDecodeError, OSError) as exc:
log.warning("Skipping unreadable session file %s: %s", f, exc)
continue
Expand Down Expand Up @@ -244,7 +244,7 @@ def _load_consolidated_decisions(self) -> list[dict]:
if not log_path.exists():
return []
try:
data = json.loads(log_path.read_text())
data = json.loads(log_path.read_text(encoding="utf-8"))
return data if isinstance(data, list) else []
except (json.JSONDecodeError, OSError):
return []
Expand Down
2 changes: 1 addition & 1 deletion src/context_engine/memory/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -837,7 +837,7 @@ def _harvest_and_delete(table: str, columns: list[str], cutoff: int) -> int:

if archive and archived and archive_path is not None:
try:
archive_path.write_text(_json.dumps(archived, indent=2, default=str))
archive_path.write_text(_json.dumps(archived, indent=2, default=str), encoding="utf-8")
log.info("memory: archived pruned rows to %s", archive_path)
except OSError as exc:
log.warning("memory: archive write failed (%s); rows still deleted", exc)
Expand Down
12 changes: 6 additions & 6 deletions src/context_engine/memory/hook_installer.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,10 +190,10 @@ def install_hook_script(target: Path = HOOK_PATH) -> bool:
"""
target.parent.mkdir(parents=True, exist_ok=True)
body = _hook_script_body()
existing = target.read_text() if target.exists() else None
existing = target.read_text(encoding="utf-8") if target.exists() else None
if existing == body:
return False
target.write_text(body)
target.write_text(body, encoding="utf-8")
if not _is_windows():
target.chmod(
target.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
Expand All @@ -215,7 +215,7 @@ def install_settings(project_dir: Path) -> dict:
data: dict = {}
if settings_path.exists():
try:
data = json.loads(settings_path.read_text() or "{}")
data = json.loads(settings_path.read_text(encoding="utf-8") or "{}")
if not isinstance(data, dict):
data = {}
except json.JSONDecodeError:
Expand Down Expand Up @@ -246,7 +246,7 @@ def install_settings(project_dir: Path) -> dict:
})
added.append(hook_name)

settings_path.write_text(json.dumps(data, indent=2) + "\n")
settings_path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
return {"added": added, "skipped": skipped, "settings_path": str(settings_path)}


Expand All @@ -256,7 +256,7 @@ def uninstall_settings(project_dir: Path) -> dict:
if not settings_path.exists():
return {"removed": [], "settings_path": str(settings_path)}
try:
data = json.loads(settings_path.read_text() or "{}")
data = json.loads(settings_path.read_text(encoding="utf-8") or "{}")
except json.JSONDecodeError:
return {"removed": [], "settings_path": str(settings_path)}
if not isinstance(data, dict):
Expand All @@ -279,7 +279,7 @@ def uninstall_settings(project_dir: Path) -> dict:
if removed:
if not hooks:
data.pop("hooks", None)
settings_path.write_text(json.dumps(data, indent=2) + "\n")
settings_path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
return {"removed": removed, "settings_path": str(settings_path)}


Expand Down
4 changes: 2 additions & 2 deletions src/context_engine/memory/hook_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,12 +85,12 @@ async def _unlink_port_files(app):
await site.start()

port_file.parent.mkdir(parents=True, exist_ok=True)
port_file.write_text(str(port))
port_file.write_text(str(port), encoding="utf-8")

try:
if default_rendezvous.resolve() != port_file.resolve():
default_rendezvous.parent.mkdir(parents=True, exist_ok=True)
default_rendezvous.write_text(str(port))
default_rendezvous.write_text(str(port), encoding="utf-8")
except OSError as exc:
# Non-fatal — capture still works for users with default storage.
log.warning("rendezvous port file write failed: %s", exc)
Expand Down
2 changes: 1 addition & 1 deletion src/context_engine/memory/migrate.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ class _ImportCounts:
def _import_one(conn: sqlite3.Connection, source: Path) -> _ImportCounts:
"""Import a single legacy JSON file. Returns counts of imported rows."""
counts = _ImportCounts()
data = json.loads(source.read_text())
data = json.loads(source.read_text(encoding="utf-8"))

# decisions_log.json is a top-level list of decision dicts, not a session.
if source.name == _DECISIONS_LOG_NAME and isinstance(data, list):
Expand Down
4 changes: 2 additions & 2 deletions src/context_engine/pricing.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ def _load_cache() -> dict[str, ModelPricing] | None:
try:
if not _CACHE_PATH.exists():
return None
data = json.loads(_CACHE_PATH.read_text())
data = json.loads(_CACHE_PATH.read_text(encoding="utf-8"))
if time.time() - data.get("ts", 0) < _CACHE_TTL:
raw = data.get("pricing")
if not raw:
Expand All @@ -159,7 +159,7 @@ def _load_cache() -> dict[str, ModelPricing] | None:
def _save_cache(pricing: dict[str, ModelPricing]) -> None:
try:
_CACHE_PATH.parent.mkdir(parents=True, exist_ok=True)
_CACHE_PATH.write_text(json.dumps({"ts": time.time(), "pricing": pricing}))
_CACHE_PATH.write_text(json.dumps({"ts": time.time(), "pricing": pricing}), encoding="utf-8")
except Exception:
pass

Expand Down
8 changes: 4 additions & 4 deletions src/context_engine/project_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ def _load_yaml(path: Path) -> dict:
if not path.exists():
return {}
try:
data = yaml.safe_load(path.read_text()) or {}
data = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
except (yaml.YAMLError, OSError) as exc:
log.warning("Failed to parse %s: %s", path, exc)
return {}
Expand Down Expand Up @@ -125,7 +125,7 @@ def save_commands(project_dir: str, commands: dict) -> None:
"""Save project commands to .cce/commands.yaml."""
path = _commands_path(project_dir)
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(yaml.dump(commands, default_flow_style=False, sort_keys=False))
path.write_text(yaml.dump(commands, default_flow_style=False, sort_keys=False), encoding="utf-8")


def add_command(project_dir: str, hook: str, command: str) -> None:
Expand Down Expand Up @@ -244,7 +244,7 @@ def remove_preference(project_dir: str, key: str) -> bool:
def ensure_gitignore(project_dir: str) -> None:
"""Add CCE-related entries to .gitignore if not already present."""
gitignore = Path(project_dir) / ".gitignore"
content = gitignore.read_text() if gitignore.exists() else ""
content = gitignore.read_text(encoding="utf-8") if gitignore.exists() else ""

additions = []
for entry, comment in _GITIGNORE_ENTRIES:
Expand All @@ -255,7 +255,7 @@ def ensure_gitignore(project_dir: str) -> None:
return

block = "\n\n# CCE (code-context-engine)\n" + "\n".join(additions) + "\n"
gitignore.write_text(content.rstrip() + block)
gitignore.write_text(content.rstrip() + block, encoding="utf-8")


def format_for_prompt(commands: dict, label: str = "Project") -> str:
Expand Down
8 changes: 4 additions & 4 deletions src/context_engine/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,13 @@ def _pid_dir() -> Path:
def _read_pid(name: str) -> int | None:
p = _pid_dir() / f"{name}.pid"
try:
return int(p.read_text().strip())
return int(p.read_text(encoding="utf-8").strip())
except (FileNotFoundError, ValueError):
return None


def _write_pid(name: str, pid: int) -> None:
(_pid_dir() / f"{name}.pid").write_text(str(pid))
(_pid_dir() / f"{name}.pid").write_text(str(pid), encoding="utf-8")


def _remove_pid(name: str) -> None:
Expand Down Expand Up @@ -165,7 +165,7 @@ def _is_remote_url(url: str) -> bool:
def get_dashboard_status() -> dict:
port_file = _pid_dir() / "dashboard.port"
try:
port = int(port_file.read_text().strip())
port = int(port_file.read_text(encoding="utf-8").strip())
except (FileNotFoundError, ValueError):
port = None

Expand Down Expand Up @@ -266,7 +266,7 @@ def start_dashboard(port: int = _DASHBOARD_DEFAULT_PORT) -> tuple[bool, str]:
start_new_session=True,
)
_write_pid("dashboard", proc.pid)
(_pid_dir() / "dashboard.port").write_text(str(port))
(_pid_dir() / "dashboard.port").write_text(str(port), encoding="utf-8")
return True, f"Dashboard started at http://localhost:{port} (PID {proc.pid})"
except Exception as exc:
return False, f"Failed to start dashboard: {exc}"
Expand Down
Loading