graphsnip 0.2.0__tar.gz → 0.2.1__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.
Files changed (38) hide show
  1. {graphsnip-0.2.0/src/graphsnip.egg-info → graphsnip-0.2.1}/PKG-INFO +1 -1
  2. {graphsnip-0.2.0 → graphsnip-0.2.1}/pyproject.toml +1 -1
  3. {graphsnip-0.2.0 → graphsnip-0.2.1}/src/graphsnip/__init__.py +1 -1
  4. {graphsnip-0.2.0 → graphsnip-0.2.1}/src/graphsnip/cli.py +21 -1
  5. graphsnip-0.2.1/src/graphsnip/probe.py +83 -0
  6. {graphsnip-0.2.0 → graphsnip-0.2.1/src/graphsnip.egg-info}/PKG-INFO +1 -1
  7. {graphsnip-0.2.0 → graphsnip-0.2.1}/src/graphsnip.egg-info/SOURCES.txt +1 -0
  8. {graphsnip-0.2.0 → graphsnip-0.2.1}/tests/test_integrations.py +30 -0
  9. {graphsnip-0.2.0 → graphsnip-0.2.1}/LICENSE +0 -0
  10. {graphsnip-0.2.0 → graphsnip-0.2.1}/NOTICE +0 -0
  11. {graphsnip-0.2.0 → graphsnip-0.2.1}/README.md +0 -0
  12. {graphsnip-0.2.0 → graphsnip-0.2.1}/setup.cfg +0 -0
  13. {graphsnip-0.2.0 → graphsnip-0.2.1}/src/graphsnip/__main__.py +0 -0
  14. {graphsnip-0.2.0 → graphsnip-0.2.1}/src/graphsnip/ab.py +0 -0
  15. {graphsnip-0.2.0 → graphsnip-0.2.1}/src/graphsnip/baseline.py +0 -0
  16. {graphsnip-0.2.0 → graphsnip-0.2.1}/src/graphsnip/bench.py +0 -0
  17. {graphsnip-0.2.0 → graphsnip-0.2.1}/src/graphsnip/dashboard.py +0 -0
  18. {graphsnip-0.2.0 → graphsnip-0.2.1}/src/graphsnip/engine.py +0 -0
  19. {graphsnip-0.2.0 → graphsnip-0.2.1}/src/graphsnip/hooks.py +0 -0
  20. {graphsnip-0.2.0 → graphsnip-0.2.1}/src/graphsnip/installer.py +0 -0
  21. {graphsnip-0.2.0 → graphsnip-0.2.1}/src/graphsnip/ledger.py +0 -0
  22. {graphsnip-0.2.0 → graphsnip-0.2.1}/src/graphsnip/mcp_server.py +0 -0
  23. {graphsnip-0.2.0 → graphsnip-0.2.1}/src/graphsnip/project.py +0 -0
  24. {graphsnip-0.2.0 → graphsnip-0.2.1}/src/graphsnip/py.typed +0 -0
  25. {graphsnip-0.2.0 → graphsnip-0.2.1}/src/graphsnip/retrieval.py +0 -0
  26. {graphsnip-0.2.0 → graphsnip-0.2.1}/src/graphsnip/router.py +0 -0
  27. {graphsnip-0.2.0 → graphsnip-0.2.1}/src/graphsnip/semantic.py +0 -0
  28. {graphsnip-0.2.0 → graphsnip-0.2.1}/src/graphsnip/shellfilter.py +0 -0
  29. {graphsnip-0.2.0 → graphsnip-0.2.1}/src/graphsnip/slicer.py +0 -0
  30. {graphsnip-0.2.0 → graphsnip-0.2.1}/src/graphsnip/tokens.py +0 -0
  31. {graphsnip-0.2.0 → graphsnip-0.2.1}/src/graphsnip/usage.py +0 -0
  32. {graphsnip-0.2.0 → graphsnip-0.2.1}/src/graphsnip.egg-info/dependency_links.txt +0 -0
  33. {graphsnip-0.2.0 → graphsnip-0.2.1}/src/graphsnip.egg-info/entry_points.txt +0 -0
  34. {graphsnip-0.2.0 → graphsnip-0.2.1}/src/graphsnip.egg-info/requires.txt +0 -0
  35. {graphsnip-0.2.0 → graphsnip-0.2.1}/src/graphsnip.egg-info/top_level.txt +0 -0
  36. {graphsnip-0.2.0 → graphsnip-0.2.1}/tests/test_core.py +0 -0
  37. {graphsnip-0.2.0 → graphsnip-0.2.1}/tests/test_refresh.py +0 -0
  38. {graphsnip-0.2.0 → graphsnip-0.2.1}/tests/test_semantic.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: graphsnip
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: Token-saving layer on top of Graphify: symbol-level code slicing, context packs, a savings ledger with an honest baseline, hooks and MCP for Claude Code, Copilot and Codex.
5
5
  License-Expression: Apache-2.0
6
6
  Project-URL: Repository, https://github.com/naubadnithin7/codegraf
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "graphsnip"
7
- version = "0.2.0"
7
+ version = "0.2.1"
8
8
  description = "Token-saving layer on top of Graphify: symbol-level code slicing, context packs, a savings ledger with an honest baseline, hooks and MCP for Claude Code, Copilot and Codex."
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0"
@@ -1,3 +1,3 @@
1
1
  """graphsnip: a token-saving layer on top of Graphify."""
2
2
 
3
- __version__ = "0.2.0"
3
+ __version__ = "0.2.1"
@@ -444,6 +444,25 @@ def cmd_doctor(a) -> int:
444
444
  f"Re-run `graphsnip install` (without --portable) to write the full path.")
445
445
  elif not os.path.exists(c):
446
446
  print(f"! {label}: server command {c} does not exist; re-run `graphsnip install`.")
447
+ if not getattr(a, "no_probe", False):
448
+ from graphsnip.probe import probe_mcp
449
+
450
+ for label, f, key in (("claude", mcp_claude, "mcpServers"), ("copilot", p.root / ".vscode" / "mcp.json", "servers")):
451
+ try:
452
+ ent = json.loads(f.read_text(encoding="utf-8")).get(key, {}).get("graphsnip")
453
+ except Exception:
454
+ ent = None
455
+ if not ent:
456
+ continue
457
+ r = probe_mcp(str(ent.get("command", "")), [str(x) for x in ent.get("args", [])], str(p.root))
458
+ if r["ok"]:
459
+ line(True, f"{label}: the MCP server starts and lists {len(r['tools'])} tools ({r['seconds']}s)")
460
+ else:
461
+ line(False, f"{label}: the MCP server does NOT work: {r['error']}", "fix the command in the config, then reload the app")
462
+ if r["stderr"]:
463
+ print(" server said: " + r["stderr"].replace("\n", "\n "))
464
+ print(" If every server above starts but the assistant still has no graphsnip tools, the app has not started or trusted it:")
465
+ print(" Claude Code: run /mcp and approve it. VS Code: Command Palette > 'MCP: List Servers' > start/trust graphsnip, then 'Configure Tools'.")
447
466
  hook_on = st.exists() and HOOK_MARK in st.read_text(encoding="utf-8", errors="ignore")
448
467
  print(f"{'✓' if hook_on else '·'} claude hook: {'installed' if hook_on else 'not installed'} (mode: {p.config()['mode']})")
449
468
  print("\nAll good." if ok else "\nSome checks failed; see arrows above.")
@@ -550,7 +569,8 @@ def main(argv=None) -> int:
550
569
  sp = add("uninstall", cmd_uninstall, "remove everything install added")
551
570
  sp.add_argument("--clients", default="all")
552
571
  sp.add_argument("--codex-scope", choices=["project", "user"], default="project")
553
- add("doctor", cmd_doctor, "check the setup")
572
+ sp = add("doctor", cmd_doctor, "check the setup (also starts the configured MCP servers to prove they work)")
573
+ sp.add_argument("--no-probe", action="store_true", help="skip starting the MCP servers")
554
574
 
555
575
  args = ap.parse_args(argv)
556
576
  if not getattr(args, "fn", None):
@@ -0,0 +1,83 @@
1
+ """Start an MCP server exactly as an assistant would and check that it answers, so `doctor` can say why tools are missing."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import queue
6
+ import subprocess
7
+ import threading
8
+ import time
9
+
10
+
11
+ def probe_mcp(command: str, args: list[str], cwd: str, timeout: float = 25.0) -> dict:
12
+ t0 = time.time()
13
+ try:
14
+ proc = subprocess.Popen([command, *args], cwd=cwd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
15
+ text=True, bufsize=1)
16
+ except FileNotFoundError:
17
+ return {"ok": False, "error": f"command not found: {command}", "stderr": "", "tools": [], "seconds": 0.0}
18
+ except OSError as e:
19
+ return {"ok": False, "error": f"could not start {command}: {e}", "stderr": "", "tools": [], "seconds": 0.0}
20
+
21
+ out: queue.Queue = queue.Queue()
22
+ err_lines: list[str] = []
23
+
24
+ def pump(stream, sink):
25
+ for ln in stream:
26
+ sink(ln)
27
+
28
+ threading.Thread(target=pump, args=(proc.stdout, out.put), daemon=True).start()
29
+ threading.Thread(target=pump, args=(proc.stderr, err_lines.append), daemon=True).start()
30
+
31
+ def send(obj):
32
+ try:
33
+ proc.stdin.write(json.dumps(obj) + "\n")
34
+ proc.stdin.flush()
35
+ except OSError:
36
+ pass
37
+
38
+ def wait_for(mid):
39
+ end = t0 + timeout
40
+ while time.time() < end:
41
+ try:
42
+ ln = out.get(timeout=0.2)
43
+ except queue.Empty:
44
+ if proc.poll() is not None and out.empty():
45
+ return None
46
+ continue
47
+ try:
48
+ msg = json.loads(ln)
49
+ except Exception:
50
+ continue
51
+ if isinstance(msg, dict) and msg.get("id") == mid:
52
+ return msg
53
+ return None
54
+
55
+ res = {"ok": False, "error": "", "stderr": "", "tools": [], "seconds": 0.0}
56
+ try:
57
+ send({"jsonrpc": "2.0", "id": 1, "method": "initialize",
58
+ "params": {"protocolVersion": "2025-03-26", "capabilities": {}, "clientInfo": {"name": "graphsnip-doctor", "version": "0"}}})
59
+ init = wait_for(1)
60
+ if init is None:
61
+ res["error"] = ("the server exited before answering" if proc.poll() is not None else f"no answer within {timeout:.0f}s")
62
+ else:
63
+ send({"jsonrpc": "2.0", "method": "notifications/initialized"})
64
+ send({"jsonrpc": "2.0", "id": 2, "method": "tools/list"})
65
+ tl = wait_for(2)
66
+ names = [t.get("name") for t in ((tl or {}).get("result") or {}).get("tools", [])]
67
+ if names:
68
+ res.update(ok=True, tools=names)
69
+ else:
70
+ res["error"] = "the server started but listed no tools"
71
+ finally:
72
+ try:
73
+ proc.stdin.close()
74
+ except OSError:
75
+ pass
76
+ try:
77
+ proc.terminate()
78
+ proc.wait(timeout=3)
79
+ except Exception:
80
+ proc.kill()
81
+ res["seconds"] = round(time.time() - t0, 1)
82
+ res["stderr"] = "".join(err_lines)[-800:].strip()
83
+ return res
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: graphsnip
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: Token-saving layer on top of Graphify: symbol-level code slicing, context packs, a savings ledger with an honest baseline, hooks and MCP for Claude Code, Copilot and Codex.
5
5
  License-Expression: Apache-2.0
6
6
  Project-URL: Repository, https://github.com/naubadnithin7/codegraf
@@ -14,6 +14,7 @@ src/graphsnip/hooks.py
14
14
  src/graphsnip/installer.py
15
15
  src/graphsnip/ledger.py
16
16
  src/graphsnip/mcp_server.py
17
+ src/graphsnip/probe.py
17
18
  src/graphsnip/project.py
18
19
  src/graphsnip/py.typed
19
20
  src/graphsnip/retrieval.py
@@ -331,3 +331,33 @@ def test_copilot_config_uses_absolute_path_even_when_portable(tmp_path, monkeypa
331
331
  cl = json.loads((tmp_path / ".mcp.json").read_text())["mcpServers"]["graphsnip"]
332
332
  assert vs["command"] == "/opt/bin/graphsnip"
333
333
  assert cl["command"] == "graphsnip"
334
+
335
+
336
+ def test_probe_reports_a_working_and_a_broken_server(tmp_path):
337
+ import sys
338
+
339
+ from graphsnip.probe import probe_mcp
340
+
341
+ (tmp_path / "a.py").write_text("def f():\n return 1\n")
342
+ ok = probe_mcp(sys.executable, ["-m", "graphsnip", "serve", "--root", "."], str(tmp_path))
343
+ assert ok["ok"] and "gs_pack" in ok["tools"] and len(ok["tools"]) == 7
344
+ bad = probe_mcp("/definitely/not/here/graphsnip", ["serve"], str(tmp_path))
345
+ assert not bad["ok"] and "not found" in bad["error"]
346
+ crash = probe_mcp(sys.executable, ["-c", "import sys; sys.stderr.write('boom: ImportError x'); sys.exit(1)"], str(tmp_path), timeout=5)
347
+ assert not crash["ok"] and "boom" in crash["stderr"]
348
+
349
+
350
+ def test_doctor_probes_configured_servers(tmp_path, capsys):
351
+ import json
352
+ import sys
353
+
354
+ from graphsnip import cli
355
+
356
+ (tmp_path / "a.py").write_text("def f():\n return 1\n")
357
+ (tmp_path / ".mcp.json").write_text(json.dumps({"mcpServers": {"graphsnip": {"command": sys.executable, "args": ["-m", "graphsnip", "serve", "--root", "."]}}}))
358
+ (tmp_path / ".vscode").mkdir()
359
+ (tmp_path / ".vscode" / "mcp.json").write_text(json.dumps({"servers": {"graphsnip": {"type": "stdio", "command": "/nope/graphsnip", "args": ["serve"]}}}))
360
+ cli.main(["doctor", "--root", str(tmp_path)])
361
+ out = capsys.readouterr().out
362
+ assert "claude: the MCP server starts and lists 7 tools" in out
363
+ assert "copilot: the MCP server does NOT work" in out and "command not found" in out
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes