graphifyy 0.3.4__tar.gz → 0.3.6__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 (60) hide show
  1. {graphifyy-0.3.4 → graphifyy-0.3.6}/PKG-INFO +8 -1
  2. {graphifyy-0.3.4 → graphifyy-0.3.6}/README.md +1 -0
  3. {graphifyy-0.3.4 → graphifyy-0.3.6}/graphify/detect.py +125 -2
  4. {graphifyy-0.3.4 → graphifyy-0.3.6}/graphify/extract.py +179 -0
  5. {graphifyy-0.3.4 → graphifyy-0.3.6}/graphifyy.egg-info/PKG-INFO +8 -1
  6. {graphifyy-0.3.4 → graphifyy-0.3.6}/graphifyy.egg-info/requires.txt +7 -0
  7. {graphifyy-0.3.4 → graphifyy-0.3.6}/pyproject.toml +4 -2
  8. {graphifyy-0.3.4 → graphifyy-0.3.6}/tests/test_extract.py +1 -1
  9. {graphifyy-0.3.4 → graphifyy-0.3.6}/tests/test_languages.py +34 -0
  10. {graphifyy-0.3.4 → graphifyy-0.3.6}/LICENSE +0 -0
  11. {graphifyy-0.3.4 → graphifyy-0.3.6}/graphify/__init__.py +0 -0
  12. {graphifyy-0.3.4 → graphifyy-0.3.6}/graphify/__main__.py +0 -0
  13. {graphifyy-0.3.4 → graphifyy-0.3.6}/graphify/analyze.py +0 -0
  14. {graphifyy-0.3.4 → graphifyy-0.3.6}/graphify/benchmark.py +0 -0
  15. {graphifyy-0.3.4 → graphifyy-0.3.6}/graphify/build.py +0 -0
  16. {graphifyy-0.3.4 → graphifyy-0.3.6}/graphify/cache.py +0 -0
  17. {graphifyy-0.3.4 → graphifyy-0.3.6}/graphify/cluster.py +0 -0
  18. {graphifyy-0.3.4 → graphifyy-0.3.6}/graphify/export.py +0 -0
  19. {graphifyy-0.3.4 → graphifyy-0.3.6}/graphify/hooks.py +0 -0
  20. {graphifyy-0.3.4 → graphifyy-0.3.6}/graphify/ingest.py +0 -0
  21. {graphifyy-0.3.4 → graphifyy-0.3.6}/graphify/manifest.py +0 -0
  22. {graphifyy-0.3.4 → graphifyy-0.3.6}/graphify/report.py +0 -0
  23. {graphifyy-0.3.4 → graphifyy-0.3.6}/graphify/security.py +0 -0
  24. {graphifyy-0.3.4 → graphifyy-0.3.6}/graphify/serve.py +0 -0
  25. {graphifyy-0.3.4 → graphifyy-0.3.6}/graphify/skill-claw.md +0 -0
  26. {graphifyy-0.3.4 → graphifyy-0.3.6}/graphify/skill-codex.md +0 -0
  27. {graphifyy-0.3.4 → graphifyy-0.3.6}/graphify/skill-opencode.md +0 -0
  28. {graphifyy-0.3.4 → graphifyy-0.3.6}/graphify/skill-windows.md +0 -0
  29. {graphifyy-0.3.4 → graphifyy-0.3.6}/graphify/skill.md +0 -0
  30. {graphifyy-0.3.4 → graphifyy-0.3.6}/graphify/validate.py +0 -0
  31. {graphifyy-0.3.4 → graphifyy-0.3.6}/graphify/watch.py +0 -0
  32. {graphifyy-0.3.4 → graphifyy-0.3.6}/graphify/wiki.py +0 -0
  33. {graphifyy-0.3.4 → graphifyy-0.3.6}/graphifyy.egg-info/SOURCES.txt +0 -0
  34. {graphifyy-0.3.4 → graphifyy-0.3.6}/graphifyy.egg-info/dependency_links.txt +0 -0
  35. {graphifyy-0.3.4 → graphifyy-0.3.6}/graphifyy.egg-info/entry_points.txt +0 -0
  36. {graphifyy-0.3.4 → graphifyy-0.3.6}/graphifyy.egg-info/top_level.txt +0 -0
  37. {graphifyy-0.3.4 → graphifyy-0.3.6}/setup.cfg +0 -0
  38. {graphifyy-0.3.4 → graphifyy-0.3.6}/tests/test_analyze.py +0 -0
  39. {graphifyy-0.3.4 → graphifyy-0.3.6}/tests/test_benchmark.py +0 -0
  40. {graphifyy-0.3.4 → graphifyy-0.3.6}/tests/test_build.py +0 -0
  41. {graphifyy-0.3.4 → graphifyy-0.3.6}/tests/test_cache.py +0 -0
  42. {graphifyy-0.3.4 → graphifyy-0.3.6}/tests/test_claude_md.py +0 -0
  43. {graphifyy-0.3.4 → graphifyy-0.3.6}/tests/test_cluster.py +0 -0
  44. {graphifyy-0.3.4 → graphifyy-0.3.6}/tests/test_confidence.py +0 -0
  45. {graphifyy-0.3.4 → graphifyy-0.3.6}/tests/test_detect.py +0 -0
  46. {graphifyy-0.3.4 → graphifyy-0.3.6}/tests/test_export.py +0 -0
  47. {graphifyy-0.3.4 → graphifyy-0.3.6}/tests/test_hooks.py +0 -0
  48. {graphifyy-0.3.4 → graphifyy-0.3.6}/tests/test_hypergraph.py +0 -0
  49. {graphifyy-0.3.4 → graphifyy-0.3.6}/tests/test_ingest.py +0 -0
  50. {graphifyy-0.3.4 → graphifyy-0.3.6}/tests/test_install.py +0 -0
  51. {graphifyy-0.3.4 → graphifyy-0.3.6}/tests/test_multilang.py +0 -0
  52. {graphifyy-0.3.4 → graphifyy-0.3.6}/tests/test_pipeline.py +0 -0
  53. {graphifyy-0.3.4 → graphifyy-0.3.6}/tests/test_rationale.py +0 -0
  54. {graphifyy-0.3.4 → graphifyy-0.3.6}/tests/test_report.py +0 -0
  55. {graphifyy-0.3.4 → graphifyy-0.3.6}/tests/test_security.py +0 -0
  56. {graphifyy-0.3.4 → graphifyy-0.3.6}/tests/test_semantic_similarity.py +0 -0
  57. {graphifyy-0.3.4 → graphifyy-0.3.6}/tests/test_serve.py +0 -0
  58. {graphifyy-0.3.4 → graphifyy-0.3.6}/tests/test_validate.py +0 -0
  59. {graphifyy-0.3.4 → graphifyy-0.3.6}/tests/test_watch.py +0 -0
  60. {graphifyy-0.3.4 → graphifyy-0.3.6}/tests/test_wiki.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: graphifyy
3
- Version: 0.3.4
3
+ Version: 0.3.6
4
4
  Summary: AI coding assistant skill (Claude Code, Codex, OpenCode, OpenClaw) - turn any folder of code, docs, papers, or images into a queryable knowledge graph
5
5
  License: MIT License
6
6
 
@@ -50,6 +50,7 @@ Requires-Dist: tree-sitter-swift
50
50
  Requires-Dist: tree-sitter-lua
51
51
  Requires-Dist: tree-sitter-zig
52
52
  Requires-Dist: tree-sitter-powershell
53
+ Requires-Dist: tree-sitter-elixir
53
54
  Provides-Extra: mcp
54
55
  Requires-Dist: mcp; extra == "mcp"
55
56
  Provides-Extra: neo4j
@@ -61,6 +62,9 @@ Provides-Extra: watch
61
62
  Requires-Dist: watchdog; extra == "watch"
62
63
  Provides-Extra: leiden
63
64
  Requires-Dist: graspologic; extra == "leiden"
65
+ Provides-Extra: office
66
+ Requires-Dist: python-docx; extra == "office"
67
+ Requires-Dist: openpyxl; extra == "office"
64
68
  Provides-Extra: all
65
69
  Requires-Dist: mcp; extra == "all"
66
70
  Requires-Dist: neo4j; extra == "all"
@@ -68,6 +72,8 @@ Requires-Dist: pypdf; extra == "all"
68
72
  Requires-Dist: html2text; extra == "all"
69
73
  Requires-Dist: watchdog; extra == "all"
70
74
  Requires-Dist: graspologic; extra == "all"
75
+ Requires-Dist: python-docx; extra == "all"
76
+ Requires-Dist: openpyxl; extra == "all"
71
77
  Dynamic: license-file
72
78
 
73
79
  # graphify
@@ -239,6 +245,7 @@ Works with any mix of file types:
239
245
  |------|-----------|------------|
240
246
  | Code | `.py .ts .js .go .rs .java .c .cpp .rb .cs .kt .scala .php .swift .lua .zig .ps1` | AST via tree-sitter + call-graph + docstring/comment rationale |
241
247
  | Docs | `.md .txt .rst` | Concepts + relationships + design rationale via Claude |
248
+ | Office | `.docx .xlsx` | Converted to markdown then extracted via Claude (requires `pip install graphifyy[office]`) |
242
249
  | Papers | `.pdf` | Citation mining + concept extraction |
243
250
  | Images | `.png .jpg .webp .gif` | Claude vision - screenshots, diagrams, any language |
244
251
 
@@ -167,6 +167,7 @@ Works with any mix of file types:
167
167
  |------|-----------|------------|
168
168
  | Code | `.py .ts .js .go .rs .java .c .cpp .rb .cs .kt .scala .php .swift .lua .zig .ps1` | AST via tree-sitter + call-graph + docstring/comment rationale |
169
169
  | Docs | `.md .txt .rst` | Concepts + relationships + design rationale via Claude |
170
+ | Office | `.docx .xlsx` | Converted to markdown then extracted via Claude (requires `pip install graphifyy[office]`) |
170
171
  | Papers | `.pdf` | Citation mining + concept extraction |
171
172
  | Images | `.png .jpg .webp .gif` | Claude vision - screenshots, diagrams, any language |
172
173
 
@@ -17,10 +17,11 @@ class FileType(str, Enum):
17
17
 
18
18
  _MANIFEST_PATH = "graphify-out/manifest.json"
19
19
 
20
- CODE_EXTENSIONS = {'.py', '.ts', '.js', '.tsx', '.go', '.rs', '.java', '.cpp', '.cc', '.cxx', '.c', '.h', '.hpp', '.rb', '.swift', '.kt', '.kts', '.cs', '.scala', '.php', '.lua', '.toc', '.zig', '.ps1'}
20
+ CODE_EXTENSIONS = {'.py', '.ts', '.js', '.tsx', '.go', '.rs', '.java', '.cpp', '.cc', '.cxx', '.c', '.h', '.hpp', '.rb', '.swift', '.kt', '.kts', '.cs', '.scala', '.php', '.lua', '.toc', '.zig', '.ps1', '.ex', '.exs'}
21
21
  DOC_EXTENSIONS = {'.md', '.txt', '.rst'}
22
22
  PAPER_EXTENSIONS = {'.pdf'}
23
23
  IMAGE_EXTENSIONS = {'.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg'}
24
+ OFFICE_EXTENSIONS = {'.docx', '.xlsx'}
24
25
 
25
26
  CORPUS_WARN_THRESHOLD = 50_000 # words - below this, warn "you may not need a graph"
26
27
  CORPUS_UPPER_THRESHOLD = 500_000 # words - above this, warn about token cost
@@ -86,6 +87,8 @@ def classify_file(path: Path) -> FileType | None:
86
87
  if _looks_like_paper(path):
87
88
  return FileType.PAPER
88
89
  return FileType.DOCUMENT
90
+ if ext in OFFICE_EXTENSIONS:
91
+ return FileType.DOCUMENT
89
92
  return None
90
93
 
91
94
 
@@ -104,10 +107,115 @@ def extract_pdf_text(path: Path) -> str:
104
107
  return ""
105
108
 
106
109
 
110
+ def docx_to_markdown(path: Path) -> str:
111
+ """Convert a .docx file to markdown text using python-docx."""
112
+ try:
113
+ from docx import Document
114
+ from docx.oxml.ns import qn
115
+ doc = Document(str(path))
116
+ lines = []
117
+ for para in doc.paragraphs:
118
+ style = para.style.name if para.style else ""
119
+ text = para.text.strip()
120
+ if not text:
121
+ lines.append("")
122
+ continue
123
+ if style.startswith("Heading 1"):
124
+ lines.append(f"# {text}")
125
+ elif style.startswith("Heading 2"):
126
+ lines.append(f"## {text}")
127
+ elif style.startswith("Heading 3"):
128
+ lines.append(f"### {text}")
129
+ elif style.startswith("List"):
130
+ lines.append(f"- {text}")
131
+ else:
132
+ lines.append(text)
133
+ # Tables
134
+ for table in doc.tables:
135
+ rows = [[cell.text.strip() for cell in row.cells] for row in table.rows]
136
+ if not rows:
137
+ continue
138
+ header = "| " + " | ".join(rows[0]) + " |"
139
+ sep = "| " + " | ".join("---" for _ in rows[0]) + " |"
140
+ lines.extend([header, sep])
141
+ for row in rows[1:]:
142
+ lines.append("| " + " | ".join(row) + " |")
143
+ return "\n".join(lines)
144
+ except ImportError:
145
+ return ""
146
+ except Exception:
147
+ return ""
148
+
149
+
150
+ def xlsx_to_markdown(path: Path) -> str:
151
+ """Convert an .xlsx file to markdown text using openpyxl."""
152
+ try:
153
+ import openpyxl
154
+ wb = openpyxl.load_workbook(str(path), read_only=True, data_only=True)
155
+ sections = []
156
+ for sheet_name in wb.sheetnames:
157
+ ws = wb[sheet_name]
158
+ rows = []
159
+ for row in ws.iter_rows(values_only=True):
160
+ # Skip entirely empty rows
161
+ if all(cell is None for cell in row):
162
+ continue
163
+ rows.append([str(cell) if cell is not None else "" for cell in row])
164
+ if not rows:
165
+ continue
166
+ sections.append(f"## Sheet: {sheet_name}")
167
+ if len(rows) >= 1:
168
+ header = "| " + " | ".join(rows[0]) + " |"
169
+ sep = "| " + " | ".join("---" for _ in rows[0]) + " |"
170
+ sections.extend([header, sep])
171
+ for row in rows[1:]:
172
+ sections.append("| " + " | ".join(row) + " |")
173
+ wb.close()
174
+ return "\n".join(sections)
175
+ except ImportError:
176
+ return ""
177
+ except Exception:
178
+ return ""
179
+
180
+
181
+ def convert_office_file(path: Path, out_dir: Path) -> Path | None:
182
+ """Convert a .docx or .xlsx to a markdown sidecar in out_dir.
183
+
184
+ Returns the path of the converted .md file, or None if conversion failed
185
+ or the required library is not installed.
186
+ """
187
+ ext = path.suffix.lower()
188
+ if ext == ".docx":
189
+ text = docx_to_markdown(path)
190
+ elif ext == ".xlsx":
191
+ text = xlsx_to_markdown(path)
192
+ else:
193
+ return None
194
+
195
+ if not text.strip():
196
+ return None
197
+
198
+ out_dir.mkdir(parents=True, exist_ok=True)
199
+ # Use a stable name derived from the original path to avoid collisions
200
+ import hashlib
201
+ name_hash = hashlib.sha256(str(path.resolve()).encode()).hexdigest()[:8]
202
+ out_path = out_dir / f"{path.stem}_{name_hash}.md"
203
+ out_path.write_text(
204
+ f"<!-- converted from {path.name} -->\n\n{text}",
205
+ encoding="utf-8",
206
+ )
207
+ return out_path
208
+
209
+
107
210
  def count_words(path: Path) -> int:
108
211
  try:
109
- if path.suffix.lower() == ".pdf":
212
+ ext = path.suffix.lower()
213
+ if ext == ".pdf":
110
214
  return len(extract_pdf_text(path).split())
215
+ if ext == ".docx":
216
+ return len(docx_to_markdown(path).split())
217
+ if ext == ".xlsx":
218
+ return len(xlsx_to_markdown(path).split())
111
219
  return len(path.read_text(errors="ignore").split())
112
220
  except Exception:
113
221
  return 0
@@ -224,6 +332,8 @@ def detect(root: Path) -> dict:
224
332
  seen.add(p)
225
333
  all_files.append(p)
226
334
 
335
+ converted_dir = root / "graphify-out" / "converted"
336
+
227
337
  for p in all_files:
228
338
  # For memory dir files, skip hidden/noise filtering
229
339
  in_memory = memory_dir.exists() and str(p).startswith(str(memory_dir))
@@ -232,6 +342,9 @@ def detect(root: Path) -> dict:
232
342
  # but catch hidden files at the root level
233
343
  if p.name.startswith("."):
234
344
  continue
345
+ # Skip files inside our own converted/ dir (avoid re-processing sidecars)
346
+ if str(p).startswith(str(converted_dir)):
347
+ continue
235
348
  if _is_ignored(p, root, ignore_patterns):
236
349
  continue
237
350
  if _is_sensitive(p):
@@ -239,6 +352,16 @@ def detect(root: Path) -> dict:
239
352
  continue
240
353
  ftype = classify_file(p)
241
354
  if ftype:
355
+ # Office files: convert to markdown sidecar so subagents can read them
356
+ if p.suffix.lower() in OFFICE_EXTENSIONS:
357
+ md_path = convert_office_file(p, converted_dir)
358
+ if md_path:
359
+ files[ftype].append(str(md_path))
360
+ total_words += count_words(md_path)
361
+ else:
362
+ # Conversion failed (library not installed) - skip with note
363
+ skipped_sensitive.append(str(p) + " [office conversion failed - pip install graphifyy[office]]")
364
+ continue
242
365
  files[ftype].append(str(p))
243
366
  total_words += count_words(p)
244
367
 
@@ -1928,6 +1928,183 @@ def _resolve_cross_file_imports(
1928
1928
  return new_edges
1929
1929
 
1930
1930
 
1931
+ def extract_elixir(path: Path) -> dict:
1932
+ """Extract modules, functions, imports, and calls from a .ex/.exs file."""
1933
+ try:
1934
+ import tree_sitter_elixir as tselixir
1935
+ from tree_sitter import Language, Parser
1936
+ except ImportError:
1937
+ return {"nodes": [], "edges": [], "error": "tree_sitter_elixir not installed"}
1938
+
1939
+ try:
1940
+ language = Language(tselixir.language())
1941
+ parser = Parser(language)
1942
+ source = path.read_bytes()
1943
+ tree = parser.parse(source)
1944
+ root = tree.root_node
1945
+ except Exception as e:
1946
+ return {"nodes": [], "edges": [], "error": str(e)}
1947
+
1948
+ stem = path.stem
1949
+ str_path = str(path)
1950
+ nodes: list[dict] = []
1951
+ edges: list[dict] = []
1952
+ seen_ids: set[str] = set()
1953
+ function_bodies: list[tuple[str, Any]] = []
1954
+
1955
+ def add_node(nid: str, label: str, line: int) -> None:
1956
+ if nid not in seen_ids:
1957
+ seen_ids.add(nid)
1958
+ nodes.append({"id": nid, "label": label, "file_type": "code",
1959
+ "source_file": str_path, "source_location": f"L{line}"})
1960
+
1961
+ def add_edge(src: str, tgt: str, relation: str, line: int,
1962
+ confidence: str = "EXTRACTED", weight: float = 1.0) -> None:
1963
+ edges.append({"source": src, "target": tgt, "relation": relation,
1964
+ "confidence": confidence, "source_file": str_path,
1965
+ "source_location": f"L{line}", "weight": weight})
1966
+
1967
+ file_nid = _make_id(stem)
1968
+ add_node(file_nid, path.name, 1)
1969
+
1970
+ _IMPORT_KEYWORDS = frozenset({"alias", "import", "require", "use"})
1971
+
1972
+ def _get_alias_text(node) -> str | None:
1973
+ for child in node.children:
1974
+ if child.type == "alias":
1975
+ return source[child.start_byte:child.end_byte].decode("utf-8", errors="replace")
1976
+ return None
1977
+
1978
+ def walk(node, parent_module_nid: str | None = None) -> None:
1979
+ if node.type != "call":
1980
+ for child in node.children:
1981
+ walk(child, parent_module_nid)
1982
+ return
1983
+
1984
+ identifier_node = None
1985
+ arguments_node = None
1986
+ do_block_node = None
1987
+ for child in node.children:
1988
+ if child.type == "identifier":
1989
+ identifier_node = child
1990
+ elif child.type == "arguments":
1991
+ arguments_node = child
1992
+ elif child.type == "do_block":
1993
+ do_block_node = child
1994
+
1995
+ if identifier_node is None:
1996
+ for child in node.children:
1997
+ walk(child, parent_module_nid)
1998
+ return
1999
+
2000
+ keyword = source[identifier_node.start_byte:identifier_node.end_byte].decode("utf-8", errors="replace")
2001
+ line = node.start_point[0] + 1
2002
+
2003
+ if keyword == "defmodule":
2004
+ module_name = _get_alias_text(arguments_node) if arguments_node else None
2005
+ if not module_name:
2006
+ return
2007
+ module_nid = _make_id(stem, module_name)
2008
+ add_node(module_nid, module_name, line)
2009
+ add_edge(file_nid, module_nid, "contains", line)
2010
+ if do_block_node:
2011
+ for child in do_block_node.children:
2012
+ walk(child, parent_module_nid=module_nid)
2013
+ return
2014
+
2015
+ if keyword in ("def", "defp"):
2016
+ func_name = None
2017
+ if arguments_node:
2018
+ for child in arguments_node.children:
2019
+ if child.type == "call":
2020
+ for sub in child.children:
2021
+ if sub.type == "identifier":
2022
+ func_name = source[sub.start_byte:sub.end_byte].decode("utf-8", errors="replace")
2023
+ break
2024
+ elif child.type == "identifier":
2025
+ func_name = source[child.start_byte:child.end_byte].decode("utf-8", errors="replace")
2026
+ break
2027
+ if not func_name:
2028
+ return
2029
+ container = parent_module_nid or file_nid
2030
+ func_nid = _make_id(container, func_name)
2031
+ add_node(func_nid, f"{func_name}()", line)
2032
+ if parent_module_nid:
2033
+ add_edge(parent_module_nid, func_nid, "method", line)
2034
+ else:
2035
+ add_edge(file_nid, func_nid, "contains", line)
2036
+ if do_block_node:
2037
+ function_bodies.append((func_nid, do_block_node))
2038
+ return
2039
+
2040
+ if keyword in _IMPORT_KEYWORDS and arguments_node:
2041
+ module_name = _get_alias_text(arguments_node)
2042
+ if module_name:
2043
+ tgt_nid = _make_id(module_name)
2044
+ add_edge(file_nid, tgt_nid, "imports", line)
2045
+ return
2046
+
2047
+ for child in node.children:
2048
+ walk(child, parent_module_nid)
2049
+
2050
+ walk(root)
2051
+
2052
+ label_to_nid: dict[str, str] = {}
2053
+ for n in nodes:
2054
+ normalised = n["label"].strip("()").lstrip(".")
2055
+ label_to_nid[normalised.lower()] = n["id"]
2056
+
2057
+ seen_call_pairs: set[tuple[str, str]] = set()
2058
+ _SKIP_KEYWORDS = frozenset({
2059
+ "def", "defp", "defmodule", "defmacro", "defmacrop",
2060
+ "defstruct", "defprotocol", "defimpl", "defguard",
2061
+ "alias", "import", "require", "use",
2062
+ "if", "unless", "case", "cond", "with", "for",
2063
+ })
2064
+
2065
+ def walk_calls(node, caller_nid: str) -> None:
2066
+ if node.type != "call":
2067
+ for child in node.children:
2068
+ walk_calls(child, caller_nid)
2069
+ return
2070
+ for child in node.children:
2071
+ if child.type == "identifier":
2072
+ kw = source[child.start_byte:child.end_byte].decode("utf-8", errors="replace")
2073
+ if kw in _SKIP_KEYWORDS:
2074
+ for c in node.children:
2075
+ walk_calls(c, caller_nid)
2076
+ return
2077
+ break
2078
+ callee_name: str | None = None
2079
+ for child in node.children:
2080
+ if child.type == "dot":
2081
+ dot_text = source[child.start_byte:child.end_byte].decode("utf-8", errors="replace")
2082
+ parts = dot_text.rstrip(".").split(".")
2083
+ if parts:
2084
+ callee_name = parts[-1]
2085
+ break
2086
+ if child.type == "identifier":
2087
+ callee_name = source[child.start_byte:child.end_byte].decode("utf-8", errors="replace")
2088
+ break
2089
+ if callee_name:
2090
+ tgt_nid = label_to_nid.get(callee_name.lower())
2091
+ if tgt_nid and tgt_nid != caller_nid:
2092
+ pair = (caller_nid, tgt_nid)
2093
+ if pair not in seen_call_pairs:
2094
+ seen_call_pairs.add(pair)
2095
+ add_edge(caller_nid, tgt_nid, "calls",
2096
+ node.start_point[0] + 1, confidence="INFERRED", weight=0.8)
2097
+ for child in node.children:
2098
+ walk_calls(child, caller_nid)
2099
+
2100
+ for caller_nid, body in function_bodies:
2101
+ walk_calls(body, caller_nid)
2102
+
2103
+ clean_edges = [e for e in edges if e["source"] in seen_ids and
2104
+ (e["target"] in seen_ids or e["relation"] == "imports")]
2105
+ return {"nodes": nodes, "edges": clean_edges, "input_tokens": 0, "output_tokens": 0}
2106
+
2107
+
1931
2108
  # ── Main extract and collect_files ────────────────────────────────────────────
1932
2109
 
1933
2110
  def extract(paths: list[Path]) -> dict:
@@ -1980,6 +2157,8 @@ def extract(paths: list[Path]) -> dict:
1980
2157
  ".toc": extract_lua,
1981
2158
  ".zig": extract_zig,
1982
2159
  ".ps1": extract_powershell,
2160
+ ".ex": extract_elixir,
2161
+ ".exs": extract_elixir,
1983
2162
  }
1984
2163
 
1985
2164
  for path in paths:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: graphifyy
3
- Version: 0.3.4
3
+ Version: 0.3.6
4
4
  Summary: AI coding assistant skill (Claude Code, Codex, OpenCode, OpenClaw) - turn any folder of code, docs, papers, or images into a queryable knowledge graph
5
5
  License: MIT License
6
6
 
@@ -50,6 +50,7 @@ Requires-Dist: tree-sitter-swift
50
50
  Requires-Dist: tree-sitter-lua
51
51
  Requires-Dist: tree-sitter-zig
52
52
  Requires-Dist: tree-sitter-powershell
53
+ Requires-Dist: tree-sitter-elixir
53
54
  Provides-Extra: mcp
54
55
  Requires-Dist: mcp; extra == "mcp"
55
56
  Provides-Extra: neo4j
@@ -61,6 +62,9 @@ Provides-Extra: watch
61
62
  Requires-Dist: watchdog; extra == "watch"
62
63
  Provides-Extra: leiden
63
64
  Requires-Dist: graspologic; extra == "leiden"
65
+ Provides-Extra: office
66
+ Requires-Dist: python-docx; extra == "office"
67
+ Requires-Dist: openpyxl; extra == "office"
64
68
  Provides-Extra: all
65
69
  Requires-Dist: mcp; extra == "all"
66
70
  Requires-Dist: neo4j; extra == "all"
@@ -68,6 +72,8 @@ Requires-Dist: pypdf; extra == "all"
68
72
  Requires-Dist: html2text; extra == "all"
69
73
  Requires-Dist: watchdog; extra == "all"
70
74
  Requires-Dist: graspologic; extra == "all"
75
+ Requires-Dist: python-docx; extra == "all"
76
+ Requires-Dist: openpyxl; extra == "all"
71
77
  Dynamic: license-file
72
78
 
73
79
  # graphify
@@ -239,6 +245,7 @@ Works with any mix of file types:
239
245
  |------|-----------|------------|
240
246
  | Code | `.py .ts .js .go .rs .java .c .cpp .rb .cs .kt .scala .php .swift .lua .zig .ps1` | AST via tree-sitter + call-graph + docstring/comment rationale |
241
247
  | Docs | `.md .txt .rst` | Concepts + relationships + design rationale via Claude |
248
+ | Office | `.docx .xlsx` | Converted to markdown then extracted via Claude (requires `pip install graphifyy[office]`) |
242
249
  | Papers | `.pdf` | Citation mining + concept extraction |
243
250
  | Images | `.png .jpg .webp .gif` | Claude vision - screenshots, diagrams, any language |
244
251
 
@@ -17,6 +17,7 @@ tree-sitter-swift
17
17
  tree-sitter-lua
18
18
  tree-sitter-zig
19
19
  tree-sitter-powershell
20
+ tree-sitter-elixir
20
21
 
21
22
  [all]
22
23
  mcp
@@ -25,6 +26,8 @@ pypdf
25
26
  html2text
26
27
  watchdog
27
28
  graspologic
29
+ python-docx
30
+ openpyxl
28
31
 
29
32
  [leiden]
30
33
  graspologic
@@ -35,6 +38,10 @@ mcp
35
38
  [neo4j]
36
39
  neo4j
37
40
 
41
+ [office]
42
+ python-docx
43
+ openpyxl
44
+
38
45
  [pdf]
39
46
  pypdf
40
47
  html2text
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "graphifyy"
7
- version = "0.3.4"
7
+ version = "0.3.6"
8
8
  description = "AI coding assistant skill (Claude Code, Codex, OpenCode, OpenClaw) - turn any folder of code, docs, papers, or images into a queryable knowledge graph"
9
9
  readme = "README.md"
10
10
  license = { file = "LICENSE" }
@@ -30,6 +30,7 @@ dependencies = [
30
30
  "tree-sitter-lua",
31
31
  "tree-sitter-zig",
32
32
  "tree-sitter-powershell",
33
+ "tree-sitter-elixir",
33
34
  ]
34
35
 
35
36
  [project.urls]
@@ -43,7 +44,8 @@ neo4j = ["neo4j"]
43
44
  pdf = ["pypdf", "html2text"]
44
45
  watch = ["watchdog"]
45
46
  leiden = ["graspologic"]
46
- all = ["mcp", "neo4j", "pypdf", "html2text", "watchdog", "graspologic"]
47
+ office = ["python-docx", "openpyxl"]
48
+ all = ["mcp", "neo4j", "pypdf", "html2text", "watchdog", "graspologic", "python-docx", "openpyxl"]
47
49
 
48
50
  [project.scripts]
49
51
  graphify = "graphify.__main__:main"
@@ -61,7 +61,7 @@ def test_collect_files_from_dir():
61
61
  supported = {".py", ".js", ".ts", ".tsx", ".go", ".rs",
62
62
  ".java", ".c", ".cpp", ".cc", ".cxx", ".rb",
63
63
  ".cs", ".kt", ".kts", ".scala", ".php", ".h", ".hpp",
64
- ".swift", ".lua", ".toc", ".zig", ".ps1"}
64
+ ".swift", ".lua", ".toc", ".zig", ".ps1", ".ex", ".exs"}
65
65
  assert all(f.suffix in supported for f in files)
66
66
  assert len(files) > 0
67
67
 
@@ -337,3 +337,37 @@ def test_swift_emits_calls():
337
337
  r = extract_swift(FIXTURES / "sample.swift")
338
338
  calls = _calls(r)
339
339
  assert any("process" in src and "validate" in tgt for src, tgt in calls)
340
+
341
+
342
+ # ── Elixir ────────────────────────────────────────────────────────────────────
343
+
344
+ from graphify.extract import extract_elixir
345
+
346
+ def test_elixir_finds_module():
347
+ r = extract_elixir(FIXTURES / "sample.ex")
348
+ assert "error" not in r
349
+ labels = [n["label"] for n in r["nodes"]]
350
+ assert any("MyApp.Accounts.User" in l for l in labels)
351
+
352
+ def test_elixir_finds_functions():
353
+ r = extract_elixir(FIXTURES / "sample.ex")
354
+ labels = [n["label"] for n in r["nodes"]]
355
+ assert any("create" in l for l in labels)
356
+ assert any("find" in l for l in labels)
357
+ assert any("validate" in l for l in labels)
358
+
359
+ def test_elixir_finds_imports():
360
+ r = extract_elixir(FIXTURES / "sample.ex")
361
+ import_edges = [e for e in r["edges"] if e["relation"] == "imports"]
362
+ assert len(import_edges) >= 2
363
+
364
+ def test_elixir_finds_calls():
365
+ r = extract_elixir(FIXTURES / "sample.ex")
366
+ calls = {(e["source"], e["target"]) for e in r["edges"] if e["relation"] == "calls"}
367
+ labels = {n["id"]: n["label"] for n in r["nodes"]}
368
+ assert any("create" in labels.get(src, "") and "validate" in labels.get(tgt, "") for src, tgt in calls)
369
+
370
+ def test_elixir_method_edges():
371
+ r = extract_elixir(FIXTURES / "sample.ex")
372
+ methods = [e for e in r["edges"] if e["relation"] == "method"]
373
+ assert len(methods) >= 3
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