graphifyy 0.7.2__tar.gz → 0.7.4__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.
- {graphifyy-0.7.2/graphifyy.egg-info → graphifyy-0.7.4}/PKG-INFO +2 -2
- {graphifyy-0.7.2 → graphifyy-0.7.4}/README.md +1 -1
- {graphifyy-0.7.2 → graphifyy-0.7.4}/graphify/__main__.py +233 -0
- {graphifyy-0.7.2 → graphifyy-0.7.4}/graphify/extract.py +83 -5
- {graphifyy-0.7.2 → graphifyy-0.7.4/graphifyy.egg-info}/PKG-INFO +2 -2
- {graphifyy-0.7.2 → graphifyy-0.7.4}/pyproject.toml +1 -1
- {graphifyy-0.7.2 → graphifyy-0.7.4}/LICENSE +0 -0
- {graphifyy-0.7.2 → graphifyy-0.7.4}/graphify/__init__.py +0 -0
- {graphifyy-0.7.2 → graphifyy-0.7.4}/graphify/analyze.py +0 -0
- {graphifyy-0.7.2 → graphifyy-0.7.4}/graphify/benchmark.py +0 -0
- {graphifyy-0.7.2 → graphifyy-0.7.4}/graphify/build.py +0 -0
- {graphifyy-0.7.2 → graphifyy-0.7.4}/graphify/cache.py +0 -0
- {graphifyy-0.7.2 → graphifyy-0.7.4}/graphify/cluster.py +0 -0
- {graphifyy-0.7.2 → graphifyy-0.7.4}/graphify/detect.py +0 -0
- {graphifyy-0.7.2 → graphifyy-0.7.4}/graphify/export.py +0 -0
- {graphifyy-0.7.2 → graphifyy-0.7.4}/graphify/hooks.py +0 -0
- {graphifyy-0.7.2 → graphifyy-0.7.4}/graphify/ingest.py +0 -0
- {graphifyy-0.7.2 → graphifyy-0.7.4}/graphify/llm.py +0 -0
- {graphifyy-0.7.2 → graphifyy-0.7.4}/graphify/manifest.py +0 -0
- {graphifyy-0.7.2 → graphifyy-0.7.4}/graphify/report.py +0 -0
- {graphifyy-0.7.2 → graphifyy-0.7.4}/graphify/security.py +0 -0
- {graphifyy-0.7.2 → graphifyy-0.7.4}/graphify/serve.py +0 -0
- {graphifyy-0.7.2 → graphifyy-0.7.4}/graphify/skill-aider.md +0 -0
- {graphifyy-0.7.2 → graphifyy-0.7.4}/graphify/skill-claw.md +0 -0
- {graphifyy-0.7.2 → graphifyy-0.7.4}/graphify/skill-codex.md +0 -0
- {graphifyy-0.7.2 → graphifyy-0.7.4}/graphify/skill-copilot.md +0 -0
- {graphifyy-0.7.2 → graphifyy-0.7.4}/graphify/skill-droid.md +0 -0
- {graphifyy-0.7.2 → graphifyy-0.7.4}/graphify/skill-kiro.md +0 -0
- {graphifyy-0.7.2 → graphifyy-0.7.4}/graphify/skill-opencode.md +0 -0
- {graphifyy-0.7.2 → graphifyy-0.7.4}/graphify/skill-pi.md +0 -0
- {graphifyy-0.7.2 → graphifyy-0.7.4}/graphify/skill-trae.md +0 -0
- {graphifyy-0.7.2 → graphifyy-0.7.4}/graphify/skill-vscode.md +0 -0
- {graphifyy-0.7.2 → graphifyy-0.7.4}/graphify/skill-windows.md +0 -0
- {graphifyy-0.7.2 → graphifyy-0.7.4}/graphify/skill.md +0 -0
- {graphifyy-0.7.2 → graphifyy-0.7.4}/graphify/transcribe.py +0 -0
- {graphifyy-0.7.2 → graphifyy-0.7.4}/graphify/tree_html.py +0 -0
- {graphifyy-0.7.2 → graphifyy-0.7.4}/graphify/validate.py +0 -0
- {graphifyy-0.7.2 → graphifyy-0.7.4}/graphify/watch.py +0 -0
- {graphifyy-0.7.2 → graphifyy-0.7.4}/graphify/wiki.py +0 -0
- {graphifyy-0.7.2 → graphifyy-0.7.4}/graphifyy.egg-info/SOURCES.txt +0 -0
- {graphifyy-0.7.2 → graphifyy-0.7.4}/graphifyy.egg-info/dependency_links.txt +0 -0
- {graphifyy-0.7.2 → graphifyy-0.7.4}/graphifyy.egg-info/entry_points.txt +0 -0
- {graphifyy-0.7.2 → graphifyy-0.7.4}/graphifyy.egg-info/requires.txt +0 -0
- {graphifyy-0.7.2 → graphifyy-0.7.4}/graphifyy.egg-info/top_level.txt +0 -0
- {graphifyy-0.7.2 → graphifyy-0.7.4}/setup.cfg +0 -0
- {graphifyy-0.7.2 → graphifyy-0.7.4}/tests/test_analyze.py +0 -0
- {graphifyy-0.7.2 → graphifyy-0.7.4}/tests/test_benchmark.py +0 -0
- {graphifyy-0.7.2 → graphifyy-0.7.4}/tests/test_build.py +0 -0
- {graphifyy-0.7.2 → graphifyy-0.7.4}/tests/test_cache.py +0 -0
- {graphifyy-0.7.2 → graphifyy-0.7.4}/tests/test_chunking.py +0 -0
- {graphifyy-0.7.2 → graphifyy-0.7.4}/tests/test_claude_md.py +0 -0
- {graphifyy-0.7.2 → graphifyy-0.7.4}/tests/test_cli_export.py +0 -0
- {graphifyy-0.7.2 → graphifyy-0.7.4}/tests/test_cluster.py +0 -0
- {graphifyy-0.7.2 → graphifyy-0.7.4}/tests/test_confidence.py +0 -0
- {graphifyy-0.7.2 → graphifyy-0.7.4}/tests/test_detect.py +0 -0
- {graphifyy-0.7.2 → graphifyy-0.7.4}/tests/test_export.py +0 -0
- {graphifyy-0.7.2 → graphifyy-0.7.4}/tests/test_extract.py +0 -0
- {graphifyy-0.7.2 → graphifyy-0.7.4}/tests/test_hooks.py +0 -0
- {graphifyy-0.7.2 → graphifyy-0.7.4}/tests/test_hypergraph.py +0 -0
- {graphifyy-0.7.2 → graphifyy-0.7.4}/tests/test_ingest.py +0 -0
- {graphifyy-0.7.2 → graphifyy-0.7.4}/tests/test_install.py +0 -0
- {graphifyy-0.7.2 → graphifyy-0.7.4}/tests/test_languages.py +0 -0
- {graphifyy-0.7.2 → graphifyy-0.7.4}/tests/test_multilang.py +0 -0
- {graphifyy-0.7.2 → graphifyy-0.7.4}/tests/test_pipeline.py +0 -0
- {graphifyy-0.7.2 → graphifyy-0.7.4}/tests/test_query_cli.py +0 -0
- {graphifyy-0.7.2 → graphifyy-0.7.4}/tests/test_rationale.py +0 -0
- {graphifyy-0.7.2 → graphifyy-0.7.4}/tests/test_report.py +0 -0
- {graphifyy-0.7.2 → graphifyy-0.7.4}/tests/test_security.py +0 -0
- {graphifyy-0.7.2 → graphifyy-0.7.4}/tests/test_semantic_similarity.py +0 -0
- {graphifyy-0.7.2 → graphifyy-0.7.4}/tests/test_serve.py +0 -0
- {graphifyy-0.7.2 → graphifyy-0.7.4}/tests/test_transcribe.py +0 -0
- {graphifyy-0.7.2 → graphifyy-0.7.4}/tests/test_validate.py +0 -0
- {graphifyy-0.7.2 → graphifyy-0.7.4}/tests/test_watch.py +0 -0
- {graphifyy-0.7.2 → graphifyy-0.7.4}/tests/test_wiki.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: graphifyy
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.4
|
|
4
4
|
Summary: AI coding assistant skill (Claude Code, Codex, OpenCode, Cursor, Gemini CLI, Aider, OpenClaw, Factory Droid, Trae, Hermes, Kiro, Pi, Google Antigravity) - turn any folder of code, docs, papers, images, or videos into a queryable knowledge graph
|
|
5
5
|
License: MIT License
|
|
6
6
|
|
|
@@ -319,7 +319,7 @@ The MCP server gives your assistant structured access: `query_graph`, `get_node`
|
|
|
319
319
|
|
|
320
320
|
- **Code files** — processed locally via tree-sitter. Nothing leaves your machine.
|
|
321
321
|
- **Video / audio** — transcribed locally with faster-whisper. Nothing leaves your machine.
|
|
322
|
-
- **Docs, PDFs, images** — sent to your AI assistant
|
|
322
|
+
- **Docs, PDFs, images** — sent to your AI assistant for semantic extraction (via the `/graphify` skill, using whatever model your IDE session runs). Headless `graphify extract` requires `ANTHROPIC_API_KEY` or `MOONSHOT_API_KEY`.
|
|
323
323
|
- No telemetry, no usage tracking, no analytics.
|
|
324
324
|
|
|
325
325
|
---
|
|
@@ -221,7 +221,7 @@ The MCP server gives your assistant structured access: `query_graph`, `get_node`
|
|
|
221
221
|
|
|
222
222
|
- **Code files** — processed locally via tree-sitter. Nothing leaves your machine.
|
|
223
223
|
- **Video / audio** — transcribed locally with faster-whisper. Nothing leaves your machine.
|
|
224
|
-
- **Docs, PDFs, images** — sent to your AI assistant
|
|
224
|
+
- **Docs, PDFs, images** — sent to your AI assistant for semantic extraction (via the `/graphify` skill, using whatever model your IDE session runs). Headless `graphify extract` requires `ANTHROPIC_API_KEY` or `MOONSHOT_API_KEY`.
|
|
225
225
|
- No telemetry, no usage tracking, no analytics.
|
|
226
226
|
|
|
227
227
|
---
|
|
@@ -1094,6 +1094,10 @@ def main() -> None:
|
|
|
1094
1094
|
print(" --max-children N cap children per node (default 200)")
|
|
1095
1095
|
print(" --top-k-edges N per-symbol outbound edges in inspector (default 12)")
|
|
1096
1096
|
print(" --label NAME project label in header")
|
|
1097
|
+
print(" extract <path> headless full extraction (AST + semantic LLM) for CI/scripts")
|
|
1098
|
+
print(" --backend B kimi|claude (default: whichever API key is set)")
|
|
1099
|
+
print(" --out DIR output dir (default: <path>); writes <DIR>/graphify-out/")
|
|
1100
|
+
print(" --no-cluster skip clustering, write raw extraction only")
|
|
1097
1101
|
print(" benchmark [graph.json] measure token reduction vs naive full-corpus approach")
|
|
1098
1102
|
print(" hook install install post-commit/post-checkout git hooks (all platforms)")
|
|
1099
1103
|
print(" hook uninstall remove git hooks")
|
|
@@ -1882,6 +1886,235 @@ def main() -> None:
|
|
|
1882
1886
|
pass
|
|
1883
1887
|
result = run_benchmark(graph_path, corpus_words=corpus_words)
|
|
1884
1888
|
print_benchmark(result)
|
|
1889
|
+
|
|
1890
|
+
elif cmd == "extract":
|
|
1891
|
+
# Headless full-pipeline extraction for CI / scripts (#698).
|
|
1892
|
+
# Runs detect -> AST extraction on code -> semantic LLM extraction on
|
|
1893
|
+
# docs/papers/images -> merge -> build -> cluster -> write outputs.
|
|
1894
|
+
# Unlike the skill.md path (which runs through Claude Code subagents),
|
|
1895
|
+
# this calls extract_corpus_parallel directly using whichever backend
|
|
1896
|
+
# has an API key set.
|
|
1897
|
+
if len(sys.argv) < 3:
|
|
1898
|
+
print(
|
|
1899
|
+
"Usage: graphify extract <path> [--backend kimi|claude] "
|
|
1900
|
+
"[--out DIR] [--no-cluster]",
|
|
1901
|
+
file=sys.stderr,
|
|
1902
|
+
)
|
|
1903
|
+
sys.exit(1)
|
|
1904
|
+
|
|
1905
|
+
target = Path(sys.argv[2]).resolve()
|
|
1906
|
+
if not target.exists():
|
|
1907
|
+
print(f"error: path not found: {target}", file=sys.stderr)
|
|
1908
|
+
sys.exit(1)
|
|
1909
|
+
|
|
1910
|
+
backend: str | None = None
|
|
1911
|
+
out_dir: Path | None = None
|
|
1912
|
+
no_cluster = False
|
|
1913
|
+
args = sys.argv[3:]
|
|
1914
|
+
i = 0
|
|
1915
|
+
while i < len(args):
|
|
1916
|
+
a = args[i]
|
|
1917
|
+
if a == "--backend" and i + 1 < len(args):
|
|
1918
|
+
backend = args[i + 1]; i += 2
|
|
1919
|
+
elif a.startswith("--backend="):
|
|
1920
|
+
backend = a.split("=", 1)[1]; i += 1
|
|
1921
|
+
elif a == "--out" and i + 1 < len(args):
|
|
1922
|
+
out_dir = Path(args[i + 1]); i += 2
|
|
1923
|
+
elif a.startswith("--out="):
|
|
1924
|
+
out_dir = Path(a.split("=", 1)[1]); i += 1
|
|
1925
|
+
elif a == "--no-cluster":
|
|
1926
|
+
no_cluster = True; i += 1
|
|
1927
|
+
else:
|
|
1928
|
+
i += 1
|
|
1929
|
+
|
|
1930
|
+
# Backend resolution. If user did not pass --backend, sniff env.
|
|
1931
|
+
# If backend was explicitly requested, validate its key is present
|
|
1932
|
+
# and surface a clear error early — don't let extract_corpus_parallel
|
|
1933
|
+
# raise mid-run after we've spent time on AST extraction.
|
|
1934
|
+
from graphify.llm import (
|
|
1935
|
+
BACKENDS as _BACKENDS,
|
|
1936
|
+
detect_backend as _detect_backend,
|
|
1937
|
+
estimate_cost as _estimate_cost,
|
|
1938
|
+
extract_corpus_parallel as _extract_corpus_parallel,
|
|
1939
|
+
)
|
|
1940
|
+
if backend is None:
|
|
1941
|
+
backend = _detect_backend()
|
|
1942
|
+
if backend is None:
|
|
1943
|
+
print(
|
|
1944
|
+
"error: no LLM API key found. Set MOONSHOT_API_KEY (kimi) "
|
|
1945
|
+
"or ANTHROPIC_API_KEY (claude), or pass --backend.",
|
|
1946
|
+
file=sys.stderr,
|
|
1947
|
+
)
|
|
1948
|
+
sys.exit(1)
|
|
1949
|
+
if backend not in _BACKENDS:
|
|
1950
|
+
print(
|
|
1951
|
+
f"error: unknown backend '{backend}'. "
|
|
1952
|
+
f"Available: {', '.join(sorted(_BACKENDS))}",
|
|
1953
|
+
file=sys.stderr,
|
|
1954
|
+
)
|
|
1955
|
+
sys.exit(1)
|
|
1956
|
+
env_key = _BACKENDS[backend]["env_key"]
|
|
1957
|
+
if not os.environ.get(env_key):
|
|
1958
|
+
print(
|
|
1959
|
+
f"error: backend '{backend}' requires {env_key} to be set.",
|
|
1960
|
+
file=sys.stderr,
|
|
1961
|
+
)
|
|
1962
|
+
sys.exit(1)
|
|
1963
|
+
|
|
1964
|
+
# Resolve output dir. The user-facing contract is "<out>/graphify-out/"
|
|
1965
|
+
# so a fresh checkout writes graphify-out/ at the project root, matching
|
|
1966
|
+
# the skill.md pipeline.
|
|
1967
|
+
out_root = (out_dir.resolve() if out_dir else target)
|
|
1968
|
+
graphify_out = out_root / "graphify-out"
|
|
1969
|
+
graphify_out.mkdir(parents=True, exist_ok=True)
|
|
1970
|
+
|
|
1971
|
+
from graphify.detect import detect as _detect
|
|
1972
|
+
print(f"[graphify extract] scanning {target}")
|
|
1973
|
+
detection = _detect(target)
|
|
1974
|
+
files_by_type = detection.get("files", {})
|
|
1975
|
+
code_files = [Path(p) for p in files_by_type.get("code", [])]
|
|
1976
|
+
doc_files = [Path(p) for p in files_by_type.get("document", [])]
|
|
1977
|
+
paper_files = [Path(p) for p in files_by_type.get("paper", [])]
|
|
1978
|
+
image_files = [Path(p) for p in files_by_type.get("image", [])]
|
|
1979
|
+
semantic_files = doc_files + paper_files + image_files
|
|
1980
|
+
print(
|
|
1981
|
+
f"[graphify extract] found {len(code_files)} code, "
|
|
1982
|
+
f"{len(doc_files)} docs, {len(paper_files)} papers, "
|
|
1983
|
+
f"{len(image_files)} images"
|
|
1984
|
+
)
|
|
1985
|
+
|
|
1986
|
+
# AST extraction on code files. Empty code list (docs-only corpus) is
|
|
1987
|
+
# the issue #698 case — skip cleanly instead of crashing inside extract().
|
|
1988
|
+
ast_result: dict = {"nodes": [], "edges": [], "input_tokens": 0, "output_tokens": 0}
|
|
1989
|
+
if code_files:
|
|
1990
|
+
from graphify.extract import extract as _ast_extract
|
|
1991
|
+
print(f"[graphify extract] AST extraction on {len(code_files)} code files...")
|
|
1992
|
+
try:
|
|
1993
|
+
ast_result = _ast_extract(code_files, cache_root=target)
|
|
1994
|
+
except Exception as exc:
|
|
1995
|
+
print(f"[graphify extract] AST extraction failed: {exc}", file=sys.stderr)
|
|
1996
|
+
ast_result = {"nodes": [], "edges": [], "input_tokens": 0, "output_tokens": 0}
|
|
1997
|
+
|
|
1998
|
+
# Semantic extraction on docs/papers/images. Skip if there's nothing
|
|
1999
|
+
# for the LLM — saves an empty API call and prevents BACKENDS validation
|
|
2000
|
+
# from firing on a no-op.
|
|
2001
|
+
sem_result: dict = {
|
|
2002
|
+
"nodes": [], "edges": [], "hyperedges": [],
|
|
2003
|
+
"input_tokens": 0, "output_tokens": 0,
|
|
2004
|
+
}
|
|
2005
|
+
if semantic_files:
|
|
2006
|
+
print(
|
|
2007
|
+
f"[graphify extract] semantic extraction on "
|
|
2008
|
+
f"{len(semantic_files)} files via {backend}..."
|
|
2009
|
+
)
|
|
2010
|
+
try:
|
|
2011
|
+
sem_result = _extract_corpus_parallel(
|
|
2012
|
+
semantic_files,
|
|
2013
|
+
backend=backend,
|
|
2014
|
+
root=target,
|
|
2015
|
+
)
|
|
2016
|
+
except ImportError as exc:
|
|
2017
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
2018
|
+
sys.exit(1)
|
|
2019
|
+
except Exception as exc:
|
|
2020
|
+
print(
|
|
2021
|
+
f"[graphify extract] semantic extraction failed: {exc}",
|
|
2022
|
+
file=sys.stderr,
|
|
2023
|
+
)
|
|
2024
|
+
|
|
2025
|
+
# Merge AST + semantic. Order matters for deduplication: passing AST
|
|
2026
|
+
# first means semantic node attributes win on collision (richer labels
|
|
2027
|
+
# for symbols also referenced in docs). Hyperedges only come from the
|
|
2028
|
+
# semantic side.
|
|
2029
|
+
merged: dict = {
|
|
2030
|
+
"nodes": list(ast_result.get("nodes", [])) + list(sem_result.get("nodes", [])),
|
|
2031
|
+
"edges": list(ast_result.get("edges", [])) + list(sem_result.get("edges", [])),
|
|
2032
|
+
"hyperedges": list(sem_result.get("hyperedges", [])),
|
|
2033
|
+
"input_tokens": ast_result.get("input_tokens", 0) + sem_result.get("input_tokens", 0),
|
|
2034
|
+
"output_tokens": ast_result.get("output_tokens", 0) + sem_result.get("output_tokens", 0),
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
graph_json_path = graphify_out / "graph.json"
|
|
2038
|
+
analysis_path = graphify_out / ".graphify_analysis.json"
|
|
2039
|
+
|
|
2040
|
+
if no_cluster:
|
|
2041
|
+
# --no-cluster: dump the raw merged extraction as graph.json.
|
|
2042
|
+
# No NetworkX, no community detection, no analysis sidecar.
|
|
2043
|
+
graph_json_path.write_text(
|
|
2044
|
+
json.dumps(merged, indent=2), encoding="utf-8"
|
|
2045
|
+
)
|
|
2046
|
+
cost = _estimate_cost(
|
|
2047
|
+
backend, merged["input_tokens"], merged["output_tokens"]
|
|
2048
|
+
)
|
|
2049
|
+
print(
|
|
2050
|
+
f"[graphify extract] wrote {graph_json_path} — "
|
|
2051
|
+
f"{len(merged['nodes'])} nodes, {len(merged['edges'])} edges "
|
|
2052
|
+
f"(no clustering)"
|
|
2053
|
+
)
|
|
2054
|
+
if merged["input_tokens"] or merged["output_tokens"]:
|
|
2055
|
+
print(
|
|
2056
|
+
f"[graphify extract] tokens: "
|
|
2057
|
+
f"{merged['input_tokens']:,} in / "
|
|
2058
|
+
f"{merged['output_tokens']:,} out, "
|
|
2059
|
+
f"est. cost: ${cost:.4f}"
|
|
2060
|
+
)
|
|
2061
|
+
sys.exit(0)
|
|
2062
|
+
|
|
2063
|
+
# Build graph + cluster + score + write.
|
|
2064
|
+
from graphify.build import build_from_json as _build_from_json
|
|
2065
|
+
from graphify.cluster import cluster as _cluster, score_all as _score_all
|
|
2066
|
+
from graphify.export import to_json as _to_json
|
|
2067
|
+
from graphify.analyze import god_nodes as _god_nodes, surprising_connections as _surprising
|
|
2068
|
+
|
|
2069
|
+
G = _build_from_json(merged)
|
|
2070
|
+
if G.number_of_nodes() == 0:
|
|
2071
|
+
print(
|
|
2072
|
+
"[graphify extract] graph is empty — extraction produced no nodes. "
|
|
2073
|
+
"Possible causes: all files skipped, binary-only corpus, or LLM "
|
|
2074
|
+
"returned no edges.",
|
|
2075
|
+
file=sys.stderr,
|
|
2076
|
+
)
|
|
2077
|
+
sys.exit(1)
|
|
2078
|
+
|
|
2079
|
+
communities = _cluster(G)
|
|
2080
|
+
cohesion = _score_all(G, communities)
|
|
2081
|
+
try:
|
|
2082
|
+
gods = _god_nodes(G)
|
|
2083
|
+
except Exception:
|
|
2084
|
+
gods = []
|
|
2085
|
+
try:
|
|
2086
|
+
surprises = _surprising(G, communities)
|
|
2087
|
+
except Exception:
|
|
2088
|
+
surprises = []
|
|
2089
|
+
|
|
2090
|
+
_to_json(G, communities, str(graph_json_path), force=True)
|
|
2091
|
+
analysis = {
|
|
2092
|
+
"communities": {str(k): v for k, v in communities.items()},
|
|
2093
|
+
"cohesion": {str(k): v for k, v in cohesion.items()},
|
|
2094
|
+
"gods": gods,
|
|
2095
|
+
"surprises": surprises,
|
|
2096
|
+
"tokens": {
|
|
2097
|
+
"input": merged["input_tokens"],
|
|
2098
|
+
"output": merged["output_tokens"],
|
|
2099
|
+
},
|
|
2100
|
+
}
|
|
2101
|
+
analysis_path.write_text(json.dumps(analysis, indent=2), encoding="utf-8")
|
|
2102
|
+
|
|
2103
|
+
cost = _estimate_cost(backend, merged["input_tokens"], merged["output_tokens"])
|
|
2104
|
+
print(
|
|
2105
|
+
f"[graphify extract] wrote {graph_json_path}: "
|
|
2106
|
+
f"{G.number_of_nodes()} nodes, {G.number_of_edges()} edges, "
|
|
2107
|
+
f"{len(communities)} communities"
|
|
2108
|
+
)
|
|
2109
|
+
print(f"[graphify extract] wrote {analysis_path}")
|
|
2110
|
+
if merged["input_tokens"] or merged["output_tokens"]:
|
|
2111
|
+
print(
|
|
2112
|
+
f"[graphify extract] tokens: "
|
|
2113
|
+
f"{merged['input_tokens']:,} in / "
|
|
2114
|
+
f"{merged['output_tokens']:,} out, "
|
|
2115
|
+
f"est. cost (~{backend}): ${cost:.4f}"
|
|
2116
|
+
)
|
|
2117
|
+
|
|
1885
2118
|
else:
|
|
1886
2119
|
print(f"error: unknown command '{cmd}'", file=sys.stderr)
|
|
1887
2120
|
print("Run 'graphify --help' for usage.", file=sys.stderr)
|
|
@@ -48,18 +48,59 @@ def _file_stem(path: Path) -> str:
|
|
|
48
48
|
_TSCONFIG_ALIAS_CACHE: dict[str, dict[str, str]] = {}
|
|
49
49
|
|
|
50
50
|
|
|
51
|
+
def _strip_jsonc(text: str) -> str:
|
|
52
|
+
"""Strip // line comments, /* */ block comments, and trailing commas from JSONC.
|
|
53
|
+
|
|
54
|
+
Preserves string contents (including // and /* inside strings) by skipping over
|
|
55
|
+
quoted spans first. Required for tsconfig.json files generated by SvelteKit,
|
|
56
|
+
NestJS, Vite, T3, Astro, etc., which use JSONC by default (#700).
|
|
57
|
+
"""
|
|
58
|
+
# Remove block and line comments while leaving string literals untouched.
|
|
59
|
+
pattern = re.compile(
|
|
60
|
+
r'"(?:\\.|[^"\\])*"' # double-quoted string (with escapes)
|
|
61
|
+
r"|/\*.*?\*/" # /* block comment */
|
|
62
|
+
r"|//[^\n]*", # // line comment
|
|
63
|
+
re.DOTALL,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
def _replace(match: re.Match) -> str:
|
|
67
|
+
token = match.group(0)
|
|
68
|
+
if token.startswith('"'):
|
|
69
|
+
return token
|
|
70
|
+
return ""
|
|
71
|
+
|
|
72
|
+
stripped = pattern.sub(_replace, text)
|
|
73
|
+
# Remove trailing commas before } or ] (allowing whitespace between).
|
|
74
|
+
stripped = re.sub(r",(\s*[}\]])", r"\1", stripped)
|
|
75
|
+
return stripped
|
|
76
|
+
|
|
77
|
+
|
|
51
78
|
def _read_tsconfig_aliases(tsconfig: Path, base_dir: Path, seen: set) -> dict[str, str]:
|
|
52
79
|
"""Recursively read path aliases from a tsconfig, following extends chains.
|
|
53
80
|
|
|
54
81
|
Child config paths override parent. Circular extends are detected via seen set.
|
|
55
82
|
npm package configs (e.g. @tsconfig/svelte) are skipped since they're not on disk.
|
|
83
|
+
Handles JSONC (comments + trailing commas) which is the default tsconfig format
|
|
84
|
+
for SvelteKit, NestJS, Vite, T3, Astro, etc. (#700).
|
|
56
85
|
"""
|
|
57
86
|
if str(tsconfig) in seen:
|
|
58
87
|
return {}
|
|
59
88
|
seen.add(str(tsconfig))
|
|
60
89
|
try:
|
|
61
|
-
|
|
62
|
-
except Exception:
|
|
90
|
+
raw = tsconfig.read_text(encoding="utf-8")
|
|
91
|
+
except Exception as e:
|
|
92
|
+
print(f" warning: could not read {tsconfig} ({type(e).__name__}: {e})", file=sys.stderr, flush=True)
|
|
93
|
+
return {}
|
|
94
|
+
try:
|
|
95
|
+
data = json.loads(raw)
|
|
96
|
+
except json.JSONDecodeError:
|
|
97
|
+
try:
|
|
98
|
+
data = json.loads(_strip_jsonc(raw))
|
|
99
|
+
except json.JSONDecodeError as e:
|
|
100
|
+
print(f" warning: failed to parse {tsconfig} as JSON/JSONC ({e.msg} at line {e.lineno} col {e.colno})", file=sys.stderr, flush=True)
|
|
101
|
+
return {}
|
|
102
|
+
except Exception as e:
|
|
103
|
+
print(f" warning: failed to parse {tsconfig} ({type(e).__name__}: {e})", file=sys.stderr, flush=True)
|
|
63
104
|
return {}
|
|
64
105
|
|
|
65
106
|
aliases: dict[str, str] = {}
|
|
@@ -1708,13 +1749,50 @@ def extract_svelte(path: Path) -> dict:
|
|
|
1708
1749
|
import re as _re
|
|
1709
1750
|
src = path.read_text(encoding="utf-8", errors="replace")
|
|
1710
1751
|
existing_ids = {n["id"] for n in result.get("nodes", [])}
|
|
1711
|
-
|
|
1752
|
+
# Source file node ID must match the one _extract_generic creates:
|
|
1753
|
+
# _make_id(str(path)) - single arg, no stem prefix. Otherwise the source
|
|
1754
|
+
# endpoint is a phantom node and build_from_json drops the edge (#701).
|
|
1755
|
+
file_node_id = _make_id(str(path))
|
|
1756
|
+
aliases = _load_tsconfig_aliases(path.parent)
|
|
1712
1757
|
for m in _re.finditer(r"""import\(\s*['"]([^'"]+)['"]\s*\)""", src):
|
|
1713
1758
|
raw = m.group(1)
|
|
1714
|
-
if not raw
|
|
1759
|
+
if not raw:
|
|
1715
1760
|
continue
|
|
1716
|
-
|
|
1761
|
+
if raw.startswith("."):
|
|
1762
|
+
# Relative import - resolve to full path so IDs match file node IDs.
|
|
1763
|
+
resolved = Path(os.path.normpath(path.parent / raw))
|
|
1764
|
+
# TypeScript ESM: imports written as .js but actual file is .ts/.tsx
|
|
1765
|
+
if resolved.suffix == ".js":
|
|
1766
|
+
resolved = resolved.with_suffix(".ts")
|
|
1767
|
+
elif resolved.suffix == ".jsx":
|
|
1768
|
+
resolved = resolved.with_suffix(".tsx")
|
|
1769
|
+
node_id = _make_id(str(resolved))
|
|
1770
|
+
else:
|
|
1771
|
+
# Check tsconfig.json path aliases (e.g. "$lib/" -> "src/lib/", "@/" -> "src/")
|
|
1772
|
+
# before treating as external. Mirrors _import_js logic so SvelteKit alias
|
|
1773
|
+
# imports resolve to the same file node IDs the extractor creates (#701).
|
|
1774
|
+
resolved_alias = None
|
|
1775
|
+
for alias_prefix, alias_base in aliases.items():
|
|
1776
|
+
if raw == alias_prefix or raw.startswith(alias_prefix + "/"):
|
|
1777
|
+
rest = raw[len(alias_prefix):].lstrip("/")
|
|
1778
|
+
resolved_alias = Path(os.path.normpath(Path(alias_base) / rest))
|
|
1779
|
+
break
|
|
1780
|
+
if resolved_alias is not None:
|
|
1781
|
+
node_id = _make_id(str(resolved_alias))
|
|
1782
|
+
else:
|
|
1783
|
+
# Bare/scoped import (node_modules) - use last segment;
|
|
1784
|
+
# build_from_json drops as external if no matching node exists.
|
|
1785
|
+
module_name = raw.split("/")[-1]
|
|
1786
|
+
if not module_name:
|
|
1787
|
+
continue
|
|
1788
|
+
node_id = _make_id(module_name)
|
|
1717
1789
|
if node_id in existing_ids:
|
|
1790
|
+
# Edge target already a real node - just add the edge, don't add a node.
|
|
1791
|
+
result.setdefault("edges", []).append({
|
|
1792
|
+
"source": file_node_id, "target": node_id,
|
|
1793
|
+
"relation": "dynamic_import", "confidence": "EXTRACTED",
|
|
1794
|
+
"source_file": str(path),
|
|
1795
|
+
})
|
|
1718
1796
|
continue
|
|
1719
1797
|
result.setdefault("nodes", []).append({
|
|
1720
1798
|
"id": node_id, "label": raw,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: graphifyy
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.4
|
|
4
4
|
Summary: AI coding assistant skill (Claude Code, Codex, OpenCode, Cursor, Gemini CLI, Aider, OpenClaw, Factory Droid, Trae, Hermes, Kiro, Pi, Google Antigravity) - turn any folder of code, docs, papers, images, or videos into a queryable knowledge graph
|
|
5
5
|
License: MIT License
|
|
6
6
|
|
|
@@ -319,7 +319,7 @@ The MCP server gives your assistant structured access: `query_graph`, `get_node`
|
|
|
319
319
|
|
|
320
320
|
- **Code files** — processed locally via tree-sitter. Nothing leaves your machine.
|
|
321
321
|
- **Video / audio** — transcribed locally with faster-whisper. Nothing leaves your machine.
|
|
322
|
-
- **Docs, PDFs, images** — sent to your AI assistant
|
|
322
|
+
- **Docs, PDFs, images** — sent to your AI assistant for semantic extraction (via the `/graphify` skill, using whatever model your IDE session runs). Headless `graphify extract` requires `ANTHROPIC_API_KEY` or `MOONSHOT_API_KEY`.
|
|
323
323
|
- No telemetry, no usage tracking, no analytics.
|
|
324
324
|
|
|
325
325
|
---
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "graphifyy"
|
|
7
|
-
version = "0.7.
|
|
7
|
+
version = "0.7.4"
|
|
8
8
|
description = "AI coding assistant skill (Claude Code, Codex, OpenCode, Cursor, Gemini CLI, Aider, OpenClaw, Factory Droid, Trae, Hermes, Kiro, Pi, Google Antigravity) - turn any folder of code, docs, papers, images, or videos into a queryable knowledge graph"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { file = "LICENSE" }
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|