arcgraph 0.1.0__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.
- arcgraph/__init__.py +41 -0
- arcgraph/_build_provenance.json +10 -0
- arcgraph/adapters/__init__.py +6 -0
- arcgraph/adapters/arq_adapter.py +369 -0
- arcgraph/adapters/celery_adapter.py +440 -0
- arcgraph/adapters/common.py +436 -0
- arcgraph/adapters/django_adapter.py +765 -0
- arcgraph/adapters/fastapi_adapter.py +415 -0
- arcgraph/adapters/logging_adapter.py +335 -0
- arcgraph/adapters/mcp_adapter.py +100 -0
- arcgraph/adapters/pydantic_adapter.py +290 -0
- arcgraph/adapters/pytest_adapter.py +221 -0
- arcgraph/adapters/pytest_collection.py +96 -0
- arcgraph/adapters/pytest_fixtures.py +371 -0
- arcgraph/adapters/pytest_types.py +251 -0
- arcgraph/adapters/registry.py +34 -0
- arcgraph/adapters/service_container_adapter.py +548 -0
- arcgraph/adapters/sqlalchemy_adapter.py +446 -0
- arcgraph/adapters/typer_click_adapter.py +292 -0
- arcgraph/analyzers/__init__.py +1 -0
- arcgraph/analyzers/bindings.py +782 -0
- arcgraph/analyzers/calls/__init__.py +6 -0
- arcgraph/analyzers/calls/analyzer.py +2921 -0
- arcgraph/analyzers/calls/constants.py +702 -0
- arcgraph/analyzers/calls/context.py +298 -0
- arcgraph/analyzers/calls/lexical.py +237 -0
- arcgraph/analyzers/coverage.py +826 -0
- arcgraph/analyzers/exports.py +46 -0
- arcgraph/analyzers/har_trace.py +323 -0
- arcgraph/analyzers/imports.py +190 -0
- arcgraph/analyzers/otel_trace.py +352 -0
- arcgraph/analyzers/precise_references.py +1199 -0
- arcgraph/analyzers/python_scopes.py +56 -0
- arcgraph/analyzers/runtime_trace.py +516 -0
- arcgraph/analyzers/similarity.py +430 -0
- arcgraph/analyzers/symbols.py +363 -0
- arcgraph/analyzers/type_unions.py +89 -0
- arcgraph/analyzers/types.py +2293 -0
- arcgraph/assets/__init__.py +1 -0
- arcgraph/assets/workbench/__init__.py +1 -0
- arcgraph/assets/workbench/index.html +2398 -0
- arcgraph/assets/workbench/vendor/LICENSE.d3.txt +15 -0
- arcgraph/assets/workbench/vendor/d3.v7.min.js +2 -0
- arcgraph/change/__init__.py +10 -0
- arcgraph/change/baseline.py +145 -0
- arcgraph/change/changed_regions.py +728 -0
- arcgraph/change/contracts.py +927 -0
- arcgraph/change/errors.py +88 -0
- arcgraph/change/evidence.py +289 -0
- arcgraph/change/graph_delta.py +1422 -0
- arcgraph/change/identities.py +1277 -0
- arcgraph/change/paths.py +234 -0
- arcgraph/change/pins.py +426 -0
- arcgraph/change/planner.py +504 -0
- arcgraph/change/protected_surfaces.py +328 -0
- arcgraph/change/provider.py +46 -0
- arcgraph/change/service.py +1030 -0
- arcgraph/change/source_provider.py +311 -0
- arcgraph/change/store.py +1138 -0
- arcgraph/change/trusted_runners.py +154 -0
- arcgraph/change/verdicts.py +141 -0
- arcgraph/change/verification_plan.py +113 -0
- arcgraph/change/verifier.py +579 -0
- arcgraph/core/__init__.py +1 -0
- arcgraph/core/assurance.py +343 -0
- arcgraph/core/binding_metrics.py +75 -0
- arcgraph/core/cleanup.py +701 -0
- arcgraph/core/confidence_profiles.py +108 -0
- arcgraph/core/evidence_manifest.py +862 -0
- arcgraph/core/expression_kinds.py +11 -0
- arcgraph/core/force_graph_export.py +1125 -0
- arcgraph/core/freshness.py +94 -0
- arcgraph/core/graph_store.py +1099 -0
- arcgraph/core/ids.py +206 -0
- arcgraph/core/language_tiers.py +150 -0
- arcgraph/core/merge.py +649 -0
- arcgraph/core/metadata.py +30 -0
- arcgraph/core/operation_lock.py +218 -0
- arcgraph/core/passes.py +142 -0
- arcgraph/core/payload_policy.py +708 -0
- arcgraph/core/precise_references.py +124 -0
- arcgraph/core/query_engine.py +3078 -0
- arcgraph/core/recovery.py +18 -0
- arcgraph/core/scanner.py +1454 -0
- arcgraph/core/schemas.py +700 -0
- arcgraph/core/semantic.py +263 -0
- arcgraph/core/semantic_metrics.py +619 -0
- arcgraph/core/sharing_retry.py +84 -0
- arcgraph/core/structural.py +260 -0
- arcgraph/core/target_resolver.py +486 -0
- arcgraph/core/type_metrics.py +67 -0
- arcgraph/core/unresolved_classification.py +471 -0
- arcgraph/core/utils.py +100 -0
- arcgraph/core/visual_contract.py +240 -0
- arcgraph/core/visual_slice_engine.py +933 -0
- arcgraph/interfaces/__init__.py +1 -0
- arcgraph/interfaces/agent_capabilities.py +493 -0
- arcgraph/interfaces/agent_help.py +313 -0
- arcgraph/interfaces/benchmark.py +548 -0
- arcgraph/interfaces/ci.py +1992 -0
- arcgraph/interfaces/cli.py +2799 -0
- arcgraph/interfaces/cli_change.py +690 -0
- arcgraph/interfaces/cli_feedback.py +176 -0
- arcgraph/interfaces/cli_ops.py +69 -0
- arcgraph/interfaces/cli_support.py +113 -0
- arcgraph/interfaces/cli_trial.py +42 -0
- arcgraph/interfaces/cli_visual.py +601 -0
- arcgraph/interfaces/cli_workspace.py +300 -0
- arcgraph/interfaces/client_setup.py +481 -0
- arcgraph/interfaces/docs.py +1255 -0
- arcgraph/interfaces/human_output.py +409 -0
- arcgraph/interfaces/local_state.py +577 -0
- arcgraph/interfaces/mcp_server.py +457 -0
- arcgraph/interfaces/mcp_tools.py +1209 -0
- arcgraph/interfaces/metrics.py +571 -0
- arcgraph/interfaces/ops.py +330 -0
- arcgraph/interfaces/precision.py +1670 -0
- arcgraph/interfaces/project_probe.py +75 -0
- arcgraph/interfaces/reports.py +1121 -0
- arcgraph/interfaces/stdio_encoding.py +29 -0
- arcgraph/interfaces/trace.py +592 -0
- arcgraph/interfaces/trial_feedback.py +519 -0
- arcgraph/interfaces/trial_setup.py +535 -0
- arcgraph/interfaces/visual_server.py +354 -0
- arcgraph/interfaces/visual_slice.py +168 -0
- arcgraph/interfaces/visual_smoke.py +666 -0
- arcgraph/interfaces/workbench.py +576 -0
- arcgraph/interfaces/workspace.py +1513 -0
- arcgraph/pipeline/__init__.py +1 -0
- arcgraph/pipeline/contracts.py +122 -0
- arcgraph/pipeline/external_frontend.py +451 -0
- arcgraph/pipeline/external_l3_frontends.py +368 -0
- arcgraph/pipeline/external_protocol.py +358 -0
- arcgraph/pipeline/frontends.py +437 -0
- arcgraph/pipeline/indexer.py +661 -0
- arcgraph/pipeline/openapi_frontend.py +708 -0
- arcgraph/pipeline/python_frontend.py +298 -0
- arcgraph/pipeline/reindexer.py +1333 -0
- arcgraph/pipeline/scip_frontend.py +726 -0
- arcgraph/pipeline/typescript_extractor/declarations.mjs +183 -0
- arcgraph/pipeline/typescript_extractor/edges.mjs +2794 -0
- arcgraph/pipeline/typescript_extractor/graph_primitives.mjs +189 -0
- arcgraph/pipeline/typescript_extractor/module_resolution.mjs +877 -0
- arcgraph/pipeline/typescript_extractor/runtime.mjs +56 -0
- arcgraph/pipeline/typescript_extractor/similarity.mjs +619 -0
- arcgraph/pipeline/typescript_extractor/similarity_runner.mjs +19 -0
- arcgraph/pipeline/typescript_extractor/sources.mjs +23 -0
- arcgraph/pipeline/typescript_extractor.mjs +177 -0
- arcgraph/pipeline/typescript_frameworks.py +947 -0
- arcgraph/pipeline/typescript_frontend.py +656 -0
- arcgraph/pipeline/typescript_references.mjs +167 -0
- arcgraph/providers/__init__.py +27 -0
- arcgraph/providers/context_provider.py +1258 -0
- arcgraph/providers/flow_payload.py +117 -0
- arcgraph/providers/memory_connector.py +216 -0
- arcgraph/providers/orchestrator.py +475 -0
- arcgraph/providers/relationship_payload.py +102 -0
- arcgraph/providers/risk_provider.py +843 -0
- arcgraph/providers/why_provider.py +349 -0
- arcgraph/version_info.py +246 -0
- arcgraph-0.1.0.dist-info/METADATA +676 -0
- arcgraph-0.1.0.dist-info/RECORD +165 -0
- arcgraph-0.1.0.dist-info/WHEEL +4 -0
- arcgraph-0.1.0.dist-info/entry_points.txt +2 -0
- arcgraph-0.1.0.dist-info/licenses/LICENSE +203 -0
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
"""Explicit multi-client onboarding. No client approval or model call is implied."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import hashlib
|
|
7
|
+
import json
|
|
8
|
+
import math
|
|
9
|
+
import os
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
import shlex
|
|
12
|
+
import stat
|
|
13
|
+
import sys
|
|
14
|
+
import subprocess
|
|
15
|
+
import tempfile
|
|
16
|
+
import tomllib
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
import yaml
|
|
20
|
+
|
|
21
|
+
from arcgraph.interfaces.trial_setup import _arcgraph_executable_selection
|
|
22
|
+
|
|
23
|
+
CLIENTS = ("claude", "codex", "cursor", "hermes", "pi")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def add_setup_parser(subparsers: Any) -> None:
|
|
27
|
+
parser = subparsers.add_parser(
|
|
28
|
+
"setup",
|
|
29
|
+
help="Build/refresh an index and connect a client, or preview with --dry-run.",
|
|
30
|
+
)
|
|
31
|
+
parser.add_argument("--client", choices=CLIENTS, required=True)
|
|
32
|
+
parser.add_argument(
|
|
33
|
+
"--dry-run",
|
|
34
|
+
action="store_true",
|
|
35
|
+
help="Inspect and render a plan without writes, indexing or subprocesses.",
|
|
36
|
+
)
|
|
37
|
+
parser.add_argument(
|
|
38
|
+
"--client-config",
|
|
39
|
+
type=Path,
|
|
40
|
+
help="Explicit client config path (Pi: SKILL.md); useful for isolated profiles.",
|
|
41
|
+
)
|
|
42
|
+
parser.add_argument(
|
|
43
|
+
"--timeout",
|
|
44
|
+
type=float,
|
|
45
|
+
default=300,
|
|
46
|
+
help="Positive per-step timeout in seconds.",
|
|
47
|
+
)
|
|
48
|
+
parser.set_defaults(handler=handle_setup)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# An NTFS junction redirects a directory like a symlink, but is_symlink() is
|
|
52
|
+
# False for it. Other reparse points, such as cloud-file placeholders, are
|
|
53
|
+
# ordinary directories and stay allowed.
|
|
54
|
+
_IO_REPARSE_TAG_MOUNT_POINT = 0xA0000003
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _is_link(path: Path) -> bool:
|
|
58
|
+
if path.is_symlink():
|
|
59
|
+
return True
|
|
60
|
+
try:
|
|
61
|
+
tag = getattr(os.lstat(path), "st_reparse_tag", 0)
|
|
62
|
+
except OSError:
|
|
63
|
+
return False
|
|
64
|
+
return tag == _IO_REPARSE_TAG_MOUNT_POINT
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _no_links(path: Path) -> None:
|
|
68
|
+
for item in (path, *path.parents):
|
|
69
|
+
if _is_link(item):
|
|
70
|
+
raise ValueError(
|
|
71
|
+
"Setup refuses symlinked or junctioned configuration or output paths"
|
|
72
|
+
)
|
|
73
|
+
if item.exists() and item != path and not item.is_dir():
|
|
74
|
+
raise ValueError("A setup parent is not a directory")
|
|
75
|
+
if path.exists() and not path.is_file():
|
|
76
|
+
raise ValueError("Configuration destination is not a regular file")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _unique_object(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
|
|
80
|
+
result: dict[str, Any] = {}
|
|
81
|
+
for key, value in pairs:
|
|
82
|
+
if key in result:
|
|
83
|
+
raise ValueError("Duplicate configuration key")
|
|
84
|
+
result[key] = value
|
|
85
|
+
return result
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class _UniqueYamlLoader(yaml.SafeLoader):
|
|
89
|
+
pass
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _yaml_mapping(loader: _UniqueYamlLoader, node: Any) -> dict[str, Any]:
|
|
93
|
+
loader.flatten_mapping(node)
|
|
94
|
+
return _unique_object(
|
|
95
|
+
[
|
|
96
|
+
(loader.construct_object(k), loader.construct_object(v))
|
|
97
|
+
for k, v in node.value
|
|
98
|
+
]
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
_UniqueYamlLoader.add_constructor(
|
|
103
|
+
yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG, _yaml_mapping
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _load_unique_yaml(text: str) -> Any:
|
|
108
|
+
# SafeLoader subclass with duplicate-key rejection. Avoid yaml.load() so
|
|
109
|
+
# Bandit B506 does not treat this as unsafe full-loader YAML.
|
|
110
|
+
loader = _UniqueYamlLoader(text)
|
|
111
|
+
try:
|
|
112
|
+
return loader.get_single_data()
|
|
113
|
+
finally:
|
|
114
|
+
loader.dispose()
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _config_path(client: str, repo: Path) -> Path:
|
|
118
|
+
if client == "hermes":
|
|
119
|
+
return (
|
|
120
|
+
Path(os.environ.get("HERMES_HOME", str(Path.home() / ".hermes")))
|
|
121
|
+
/ "config.yaml"
|
|
122
|
+
)
|
|
123
|
+
return (
|
|
124
|
+
repo
|
|
125
|
+
/ {
|
|
126
|
+
"claude": ".mcp.json",
|
|
127
|
+
"codex": ".codex/config.toml",
|
|
128
|
+
"cursor": ".cursor/mcp.json",
|
|
129
|
+
"pi": ".pi/skills/arcgraph/SKILL.md",
|
|
130
|
+
}[client]
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _guide(executable: str, repo: Path, output: Path) -> str:
|
|
135
|
+
# JSON argv is portable; the shell illustration is explicitly POSIX only.
|
|
136
|
+
argv = [executable, "--repo-root", str(repo), "--output-dir", str(output)]
|
|
137
|
+
return "\n".join(
|
|
138
|
+
[
|
|
139
|
+
"---",
|
|
140
|
+
"name: arcgraph",
|
|
141
|
+
"description: Inspect code relationships and change impact with the local ArcGraph CLI.",
|
|
142
|
+
"---",
|
|
143
|
+
"",
|
|
144
|
+
"# ArcGraph project workflow",
|
|
145
|
+
"",
|
|
146
|
+
"Use the following fixed argument prefix (JSON argv; invoke without a shell where possible):",
|
|
147
|
+
"",
|
|
148
|
+
"```json",
|
|
149
|
+
json.dumps(argv, ensure_ascii=False),
|
|
150
|
+
"```",
|
|
151
|
+
"",
|
|
152
|
+
"POSIX shell example (on Windows use the argv array with the host's native quoting):",
|
|
153
|
+
"```sh",
|
|
154
|
+
shlex.join([*argv, "help"]),
|
|
155
|
+
"```",
|
|
156
|
+
"",
|
|
157
|
+
"1. Run help, then current before trusting graph answers and after editing source.",
|
|
158
|
+
"2. For stale indexes run sync --if-stale with this SAME prefix; build a missing/incompatible index. These are explicit index writes.",
|
|
159
|
+
"3. Use bounded context/explain/impact/callers/callees/tests queries on concrete targets. Inspect freshness, target resolution, warnings, analysis limits and truncation.",
|
|
160
|
+
"4. Use source search for exact text and tests for behavior. Empty or unresolved results do not prove absence. CLI shares the same analysis limitations as MCP.",
|
|
161
|
+
"5. Do not default to --raw, execute project code to index it, or change unrelated client configuration.",
|
|
162
|
+
"",
|
|
163
|
+
]
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _render(
|
|
168
|
+
client: str, old: bytes | None, name: str, server: dict[str, Any], guide: str
|
|
169
|
+
) -> bytes:
|
|
170
|
+
if client == "pi":
|
|
171
|
+
result = guide.encode()
|
|
172
|
+
if old is not None and old != result:
|
|
173
|
+
raise ValueError(
|
|
174
|
+
"Existing Pi skill differs; choose another --client-config path or review it manually"
|
|
175
|
+
)
|
|
176
|
+
return result
|
|
177
|
+
try:
|
|
178
|
+
text = (old or b"").decode("utf-8")
|
|
179
|
+
if client == "codex":
|
|
180
|
+
data = tomllib.loads(text)
|
|
181
|
+
elif client == "hermes":
|
|
182
|
+
data = _load_unique_yaml(text) if text.strip() else {}
|
|
183
|
+
else:
|
|
184
|
+
data = (
|
|
185
|
+
json.loads(text, object_pairs_hook=_unique_object)
|
|
186
|
+
if text.strip()
|
|
187
|
+
else {}
|
|
188
|
+
)
|
|
189
|
+
except (ValueError, yaml.YAMLError, UnicodeError) as error:
|
|
190
|
+
raise ValueError(
|
|
191
|
+
"Client configuration is not valid supported JSON/TOML/YAML; existing bytes were not changed"
|
|
192
|
+
) from error
|
|
193
|
+
if not isinstance(data, dict):
|
|
194
|
+
raise ValueError("Client configuration must be a mapping")
|
|
195
|
+
key = "mcp_servers" if client in {"codex", "hermes"} else "mcpServers"
|
|
196
|
+
servers = data.get(key, {})
|
|
197
|
+
if not isinstance(servers, dict):
|
|
198
|
+
raise ValueError("MCP configuration must be a mapping")
|
|
199
|
+
entry = dict(server)
|
|
200
|
+
if client in {"claude", "cursor"}:
|
|
201
|
+
entry["type"] = "stdio"
|
|
202
|
+
if name in servers:
|
|
203
|
+
if servers[name] != entry:
|
|
204
|
+
raise ValueError(
|
|
205
|
+
"Existing ArcGraph server entry differs; refusing to replace it automatically"
|
|
206
|
+
)
|
|
207
|
+
return old or b""
|
|
208
|
+
if client == "codex":
|
|
209
|
+
# Append one new table; preserve every existing byte/comment and let
|
|
210
|
+
# tomllib reject layouts (e.g. inline sealed tables) that cannot extend.
|
|
211
|
+
result = text + "\n[mcp_servers." + json.dumps(name) + "]\n"
|
|
212
|
+
result += "command = " + json.dumps(server["command"]) + "\n"
|
|
213
|
+
result += "args = " + json.dumps(server["args"]) + "\n"
|
|
214
|
+
try:
|
|
215
|
+
parsed = tomllib.loads(result)
|
|
216
|
+
except ValueError as error:
|
|
217
|
+
raise ValueError(
|
|
218
|
+
"Cannot safely extend this TOML layout; use a separate config"
|
|
219
|
+
) from error
|
|
220
|
+
expected = {**data, key: {**servers, name: entry}}
|
|
221
|
+
if parsed != expected:
|
|
222
|
+
raise ValueError("TOML append changed unrelated configuration")
|
|
223
|
+
return result.encode()
|
|
224
|
+
data[key] = {**servers, name: entry}
|
|
225
|
+
if client == "hermes":
|
|
226
|
+
return yaml.safe_dump(data, sort_keys=False, allow_unicode=True).encode()
|
|
227
|
+
return (json.dumps(data, indent=2, ensure_ascii=False) + "\n").encode()
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _write_config(path: Path, old: bytes | None, new: bytes) -> str | None:
|
|
231
|
+
if old == new:
|
|
232
|
+
return None
|
|
233
|
+
_no_links(path)
|
|
234
|
+
if (path.read_bytes() if path.exists() else None) != old:
|
|
235
|
+
raise ValueError(
|
|
236
|
+
"Configuration changed during setup; rerun instead of overwriting"
|
|
237
|
+
)
|
|
238
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
239
|
+
backup = None
|
|
240
|
+
if old is not None:
|
|
241
|
+
descriptor, backup = tempfile.mkstemp(
|
|
242
|
+
prefix=path.name + ".arcgraph-backup-", dir=path.parent
|
|
243
|
+
)
|
|
244
|
+
with os.fdopen(descriptor, "wb") as stream:
|
|
245
|
+
stream.write(old)
|
|
246
|
+
descriptor, temporary = tempfile.mkstemp(
|
|
247
|
+
prefix=".arcgraph-config-", dir=path.parent
|
|
248
|
+
)
|
|
249
|
+
try:
|
|
250
|
+
with os.fdopen(descriptor, "wb") as stream:
|
|
251
|
+
stream.write(new)
|
|
252
|
+
stream.flush()
|
|
253
|
+
os.fsync(stream.fileno())
|
|
254
|
+
if os.name == "posix" and path.exists():
|
|
255
|
+
os.chmod(temporary, stat.S_IMODE(path.stat().st_mode))
|
|
256
|
+
_no_links(path)
|
|
257
|
+
if (path.read_bytes() if path.exists() else None) != old:
|
|
258
|
+
raise ValueError(
|
|
259
|
+
"Configuration changed during setup; rerun instead of overwriting"
|
|
260
|
+
)
|
|
261
|
+
os.replace(temporary, path)
|
|
262
|
+
finally:
|
|
263
|
+
if os.path.exists(temporary):
|
|
264
|
+
os.unlink(temporary)
|
|
265
|
+
return backup
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _run_json(command: list[str], timeout: float, repo: Path) -> dict[str, Any]:
|
|
269
|
+
result = subprocess.run(
|
|
270
|
+
command,
|
|
271
|
+
cwd=repo,
|
|
272
|
+
capture_output=True,
|
|
273
|
+
text=True,
|
|
274
|
+
encoding="utf-8",
|
|
275
|
+
errors="replace",
|
|
276
|
+
timeout=timeout,
|
|
277
|
+
check=False,
|
|
278
|
+
)
|
|
279
|
+
if result.returncode:
|
|
280
|
+
raise RuntimeError(
|
|
281
|
+
f"ArcGraph {command[-1]} exited {result.returncode}; run the reported command directly for diagnostics"
|
|
282
|
+
)
|
|
283
|
+
data = json.loads(result.stdout)
|
|
284
|
+
if not isinstance(data, dict):
|
|
285
|
+
raise ValueError("Expected an ArcGraph JSON object")
|
|
286
|
+
return data
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def _probe(server: dict[str, Any], timeout: float) -> dict[str, Any]:
|
|
290
|
+
import anyio
|
|
291
|
+
from mcp import StdioServerParameters
|
|
292
|
+
from mcp.client import Client
|
|
293
|
+
from mcp.client.stdio import stdio_client
|
|
294
|
+
from arcgraph.interfaces.agent_capabilities import mcp_capability_names
|
|
295
|
+
|
|
296
|
+
async def check() -> dict[str, Any]:
|
|
297
|
+
with anyio.fail_after(timeout):
|
|
298
|
+
with tempfile.TemporaryFile(
|
|
299
|
+
mode="w+", encoding="utf-8", errors="replace"
|
|
300
|
+
) as errors:
|
|
301
|
+
params = StdioServerParameters(
|
|
302
|
+
command=server["command"], args=server["args"]
|
|
303
|
+
)
|
|
304
|
+
async with Client(
|
|
305
|
+
stdio_client(params, errlog=errors),
|
|
306
|
+
mode="auto",
|
|
307
|
+
raise_exceptions=True,
|
|
308
|
+
read_timeout_seconds=timeout,
|
|
309
|
+
) as session:
|
|
310
|
+
tools = await session.list_tools()
|
|
311
|
+
names = {tool.name for tool in tools.tools}
|
|
312
|
+
if names != set(mcp_capability_names()):
|
|
313
|
+
raise ValueError("Unexpected MCP tool inventory")
|
|
314
|
+
response = await session.call_tool("arcgraph_index_status", {})
|
|
315
|
+
payload = response.structured_content
|
|
316
|
+
if (
|
|
317
|
+
response.is_error
|
|
318
|
+
or not payload
|
|
319
|
+
or payload.get("freshness", {}).get("status") != "fresh"
|
|
320
|
+
):
|
|
321
|
+
raise ValueError("MCP index status is not fresh")
|
|
322
|
+
return {
|
|
323
|
+
"status": "pass",
|
|
324
|
+
"tool_count": len(names),
|
|
325
|
+
"index_version": payload.get("index_version"),
|
|
326
|
+
"build_identity": payload.get("build_identity"),
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return anyio.run(check)
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def handle_setup(args: argparse.Namespace) -> dict[str, Any]:
|
|
333
|
+
result: dict[str, Any] = {
|
|
334
|
+
"schema_version": "1.0.0",
|
|
335
|
+
"client": args.client,
|
|
336
|
+
"status": "blocked",
|
|
337
|
+
"configuration_written": False,
|
|
338
|
+
"client_connection_verified": False,
|
|
339
|
+
"model_call_verified": False,
|
|
340
|
+
"_exit_code": 1,
|
|
341
|
+
}
|
|
342
|
+
try:
|
|
343
|
+
if not math.isfinite(args.timeout) or args.timeout <= 0:
|
|
344
|
+
raise ValueError("--timeout must be positive and finite")
|
|
345
|
+
repo = Path(args.repo_root).resolve()
|
|
346
|
+
if not repo.is_dir():
|
|
347
|
+
raise ValueError("Repository root is not a directory")
|
|
348
|
+
output = Path(os.path.abspath(repo / args.output_dir))
|
|
349
|
+
# Validate ancestors without rejecting the index directory itself.
|
|
350
|
+
_no_links(output / "current.json")
|
|
351
|
+
config = Path(
|
|
352
|
+
os.path.abspath(args.client_config or _config_path(args.client, repo))
|
|
353
|
+
)
|
|
354
|
+
_no_links(config)
|
|
355
|
+
selected = _arcgraph_executable_selection()
|
|
356
|
+
executable = str(selected.path)
|
|
357
|
+
if (
|
|
358
|
+
selected.source not in {"invocation", "distribution_record"}
|
|
359
|
+
or not selected.path.is_file()
|
|
360
|
+
):
|
|
361
|
+
raise ValueError(
|
|
362
|
+
"Use an installed arcgraph executable belonging to this runtime"
|
|
363
|
+
)
|
|
364
|
+
name = "arcgraph-" + hashlib.sha256(str(repo).encode()).hexdigest()[:12]
|
|
365
|
+
prefix = [executable, "--repo-root", str(repo), "--output-dir", str(output)]
|
|
366
|
+
server = {
|
|
367
|
+
"command": executable,
|
|
368
|
+
"args": [
|
|
369
|
+
"mcp",
|
|
370
|
+
"serve",
|
|
371
|
+
"--repo-root",
|
|
372
|
+
str(repo),
|
|
373
|
+
"--output-dir",
|
|
374
|
+
str(output),
|
|
375
|
+
],
|
|
376
|
+
}
|
|
377
|
+
guide = _guide(executable, repo, output)
|
|
378
|
+
old = config.read_bytes() if config.exists() else None
|
|
379
|
+
rendered = _render(args.client, old, name, server, guide)
|
|
380
|
+
result.update(
|
|
381
|
+
{
|
|
382
|
+
"repo_root": str(repo),
|
|
383
|
+
"output_dir": str(output),
|
|
384
|
+
"client_config": str(config),
|
|
385
|
+
"server_name": name if args.client != "pi" else None,
|
|
386
|
+
"transport": "cli_skill" if args.client == "pi" else "stdio",
|
|
387
|
+
"serve_command": (
|
|
388
|
+
[server["command"], *server["args"]]
|
|
389
|
+
if args.client != "pi"
|
|
390
|
+
else None
|
|
391
|
+
),
|
|
392
|
+
"cli_prefix": prefix,
|
|
393
|
+
"planned_change": old != rendered,
|
|
394
|
+
"configuration_entry": (
|
|
395
|
+
server if args.client != "pi" else {"skill": guide}
|
|
396
|
+
),
|
|
397
|
+
"guidance": guide,
|
|
398
|
+
"warnings": (
|
|
399
|
+
[
|
|
400
|
+
"Hermes writes its selected profile config; JSON/YAML edits preserve values but may reformat comments. Existing files are backed up."
|
|
401
|
+
]
|
|
402
|
+
if args.client == "hermes"
|
|
403
|
+
else []
|
|
404
|
+
),
|
|
405
|
+
"next_steps": [
|
|
406
|
+
"Open the target project in the selected client; approve project trust/MCP if requested and reconnect.",
|
|
407
|
+
"Ask the agent to call arcgraph_help then arcgraph_index_status (Pi: use the arcgraph skill and CLI help/current).",
|
|
408
|
+
"Registration and a protocol probe do not prove that the client exposed tools to its model.",
|
|
409
|
+
],
|
|
410
|
+
}
|
|
411
|
+
)
|
|
412
|
+
if args.dry_run:
|
|
413
|
+
result.update(status="planned", _exit_code=0)
|
|
414
|
+
return result
|
|
415
|
+
if args.client != "pi":
|
|
416
|
+
try:
|
|
417
|
+
import mcp.client # noqa: F401
|
|
418
|
+
except ImportError as error:
|
|
419
|
+
raise ValueError(
|
|
420
|
+
"Install arcgraph[mcp] in this same environment before setup"
|
|
421
|
+
) from error
|
|
422
|
+
print("ArcGraph setup: checking index", file=sys.stderr)
|
|
423
|
+
current_path = output / "current.json"
|
|
424
|
+
current = (
|
|
425
|
+
_run_json([*prefix, "current"], args.timeout, repo)
|
|
426
|
+
if current_path.exists()
|
|
427
|
+
else {}
|
|
428
|
+
)
|
|
429
|
+
if current and Path(current.get("repo_root", "")).resolve() != repo:
|
|
430
|
+
raise ValueError(
|
|
431
|
+
"Existing index belongs to another repository; choose a separate --output-dir"
|
|
432
|
+
)
|
|
433
|
+
if current.get("freshness", {}).get("status") != "fresh":
|
|
434
|
+
operation = (
|
|
435
|
+
"sync"
|
|
436
|
+
if current.get("freshness", {}).get("status") == "stale"
|
|
437
|
+
else "build"
|
|
438
|
+
)
|
|
439
|
+
command = [*prefix, operation] + (
|
|
440
|
+
["--if-stale"] if operation == "sync" else []
|
|
441
|
+
)
|
|
442
|
+
result["index_command"] = command
|
|
443
|
+
print("ArcGraph setup: " + operation + " index", file=sys.stderr)
|
|
444
|
+
_run_json(command, args.timeout, repo)
|
|
445
|
+
current = _run_json([*prefix, "current"], args.timeout, repo)
|
|
446
|
+
if Path(current.get("repo_root", "")).resolve() != repo:
|
|
447
|
+
raise ValueError("Index repository does not match the selected project")
|
|
448
|
+
if current.get("freshness", {}).get("status") != "fresh":
|
|
449
|
+
raise ValueError("Index is not fresh after initialization")
|
|
450
|
+
result["index_version"] = current.get("index_version")
|
|
451
|
+
if args.client != "pi":
|
|
452
|
+
result["protocol_probe"] = _probe(server, min(args.timeout, 60))
|
|
453
|
+
else:
|
|
454
|
+
_run_json([*prefix, "help"], args.timeout, repo)
|
|
455
|
+
result["cli_probe"] = {"status": "pass"}
|
|
456
|
+
backup = _write_config(config, old, rendered)
|
|
457
|
+
result.update(
|
|
458
|
+
status="prepared",
|
|
459
|
+
configuration_written=old != rendered,
|
|
460
|
+
backup_path=backup,
|
|
461
|
+
_exit_code=0,
|
|
462
|
+
)
|
|
463
|
+
except (
|
|
464
|
+
OSError,
|
|
465
|
+
ValueError,
|
|
466
|
+
RuntimeError,
|
|
467
|
+
ImportError,
|
|
468
|
+
subprocess.SubprocessError,
|
|
469
|
+
) as error:
|
|
470
|
+
result["error"] = (
|
|
471
|
+
str(error)
|
|
472
|
+
if isinstance(error, (ValueError, RuntimeError))
|
|
473
|
+
else type(error).__name__
|
|
474
|
+
)
|
|
475
|
+
except Exception as error:
|
|
476
|
+
# MCP SDK errors may be exception groups; don't serialize transport
|
|
477
|
+
# internals or client configuration into user-visible diagnostics.
|
|
478
|
+
result["error"] = (
|
|
479
|
+
f"Setup probe failed ({type(error).__name__}); configuration was not applied"
|
|
480
|
+
)
|
|
481
|
+
return result
|