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.
Files changed (74) hide show
  1. {graphifyy-0.7.2/graphifyy.egg-info → graphifyy-0.7.4}/PKG-INFO +2 -2
  2. {graphifyy-0.7.2 → graphifyy-0.7.4}/README.md +1 -1
  3. {graphifyy-0.7.2 → graphifyy-0.7.4}/graphify/__main__.py +233 -0
  4. {graphifyy-0.7.2 → graphifyy-0.7.4}/graphify/extract.py +83 -5
  5. {graphifyy-0.7.2 → graphifyy-0.7.4/graphifyy.egg-info}/PKG-INFO +2 -2
  6. {graphifyy-0.7.2 → graphifyy-0.7.4}/pyproject.toml +1 -1
  7. {graphifyy-0.7.2 → graphifyy-0.7.4}/LICENSE +0 -0
  8. {graphifyy-0.7.2 → graphifyy-0.7.4}/graphify/__init__.py +0 -0
  9. {graphifyy-0.7.2 → graphifyy-0.7.4}/graphify/analyze.py +0 -0
  10. {graphifyy-0.7.2 → graphifyy-0.7.4}/graphify/benchmark.py +0 -0
  11. {graphifyy-0.7.2 → graphifyy-0.7.4}/graphify/build.py +0 -0
  12. {graphifyy-0.7.2 → graphifyy-0.7.4}/graphify/cache.py +0 -0
  13. {graphifyy-0.7.2 → graphifyy-0.7.4}/graphify/cluster.py +0 -0
  14. {graphifyy-0.7.2 → graphifyy-0.7.4}/graphify/detect.py +0 -0
  15. {graphifyy-0.7.2 → graphifyy-0.7.4}/graphify/export.py +0 -0
  16. {graphifyy-0.7.2 → graphifyy-0.7.4}/graphify/hooks.py +0 -0
  17. {graphifyy-0.7.2 → graphifyy-0.7.4}/graphify/ingest.py +0 -0
  18. {graphifyy-0.7.2 → graphifyy-0.7.4}/graphify/llm.py +0 -0
  19. {graphifyy-0.7.2 → graphifyy-0.7.4}/graphify/manifest.py +0 -0
  20. {graphifyy-0.7.2 → graphifyy-0.7.4}/graphify/report.py +0 -0
  21. {graphifyy-0.7.2 → graphifyy-0.7.4}/graphify/security.py +0 -0
  22. {graphifyy-0.7.2 → graphifyy-0.7.4}/graphify/serve.py +0 -0
  23. {graphifyy-0.7.2 → graphifyy-0.7.4}/graphify/skill-aider.md +0 -0
  24. {graphifyy-0.7.2 → graphifyy-0.7.4}/graphify/skill-claw.md +0 -0
  25. {graphifyy-0.7.2 → graphifyy-0.7.4}/graphify/skill-codex.md +0 -0
  26. {graphifyy-0.7.2 → graphifyy-0.7.4}/graphify/skill-copilot.md +0 -0
  27. {graphifyy-0.7.2 → graphifyy-0.7.4}/graphify/skill-droid.md +0 -0
  28. {graphifyy-0.7.2 → graphifyy-0.7.4}/graphify/skill-kiro.md +0 -0
  29. {graphifyy-0.7.2 → graphifyy-0.7.4}/graphify/skill-opencode.md +0 -0
  30. {graphifyy-0.7.2 → graphifyy-0.7.4}/graphify/skill-pi.md +0 -0
  31. {graphifyy-0.7.2 → graphifyy-0.7.4}/graphify/skill-trae.md +0 -0
  32. {graphifyy-0.7.2 → graphifyy-0.7.4}/graphify/skill-vscode.md +0 -0
  33. {graphifyy-0.7.2 → graphifyy-0.7.4}/graphify/skill-windows.md +0 -0
  34. {graphifyy-0.7.2 → graphifyy-0.7.4}/graphify/skill.md +0 -0
  35. {graphifyy-0.7.2 → graphifyy-0.7.4}/graphify/transcribe.py +0 -0
  36. {graphifyy-0.7.2 → graphifyy-0.7.4}/graphify/tree_html.py +0 -0
  37. {graphifyy-0.7.2 → graphifyy-0.7.4}/graphify/validate.py +0 -0
  38. {graphifyy-0.7.2 → graphifyy-0.7.4}/graphify/watch.py +0 -0
  39. {graphifyy-0.7.2 → graphifyy-0.7.4}/graphify/wiki.py +0 -0
  40. {graphifyy-0.7.2 → graphifyy-0.7.4}/graphifyy.egg-info/SOURCES.txt +0 -0
  41. {graphifyy-0.7.2 → graphifyy-0.7.4}/graphifyy.egg-info/dependency_links.txt +0 -0
  42. {graphifyy-0.7.2 → graphifyy-0.7.4}/graphifyy.egg-info/entry_points.txt +0 -0
  43. {graphifyy-0.7.2 → graphifyy-0.7.4}/graphifyy.egg-info/requires.txt +0 -0
  44. {graphifyy-0.7.2 → graphifyy-0.7.4}/graphifyy.egg-info/top_level.txt +0 -0
  45. {graphifyy-0.7.2 → graphifyy-0.7.4}/setup.cfg +0 -0
  46. {graphifyy-0.7.2 → graphifyy-0.7.4}/tests/test_analyze.py +0 -0
  47. {graphifyy-0.7.2 → graphifyy-0.7.4}/tests/test_benchmark.py +0 -0
  48. {graphifyy-0.7.2 → graphifyy-0.7.4}/tests/test_build.py +0 -0
  49. {graphifyy-0.7.2 → graphifyy-0.7.4}/tests/test_cache.py +0 -0
  50. {graphifyy-0.7.2 → graphifyy-0.7.4}/tests/test_chunking.py +0 -0
  51. {graphifyy-0.7.2 → graphifyy-0.7.4}/tests/test_claude_md.py +0 -0
  52. {graphifyy-0.7.2 → graphifyy-0.7.4}/tests/test_cli_export.py +0 -0
  53. {graphifyy-0.7.2 → graphifyy-0.7.4}/tests/test_cluster.py +0 -0
  54. {graphifyy-0.7.2 → graphifyy-0.7.4}/tests/test_confidence.py +0 -0
  55. {graphifyy-0.7.2 → graphifyy-0.7.4}/tests/test_detect.py +0 -0
  56. {graphifyy-0.7.2 → graphifyy-0.7.4}/tests/test_export.py +0 -0
  57. {graphifyy-0.7.2 → graphifyy-0.7.4}/tests/test_extract.py +0 -0
  58. {graphifyy-0.7.2 → graphifyy-0.7.4}/tests/test_hooks.py +0 -0
  59. {graphifyy-0.7.2 → graphifyy-0.7.4}/tests/test_hypergraph.py +0 -0
  60. {graphifyy-0.7.2 → graphifyy-0.7.4}/tests/test_ingest.py +0 -0
  61. {graphifyy-0.7.2 → graphifyy-0.7.4}/tests/test_install.py +0 -0
  62. {graphifyy-0.7.2 → graphifyy-0.7.4}/tests/test_languages.py +0 -0
  63. {graphifyy-0.7.2 → graphifyy-0.7.4}/tests/test_multilang.py +0 -0
  64. {graphifyy-0.7.2 → graphifyy-0.7.4}/tests/test_pipeline.py +0 -0
  65. {graphifyy-0.7.2 → graphifyy-0.7.4}/tests/test_query_cli.py +0 -0
  66. {graphifyy-0.7.2 → graphifyy-0.7.4}/tests/test_rationale.py +0 -0
  67. {graphifyy-0.7.2 → graphifyy-0.7.4}/tests/test_report.py +0 -0
  68. {graphifyy-0.7.2 → graphifyy-0.7.4}/tests/test_security.py +0 -0
  69. {graphifyy-0.7.2 → graphifyy-0.7.4}/tests/test_semantic_similarity.py +0 -0
  70. {graphifyy-0.7.2 → graphifyy-0.7.4}/tests/test_serve.py +0 -0
  71. {graphifyy-0.7.2 → graphifyy-0.7.4}/tests/test_transcribe.py +0 -0
  72. {graphifyy-0.7.2 → graphifyy-0.7.4}/tests/test_validate.py +0 -0
  73. {graphifyy-0.7.2 → graphifyy-0.7.4}/tests/test_watch.py +0 -0
  74. {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.2
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's model API (Anthropic, OpenAI, etc.) using your own API key.
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's model API (Anthropic, OpenAI, etc.) using your own API key.
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
- data = json.loads(tsconfig.read_text(encoding="utf-8"))
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
- file_node_id = _make_id(path.stem, str(path))
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.startswith("."):
1759
+ if not raw:
1715
1760
  continue
1716
- node_id = _make_id(raw, str(path))
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.2
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's model API (Anthropic, OpenAI, etc.) using your own API key.
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.2"
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