codecortex 0.8.1__tar.gz → 0.8.2__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.

Potentially problematic release.


This version of codecortex might be problematic. Click here for more details.

Files changed (68) hide show
  1. {codecortex-0.8.1/src/codecortex.egg-info → codecortex-0.8.2}/PKG-INFO +1 -1
  2. {codecortex-0.8.1 → codecortex-0.8.2/src/codecortex.egg-info}/PKG-INFO +1 -1
  3. {codecortex-0.8.1 → codecortex-0.8.2}/src/codecortex.egg-info/SOURCES.txt +1 -0
  4. codecortex-0.8.2/src/codeintel/__init__.py +1 -0
  5. codecortex-0.8.2/src/codeintel/installer.py +120 -0
  6. {codecortex-0.8.1 → codecortex-0.8.2}/src/codeintel/providers/graph.py +8 -0
  7. {codecortex-0.8.1 → codecortex-0.8.2}/tests/test_graph_provider.py +13 -0
  8. codecortex-0.8.2/tests/test_installer.py +97 -0
  9. codecortex-0.8.1/src/codeintel/__init__.py +0 -1
  10. codecortex-0.8.1/src/codeintel/installer.py +0 -113
  11. {codecortex-0.8.1 → codecortex-0.8.2}/LICENSE +0 -0
  12. {codecortex-0.8.1 → codecortex-0.8.2}/README.md +0 -0
  13. {codecortex-0.8.1 → codecortex-0.8.2}/pyproject.toml +0 -0
  14. {codecortex-0.8.1 → codecortex-0.8.2}/setup.cfg +0 -0
  15. {codecortex-0.8.1 → codecortex-0.8.2}/src/codecortex.egg-info/dependency_links.txt +0 -0
  16. {codecortex-0.8.1 → codecortex-0.8.2}/src/codecortex.egg-info/entry_points.txt +0 -0
  17. {codecortex-0.8.1 → codecortex-0.8.2}/src/codecortex.egg-info/requires.txt +0 -0
  18. {codecortex-0.8.1 → codecortex-0.8.2}/src/codecortex.egg-info/top_level.txt +0 -0
  19. {codecortex-0.8.1 → codecortex-0.8.2}/src/codeintel/__main__.py +0 -0
  20. {codecortex-0.8.1 → codecortex-0.8.2}/src/codeintel/auth.py +0 -0
  21. {codecortex-0.8.1 → codecortex-0.8.2}/src/codeintel/cache.py +0 -0
  22. {codecortex-0.8.1 → codecortex-0.8.2}/src/codeintel/config.py +0 -0
  23. {codecortex-0.8.1 → codecortex-0.8.2}/src/codeintel/doctor.py +0 -0
  24. {codecortex-0.8.1 → codecortex-0.8.2}/src/codeintel/gateway.py +0 -0
  25. {codecortex-0.8.1 → codecortex-0.8.2}/src/codeintel/http_server.py +0 -0
  26. {codecortex-0.8.1 → codecortex-0.8.2}/src/codeintel/indexer.py +0 -0
  27. {codecortex-0.8.1 → codecortex-0.8.2}/src/codeintel/injector.py +0 -0
  28. {codecortex-0.8.1 → codecortex-0.8.2}/src/codeintel/logconfig.py +0 -0
  29. {codecortex-0.8.1 → codecortex-0.8.2}/src/codeintel/mapper.py +0 -0
  30. {codecortex-0.8.1 → codecortex-0.8.2}/src/codeintel/metrics.py +0 -0
  31. {codecortex-0.8.1 → codecortex-0.8.2}/src/codeintel/onboarding.py +0 -0
  32. {codecortex-0.8.1 → codecortex-0.8.2}/src/codeintel/policy.py +0 -0
  33. {codecortex-0.8.1 → codecortex-0.8.2}/src/codeintel/provider.py +0 -0
  34. {codecortex-0.8.1 → codecortex-0.8.2}/src/codeintel/providers/__init__.py +0 -0
  35. {codecortex-0.8.1 → codecortex-0.8.2}/src/codeintel/providers/lsp.py +0 -0
  36. {codecortex-0.8.1 → codecortex-0.8.2}/src/codeintel/providers/none.py +0 -0
  37. {codecortex-0.8.1 → codecortex-0.8.2}/src/codeintel/providers/semantic.py +0 -0
  38. {codecortex-0.8.1 → codecortex-0.8.2}/src/codeintel/reindexer.py +0 -0
  39. {codecortex-0.8.1 → codecortex-0.8.2}/src/codeintel/reset.py +0 -0
  40. {codecortex-0.8.1 → codecortex-0.8.2}/src/codeintel/searcher.py +0 -0
  41. {codecortex-0.8.1 → codecortex-0.8.2}/src/codeintel/semantic_db.py +0 -0
  42. {codecortex-0.8.1 → codecortex-0.8.2}/src/codeintel/server.py +0 -0
  43. {codecortex-0.8.1 → codecortex-0.8.2}/src/codeintel/term.py +0 -0
  44. {codecortex-0.8.1 → codecortex-0.8.2}/tests/test_cache.py +0 -0
  45. {codecortex-0.8.1 → codecortex-0.8.2}/tests/test_chunking.py +0 -0
  46. {codecortex-0.8.1 → codecortex-0.8.2}/tests/test_config.py +0 -0
  47. {codecortex-0.8.1 → codecortex-0.8.2}/tests/test_doctor.py +0 -0
  48. {codecortex-0.8.1 → codecortex-0.8.2}/tests/test_e2e.py +0 -0
  49. {codecortex-0.8.1 → codecortex-0.8.2}/tests/test_enterprise.py +0 -0
  50. {codecortex-0.8.1 → codecortex-0.8.2}/tests/test_gateway.py +0 -0
  51. {codecortex-0.8.1 → codecortex-0.8.2}/tests/test_graph_real.py +0 -0
  52. {codecortex-0.8.1 → codecortex-0.8.2}/tests/test_graph_stdin.py +0 -0
  53. {codecortex-0.8.1 → codecortex-0.8.2}/tests/test_hardening.py +0 -0
  54. {codecortex-0.8.1 → codecortex-0.8.2}/tests/test_http_auth.py +0 -0
  55. {codecortex-0.8.1 → codecortex-0.8.2}/tests/test_http_server.py +0 -0
  56. {codecortex-0.8.1 → codecortex-0.8.2}/tests/test_integration.py +0 -0
  57. {codecortex-0.8.1 → codecortex-0.8.2}/tests/test_lsp_provider.py +0 -0
  58. {codecortex-0.8.1 → codecortex-0.8.2}/tests/test_lsp_real.py +0 -0
  59. {codecortex-0.8.1 → codecortex-0.8.2}/tests/test_mapper.py +0 -0
  60. {codecortex-0.8.1 → codecortex-0.8.2}/tests/test_never_raise.py +0 -0
  61. {codecortex-0.8.1 → codecortex-0.8.2}/tests/test_onboarding.py +0 -0
  62. {codecortex-0.8.1 → codecortex-0.8.2}/tests/test_rbac.py +0 -0
  63. {codecortex-0.8.1 → codecortex-0.8.2}/tests/test_reindexer.py +0 -0
  64. {codecortex-0.8.1 → codecortex-0.8.2}/tests/test_rerank.py +0 -0
  65. {codecortex-0.8.1 → codecortex-0.8.2}/tests/test_reset.py +0 -0
  66. {codecortex-0.8.1 → codecortex-0.8.2}/tests/test_semantic_provider.py +0 -0
  67. {codecortex-0.8.1 → codecortex-0.8.2}/tests/test_term.py +0 -0
  68. {codecortex-0.8.1 → codecortex-0.8.2}/tests/test_treesitter.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codecortex
3
- Version: 0.8.1
3
+ Version: 0.8.2
4
4
  Summary: Local-first, MCP-native code-intelligence server — graph, LSP, and semantic search behind one safe code.query tool for coding agents.
5
5
  Author: Shammai Hamilton
6
6
  License-Expression: MIT
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codecortex
3
- Version: 0.8.1
3
+ Version: 0.8.2
4
4
  Summary: Local-first, MCP-native code-intelligence server — graph, LSP, and semantic search behind one safe code.query tool for coding agents.
5
5
  Author: Shammai Hamilton
6
6
  License-Expression: MIT
@@ -48,6 +48,7 @@ tests/test_graph_stdin.py
48
48
  tests/test_hardening.py
49
49
  tests/test_http_auth.py
50
50
  tests/test_http_server.py
51
+ tests/test_installer.py
51
52
  tests/test_integration.py
52
53
  tests/test_lsp_provider.py
53
54
  tests/test_lsp_real.py
@@ -0,0 +1 @@
1
+ __version__ = "0.8.2"
@@ -0,0 +1,120 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ import pathlib
6
+
7
+ _AGENTS = ["claude", "codex", "gemini", "zed"]
8
+
9
+
10
+ def _atomic_write_text(path: pathlib.Path, text: str) -> None:
11
+ """Write via a sibling temp file + os.replace so an interrupted write can never truncate
12
+ the user's existing agent config (which holds unrelated settings) to a partial/empty file.
13
+ The temp lives in the same directory as ``path`` so the replace stays on one filesystem."""
14
+ tmp = path.with_name(path.name + ".codeintel.tmp")
15
+ tmp.write_text(text, encoding="utf-8")
16
+ os.replace(tmp, path)
17
+
18
+
19
+ # Per-agent registration recipe. Most agents take a Claude-style JSON block; Codex is different —
20
+ # its CLI reads MCP servers from ``~/.codex/config.toml`` as ``[mcp_servers.<name>]`` TOML tables,
21
+ # NOT a JSON ``mcpServers`` map in config.json (writing the latter registers nothing).
22
+ _CONFIG: dict[str, dict] = {
23
+ "claude": {
24
+ "format": "json",
25
+ "path": "~/.claude/settings.json",
26
+ "key": ["mcpServers", "codeintel"],
27
+ "value": {"command": "codeintel", "args": ["serve"]},
28
+ },
29
+ "codex": {
30
+ "format": "toml-mcp",
31
+ "path": "~/.codex/config.toml",
32
+ "table": "[mcp_servers.codeintel]",
33
+ "block": '[mcp_servers.codeintel]\ncommand = "codeintel"\nargs = ["serve"]\n',
34
+ },
35
+ "gemini": {
36
+ "format": "json",
37
+ "path": "~/.gemini/settings.json",
38
+ "key": ["mcpServers", "codeintel"],
39
+ "value": {"command": "codeintel", "args": ["serve"]},
40
+ },
41
+ "zed": {
42
+ "format": "json",
43
+ "path": "~/.config/zed/settings.json",
44
+ "key": ["context_servers", "codeintel"],
45
+ "value": {"command": {"path": "codeintel", "args": ["serve"]}},
46
+ },
47
+ }
48
+
49
+
50
+ def _get_nested(data: dict, keys: list[str]):
51
+ node = data
52
+ for k in keys:
53
+ if not isinstance(node, dict) or k not in node:
54
+ return None
55
+ node = node[k]
56
+ return node
57
+
58
+
59
+ def _set_nested(data: dict, keys: list[str], value) -> None:
60
+ node = data
61
+ for k in keys[:-1]:
62
+ if k not in node or not isinstance(node[k], dict):
63
+ node[k] = {}
64
+ node = node[k]
65
+ node[keys[-1]] = value
66
+
67
+
68
+ class Installer:
69
+ def register(self, agent: str) -> dict:
70
+ spec = _CONFIG.get(agent)
71
+ if spec is None:
72
+ return self._result(agent, "", False, "failed", f"unknown agent '{agent}'")
73
+ config_path = pathlib.Path(spec["path"]).expanduser()
74
+ try:
75
+ if spec.get("format") == "toml-mcp":
76
+ return self._register_toml(agent, config_path, spec)
77
+ return self._register_json(agent, config_path, spec)
78
+ except Exception as exc:
79
+ return self._result(agent, str(config_path), False, "failed", str(exc))
80
+
81
+ def _register_json(self, agent: str, config_path: pathlib.Path, spec: dict) -> dict:
82
+ if config_path.exists():
83
+ data = json.loads(config_path.read_text(encoding="utf-8"))
84
+ if not isinstance(data, dict):
85
+ data = {}
86
+ else:
87
+ data = {}
88
+
89
+ if _get_nested(data, spec["key"]) == spec["value"]:
90
+ return self._result(agent, str(config_path), True, "already")
91
+
92
+ config_path.parent.mkdir(parents=True, exist_ok=True)
93
+ _set_nested(data, spec["key"], spec["value"])
94
+ _atomic_write_text(config_path, json.dumps(data, indent=2))
95
+ return self._result(agent, str(config_path), True, "registered")
96
+
97
+ def _register_toml(self, agent: str, config_path: pathlib.Path, spec: dict) -> dict:
98
+ """Codex: append an ``[mcp_servers.codeintel]`` table to config.toml, preserving everything
99
+ already there (other servers, project trust levels, hooks). Text-based on purpose — it must
100
+ not reformat or risk corrupting a config the user hand-edits, and it stays idempotent by
101
+ checking for the table header. If a codeintel entry already exists it is left untouched."""
102
+ existing = config_path.read_text(encoding="utf-8") if config_path.exists() else ""
103
+ if spec["table"] in existing:
104
+ return self._result(agent, str(config_path), True, "already")
105
+
106
+ config_path.parent.mkdir(parents=True, exist_ok=True)
107
+ if existing == "":
108
+ new_text = spec["block"]
109
+ else:
110
+ prefix = existing if existing.endswith("\n") else existing + "\n"
111
+ new_text = prefix + "\n" + spec["block"] # blank line before the new table
112
+ _atomic_write_text(config_path, new_text)
113
+ return self._result(agent, str(config_path), True, "registered")
114
+
115
+ @staticmethod
116
+ def _result(agent: str, path: str, ok: bool, action: str, reason: str = "") -> dict:
117
+ return {"agent": agent, "path": path, "ok": ok, "action": action, "reason": reason}
118
+
119
+ def register_all(self) -> list[dict]:
120
+ return [self.register(agent) for agent in _AGENTS]
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import json
4
+ import os
4
5
  import shutil
5
6
  import subprocess
6
7
  import threading
@@ -107,6 +108,13 @@ class GraphProvider:
107
108
  older/mocked shape — accept both. Prefer an exact ``root_path`` match; otherwise the
108
109
  LONGEST prefix match (so ``.../project/codeintel`` resolves to codeintel, not its
109
110
  parent ``.../project``). Pure + static so ``_resolve_project`` and ``probe`` share it."""
111
+ # Normalize the input to an absolute realpath: the backend stores absolute root_paths, so a
112
+ # relative ``project_root`` (e.g. `codeintel map .` passing ".") would otherwise never match
113
+ # — the bug where the map/query silently reported "not indexed" from inside the repo.
114
+ try:
115
+ project_root = os.path.realpath(project_root)
116
+ except Exception:
117
+ pass
110
118
  entries = raw.get("projects", []) if isinstance(raw, dict) else raw
111
119
  if not isinstance(entries, list):
112
120
  return None
@@ -1,12 +1,25 @@
1
1
  """GraphProvider tests: never-raise invariant and key behavioral guarantees."""
2
2
  from __future__ import annotations
3
3
 
4
+ import os
4
5
  import subprocess
5
6
 
6
7
  from codeintel.providers.graph import GraphProvider
7
8
  from codeintel.server import code_status_handler
8
9
 
9
10
 
11
+ def test_match_project_resolves_a_relative_path(tmp_path, monkeypatch):
12
+ # The backend stores absolute root_paths, so a relative project_root (e.g. `codeintel map .`)
13
+ # must be normalized before matching — otherwise resolution fails from inside the repo and the
14
+ # map/graph query silently reports "not indexed" (the map-stub bug).
15
+ real = os.path.realpath(str(tmp_path))
16
+ raw = {"projects": [{"name": "myrepo", "root_path": real}]}
17
+ monkeypatch.chdir(tmp_path)
18
+ assert GraphProvider._match_project(raw, ".") == "myrepo" # relative resolves now
19
+ assert GraphProvider._match_project(raw, real) == "myrepo" # absolute still works
20
+ assert GraphProvider._match_project(raw, os.path.join(real, "src")) == "myrepo" # subdir prefix
21
+
22
+
10
23
  # ---------------------------------------------------------------------------
11
24
  # Group 1 — Never-raise: None args
12
25
  # ---------------------------------------------------------------------------
@@ -0,0 +1,97 @@
1
+ """Installer / agent-registration tests (added 0.8.2).
2
+
3
+ The installer had no tests, which is why `--agent codex` shipped writing a Claude-style JSON
4
+ `mcpServers` block to `~/.codex/config.json` — but Codex CLI reads MCP servers from
5
+ `~/.codex/config.toml` as `[mcp_servers.<name>]` TOML, so nothing was actually registered.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ from pathlib import Path
11
+
12
+ from codeintel.installer import Installer
13
+
14
+
15
+ def _codex_toml(home: Path) -> Path:
16
+ return home / ".codex" / "config.toml"
17
+
18
+
19
+ # --------------------------------------------------------------------------- codex (TOML)
20
+
21
+ def test_codex_registers_as_toml_in_config_toml(tmp_path, monkeypatch):
22
+ monkeypatch.setenv("HOME", str(tmp_path))
23
+ cfg = _codex_toml(tmp_path)
24
+ cfg.parent.mkdir(parents=True)
25
+ cfg.write_text('[mcp_servers.serena]\ncommand = "serena"\n') # pre-existing MCP server
26
+
27
+ res = Installer().register("codex")
28
+
29
+ assert res["ok"] and res["action"] == "registered"
30
+ text = cfg.read_text()
31
+ assert "[mcp_servers.codeintel]" in text # written to config.toml as TOML
32
+ assert 'command = "codeintel"' in text and 'args = ["serve"]' in text
33
+ assert "[mcp_servers.serena]" in text # existing content preserved
34
+
35
+
36
+ def test_codex_does_not_write_the_wrong_json_file(tmp_path, monkeypatch):
37
+ # regression: the old installer wrote ~/.codex/config.json (mcpServers), which Codex ignores
38
+ monkeypatch.setenv("HOME", str(tmp_path))
39
+ Installer().register("codex")
40
+ assert _codex_toml(tmp_path).exists()
41
+ assert not (tmp_path / ".codex" / "config.json").exists()
42
+
43
+
44
+ def test_codex_creates_config_when_missing(tmp_path, monkeypatch):
45
+ monkeypatch.setenv("HOME", str(tmp_path))
46
+ res = Installer().register("codex")
47
+ assert res["ok"] and _codex_toml(tmp_path).read_text().startswith("[mcp_servers.codeintel]")
48
+
49
+
50
+ def test_codex_is_idempotent(tmp_path, monkeypatch):
51
+ monkeypatch.setenv("HOME", str(tmp_path))
52
+ assert Installer().register("codex")["action"] == "registered"
53
+ assert Installer().register("codex")["action"] == "already"
54
+ assert _codex_toml(tmp_path).read_text().count("[mcp_servers.codeintel]") == 1 # not duplicated
55
+
56
+
57
+ # --------------------------------------------------------------------------- json agents
58
+
59
+ def test_claude_registers_json_mcp_servers(tmp_path, monkeypatch):
60
+ monkeypatch.setenv("HOME", str(tmp_path))
61
+ res = Installer().register("claude")
62
+ assert res["ok"]
63
+ data = json.loads((tmp_path / ".claude" / "settings.json").read_text())
64
+ assert data["mcpServers"]["codeintel"] == {"command": "codeintel", "args": ["serve"]}
65
+
66
+
67
+ def test_claude_preserves_unrelated_settings(tmp_path, monkeypatch):
68
+ monkeypatch.setenv("HOME", str(tmp_path))
69
+ s = tmp_path / ".claude" / "settings.json"
70
+ s.parent.mkdir(parents=True)
71
+ s.write_text(json.dumps({"theme": "dark", "mcpServers": {"other": {"command": "x"}}}))
72
+ Installer().register("claude")
73
+ data = json.loads(s.read_text())
74
+ assert data["theme"] == "dark" # unrelated key preserved
75
+ assert "other" in data["mcpServers"] # other server preserved
76
+ assert "codeintel" in data["mcpServers"] # ours added
77
+
78
+
79
+ def test_zed_uses_context_servers_key(tmp_path, monkeypatch):
80
+ monkeypatch.setenv("HOME", str(tmp_path))
81
+ Installer().register("zed")
82
+ data = json.loads((tmp_path / ".config" / "zed" / "settings.json").read_text())
83
+ assert "codeintel" in data["context_servers"]
84
+
85
+
86
+ # --------------------------------------------------------------------------- misc
87
+
88
+ def test_unknown_agent_fails_safely():
89
+ res = Installer().register("nonesuch")
90
+ assert res["ok"] is False and res["action"] == "failed" and "unknown" in res["reason"]
91
+
92
+
93
+ def test_register_all_covers_every_agent(tmp_path, monkeypatch):
94
+ monkeypatch.setenv("HOME", str(tmp_path))
95
+ results = Installer().register_all()
96
+ assert {r["agent"] for r in results} == {"claude", "codex", "gemini", "zed"}
97
+ assert all(r["ok"] for r in results)
@@ -1 +0,0 @@
1
- __version__ = "0.8.1"
@@ -1,113 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import json
4
- import os
5
- import pathlib
6
-
7
- _AGENTS = ["claude", "codex", "gemini", "zed"]
8
-
9
-
10
- def _atomic_write_text(path: pathlib.Path, text: str) -> None:
11
- """Write via a sibling temp file + os.replace so an interrupted write can never truncate
12
- the user's existing agent config (which holds unrelated settings) to a partial/empty file.
13
- The temp lives in the same directory as ``path`` so the replace stays on one filesystem."""
14
- tmp = path.with_name(path.name + ".codeintel.tmp")
15
- tmp.write_text(text, encoding="utf-8")
16
- os.replace(tmp, path)
17
-
18
- _CONFIG: dict[str, dict] = {
19
- "claude": {
20
- "path": "~/.claude/settings.json",
21
- "key": ["mcpServers", "codeintel"],
22
- "value": {"command": "codeintel", "args": ["serve"]},
23
- },
24
- "codex": {
25
- "path": "~/.codex/config.json",
26
- "key": ["mcpServers", "codeintel"],
27
- "value": {"command": "codeintel", "args": ["serve"]},
28
- },
29
- "gemini": {
30
- "path": "~/.gemini/settings.json",
31
- "key": ["mcpServers", "codeintel"],
32
- "value": {"command": "codeintel", "args": ["serve"]},
33
- },
34
- "zed": {
35
- "path": "~/.config/zed/settings.json",
36
- "key": ["context_servers", "codeintel"],
37
- "value": {"command": {"path": "codeintel", "args": ["serve"]}},
38
- },
39
- }
40
-
41
-
42
- def _get_nested(data: dict, keys: list[str]):
43
- node = data
44
- for k in keys:
45
- if not isinstance(node, dict) or k not in node:
46
- return None
47
- node = node[k]
48
- return node
49
-
50
-
51
- def _set_nested(data: dict, keys: list[str], value) -> None:
52
- node = data
53
- for k in keys[:-1]:
54
- if k not in node or not isinstance(node[k], dict):
55
- node[k] = {}
56
- node = node[k]
57
- node[keys[-1]] = value
58
-
59
-
60
- class Installer:
61
- def register(self, agent: str) -> dict:
62
- spec = _CONFIG.get(agent)
63
- if spec is None:
64
- return {
65
- "agent": agent,
66
- "path": "",
67
- "ok": False,
68
- "action": "failed",
69
- "reason": f"unknown agent '{agent}'",
70
- }
71
-
72
- config_path = pathlib.Path(spec["path"]).expanduser()
73
- try:
74
- if config_path.exists():
75
- data = json.loads(config_path.read_text(encoding="utf-8"))
76
- if not isinstance(data, dict):
77
- data = {}
78
- else:
79
- data = {}
80
-
81
- current = _get_nested(data, spec["key"])
82
- if current == spec["value"]:
83
- return {
84
- "agent": agent,
85
- "path": str(config_path),
86
- "ok": True,
87
- "action": "already",
88
- "reason": "",
89
- }
90
-
91
- config_path.parent.mkdir(parents=True, exist_ok=True)
92
- _set_nested(data, spec["key"], spec["value"])
93
- _atomic_write_text(config_path, json.dumps(data, indent=2))
94
-
95
- return {
96
- "agent": agent,
97
- "path": str(config_path),
98
- "ok": True,
99
- "action": "registered",
100
- "reason": "",
101
- }
102
-
103
- except Exception as exc:
104
- return {
105
- "agent": agent,
106
- "path": str(config_path),
107
- "ok": False,
108
- "action": "failed",
109
- "reason": str(exc),
110
- }
111
-
112
- def register_all(self) -> list[dict]:
113
- return [self.register(agent) for agent in _AGENTS]
File without changes
File without changes
File without changes
File without changes
File without changes