imhcode 1.0.0

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 (986) hide show
  1. package/.github/ISSUE_TEMPLATE/bug_report.md +41 -0
  2. package/.github/ISSUE_TEMPLATE/feature_request.md +39 -0
  3. package/.github/PULL_REQUEST_TEMPLATE.md +29 -0
  4. package/.github/copilot-instructions.md +43 -0
  5. package/.github/workflows/framework-validator.yml +53 -0
  6. package/.gitignore.template +15 -0
  7. package/AGENTS.md +69 -0
  8. package/CLAUDE.md +27 -0
  9. package/README.md +63 -0
  10. package/SETUP.md +81 -0
  11. package/USER_MANUAL.md +70 -0
  12. package/agents/android-executor/SYSTEM.md +32 -0
  13. package/agents/android-executor/agent.yml +53 -0
  14. package/agents/debugger/SYSTEM.md +32 -0
  15. package/agents/debugger/agent.yml +56 -0
  16. package/agents/designer/SYSTEM.md +32 -0
  17. package/agents/designer/agent.yml +59 -0
  18. package/agents/devops-executor/SYSTEM.md +32 -0
  19. package/agents/devops-executor/agent.yml +56 -0
  20. package/agents/flutter-executor/SYSTEM.md +32 -0
  21. package/agents/flutter-executor/agent.yml +54 -0
  22. package/agents/ios-executor/SYSTEM.md +32 -0
  23. package/agents/ios-executor/agent.yml +53 -0
  24. package/agents/java-executor/SYSTEM.md +32 -0
  25. package/agents/java-executor/agent.yml +57 -0
  26. package/agents/laravel-executor/SYSTEM.md +32 -0
  27. package/agents/laravel-executor/agent.yml +59 -0
  28. package/agents/nextjs-executor/SYSTEM.md +32 -0
  29. package/agents/nextjs-executor/agent.yml +60 -0
  30. package/agents/planner/SYSTEM.md +32 -0
  31. package/agents/planner/agent.yml +56 -0
  32. package/agents/python-executor/SYSTEM.md +32 -0
  33. package/agents/python-executor/agent.yml +58 -0
  34. package/agents/react-executor/SYSTEM.md +32 -0
  35. package/agents/react-executor/agent.yml +58 -0
  36. package/agents/react-native-executor/SYSTEM.md +32 -0
  37. package/agents/react-native-executor/agent.yml +53 -0
  38. package/agents/security-auditor/SYSTEM.md +32 -0
  39. package/agents/security-auditor/agent.yml +56 -0
  40. package/agents/seo-optimizer/SYSTEM.md +32 -0
  41. package/agents/seo-optimizer/agent.yml +54 -0
  42. package/agents/systems-executor/SYSTEM.md +32 -0
  43. package/agents/systems-executor/agent.yml +54 -0
  44. package/agents/tester/SYSTEM.md +32 -0
  45. package/agents/tester/agent.yml +58 -0
  46. package/agents/vue-executor/SYSTEM.md +32 -0
  47. package/agents/vue-executor/agent.yml +58 -0
  48. package/agents/web3-executor/SYSTEM.md +32 -0
  49. package/agents/web3-executor/agent.yml +55 -0
  50. package/bin/imhcode.js +1634 -0
  51. package/dist/orchestrator/builder.d.ts +23 -0
  52. package/dist/orchestrator/builder.d.ts.map +1 -0
  53. package/dist/orchestrator/builder.js +231 -0
  54. package/dist/orchestrator/builder.js.map +1 -0
  55. package/dist/orchestrator/executor.d.ts +43 -0
  56. package/dist/orchestrator/executor.d.ts.map +1 -0
  57. package/dist/orchestrator/executor.js +356 -0
  58. package/dist/orchestrator/executor.js.map +1 -0
  59. package/dist/orchestrator/index.d.ts +35 -0
  60. package/dist/orchestrator/index.d.ts.map +1 -0
  61. package/dist/orchestrator/index.js +490 -0
  62. package/dist/orchestrator/index.js.map +1 -0
  63. package/dist/orchestrator/loader.d.ts +33 -0
  64. package/dist/orchestrator/loader.d.ts.map +1 -0
  65. package/dist/orchestrator/loader.js +203 -0
  66. package/dist/orchestrator/loader.js.map +1 -0
  67. package/dist/orchestrator/registry.d.ts +63 -0
  68. package/dist/orchestrator/registry.d.ts.map +1 -0
  69. package/dist/orchestrator/registry.js +181 -0
  70. package/dist/orchestrator/registry.js.map +1 -0
  71. package/dist/orchestrator/schema.d.ts +132 -0
  72. package/dist/orchestrator/schema.d.ts.map +1 -0
  73. package/dist/orchestrator/schema.js +101 -0
  74. package/dist/orchestrator/schema.js.map +1 -0
  75. package/dist/orchestrator/session.d.ts +25 -0
  76. package/dist/orchestrator/session.d.ts.map +1 -0
  77. package/dist/orchestrator/session.js +80 -0
  78. package/dist/orchestrator/session.js.map +1 -0
  79. package/dist/orchestrator/types.d.ts +91 -0
  80. package/dist/orchestrator/types.d.ts.map +1 -0
  81. package/dist/orchestrator/types.js +7 -0
  82. package/dist/orchestrator/types.js.map +1 -0
  83. package/package.json +95 -0
  84. package/skills/AnimatePresence/SKILL.md +3 -0
  85. package/skills/accessibility/SKILL.md +146 -0
  86. package/skills/agent-architecture-audit/SKILL.md +256 -0
  87. package/skills/agent-eval/SKILL.md +145 -0
  88. package/skills/agent-introspection-debugging/SKILL.md +153 -0
  89. package/skills/agentic-engineering/SKILL.md +63 -0
  90. package/skills/ai-regression-testing/SKILL.md +385 -0
  91. package/skills/android-clean-architecture/SKILL.md +339 -0
  92. package/skills/animated-component-libraries/SKILL.md +8 -0
  93. package/skills/animejs/SKILL.md +8 -0
  94. package/skills/api-connector-builder/SKILL.md +120 -0
  95. package/skills/api-design/SKILL.md +523 -0
  96. package/skills/apple/SKILL.md +158 -0
  97. package/skills/apple/metadata.json +16 -0
  98. package/skills/architecture-decision-records/SKILL.md +179 -0
  99. package/skills/article-writing/SKILL.md +79 -0
  100. package/skills/automation-audit-ops/SKILL.md +142 -0
  101. package/skills/awwwards/SKILL.md +162 -0
  102. package/skills/awwwards/metadata.json +16 -0
  103. package/skills/babylonjs-engine/SKILL.md +8 -0
  104. package/skills/backend-patterns/SKILL.md +561 -0
  105. package/skills/baqir-docs/SKILL.md +3 -0
  106. package/skills/barba-js/SKILL.md +8 -0
  107. package/skills/benchmark/SKILL.md +93 -0
  108. package/skills/best-practice/SKILL.md +7 -0
  109. package/skills/best-practice/assets/claude-memory/claude-memory-monorepo.jpg +0 -0
  110. package/skills/best-practice/assets/claude-power-ups/dial-the-model-1.png +0 -0
  111. package/skills/best-practice/assets/claude-power-ups/dial-the-model-2.png +0 -0
  112. package/skills/best-practice/assets/claude-power-ups/dial-the-model-3.png +0 -0
  113. package/skills/best-practice/assets/claude-power-ups/powerup-menu.png +0 -0
  114. package/skills/best-practice/claude-cli-startup-flags.md +231 -0
  115. package/skills/best-practice/claude-commands.md +132 -0
  116. package/skills/best-practice/claude-mcp.md +132 -0
  117. package/skills/best-practice/claude-memory.md +121 -0
  118. package/skills/best-practice/claude-power-ups.md +66 -0
  119. package/skills/best-practice/claude-settings.md +1170 -0
  120. package/skills/best-practice/claude-skills.md +58 -0
  121. package/skills/best-practice/claude-subagents.md +56 -0
  122. package/skills/blender-motion-state-inspection/SKILL.md +164 -0
  123. package/skills/bootstrap-patterns/SKILL.md +316 -0
  124. package/skills/brand-guidelines/LICENSE.txt +202 -0
  125. package/skills/brand-guidelines/SKILL.md +73 -0
  126. package/skills/brand-voice/SKILL.md +97 -0
  127. package/skills/brand-voice/references/voice-profile-schema.md +55 -0
  128. package/skills/browser-qa/SKILL.md +87 -0
  129. package/skills/canary-watch/SKILL.md +107 -0
  130. package/skills/canvas-design/LICENSE.txt +202 -0
  131. package/skills/canvas-design/SKILL.md +8 -0
  132. package/skills/canvas-design/canvas-fonts/ArsenalSC-OFL.txt +93 -0
  133. package/skills/canvas-design/canvas-fonts/ArsenalSC-Regular.ttf +0 -0
  134. package/skills/canvas-design/canvas-fonts/BigShoulders-Bold.ttf +0 -0
  135. package/skills/canvas-design/canvas-fonts/BigShoulders-OFL.txt +93 -0
  136. package/skills/canvas-design/canvas-fonts/BigShoulders-Regular.ttf +0 -0
  137. package/skills/canvas-design/canvas-fonts/Boldonse-OFL.txt +93 -0
  138. package/skills/canvas-design/canvas-fonts/Boldonse-Regular.ttf +0 -0
  139. package/skills/canvas-design/canvas-fonts/BricolageGrotesque-Bold.ttf +0 -0
  140. package/skills/canvas-design/canvas-fonts/BricolageGrotesque-OFL.txt +93 -0
  141. package/skills/canvas-design/canvas-fonts/BricolageGrotesque-Regular.ttf +0 -0
  142. package/skills/canvas-design/canvas-fonts/CrimsonPro-Bold.ttf +0 -0
  143. package/skills/canvas-design/canvas-fonts/CrimsonPro-Italic.ttf +0 -0
  144. package/skills/canvas-design/canvas-fonts/CrimsonPro-OFL.txt +93 -0
  145. package/skills/canvas-design/canvas-fonts/CrimsonPro-Regular.ttf +0 -0
  146. package/skills/canvas-design/canvas-fonts/DMMono-OFL.txt +93 -0
  147. package/skills/canvas-design/canvas-fonts/DMMono-Regular.ttf +0 -0
  148. package/skills/canvas-design/canvas-fonts/EricaOne-OFL.txt +94 -0
  149. package/skills/canvas-design/canvas-fonts/EricaOne-Regular.ttf +0 -0
  150. package/skills/canvas-design/canvas-fonts/GeistMono-Bold.ttf +0 -0
  151. package/skills/canvas-design/canvas-fonts/GeistMono-OFL.txt +93 -0
  152. package/skills/canvas-design/canvas-fonts/GeistMono-Regular.ttf +0 -0
  153. package/skills/canvas-design/canvas-fonts/Gloock-OFL.txt +93 -0
  154. package/skills/canvas-design/canvas-fonts/Gloock-Regular.ttf +0 -0
  155. package/skills/canvas-design/canvas-fonts/IBMPlexMono-Bold.ttf +0 -0
  156. package/skills/canvas-design/canvas-fonts/IBMPlexMono-OFL.txt +93 -0
  157. package/skills/canvas-design/canvas-fonts/IBMPlexMono-Regular.ttf +0 -0
  158. package/skills/canvas-design/canvas-fonts/IBMPlexSerif-Bold.ttf +0 -0
  159. package/skills/canvas-design/canvas-fonts/IBMPlexSerif-BoldItalic.ttf +0 -0
  160. package/skills/canvas-design/canvas-fonts/IBMPlexSerif-Italic.ttf +0 -0
  161. package/skills/canvas-design/canvas-fonts/IBMPlexSerif-Regular.ttf +0 -0
  162. package/skills/canvas-design/canvas-fonts/InstrumentSans-Bold.ttf +0 -0
  163. package/skills/canvas-design/canvas-fonts/InstrumentSans-BoldItalic.ttf +0 -0
  164. package/skills/canvas-design/canvas-fonts/InstrumentSans-Italic.ttf +0 -0
  165. package/skills/canvas-design/canvas-fonts/InstrumentSans-OFL.txt +93 -0
  166. package/skills/canvas-design/canvas-fonts/InstrumentSans-Regular.ttf +0 -0
  167. package/skills/canvas-design/canvas-fonts/InstrumentSerif-Italic.ttf +0 -0
  168. package/skills/canvas-design/canvas-fonts/InstrumentSerif-Regular.ttf +0 -0
  169. package/skills/canvas-design/canvas-fonts/Italiana-OFL.txt +93 -0
  170. package/skills/canvas-design/canvas-fonts/Italiana-Regular.ttf +0 -0
  171. package/skills/canvas-design/canvas-fonts/JetBrainsMono-Bold.ttf +0 -0
  172. package/skills/canvas-design/canvas-fonts/JetBrainsMono-OFL.txt +93 -0
  173. package/skills/canvas-design/canvas-fonts/JetBrainsMono-Regular.ttf +0 -0
  174. package/skills/canvas-design/canvas-fonts/Jura-Light.ttf +0 -0
  175. package/skills/canvas-design/canvas-fonts/Jura-Medium.ttf +0 -0
  176. package/skills/canvas-design/canvas-fonts/Jura-OFL.txt +93 -0
  177. package/skills/canvas-design/canvas-fonts/LibreBaskerville-OFL.txt +93 -0
  178. package/skills/canvas-design/canvas-fonts/LibreBaskerville-Regular.ttf +0 -0
  179. package/skills/canvas-design/canvas-fonts/Lora-Bold.ttf +0 -0
  180. package/skills/canvas-design/canvas-fonts/Lora-BoldItalic.ttf +0 -0
  181. package/skills/canvas-design/canvas-fonts/Lora-Italic.ttf +0 -0
  182. package/skills/canvas-design/canvas-fonts/Lora-OFL.txt +93 -0
  183. package/skills/canvas-design/canvas-fonts/Lora-Regular.ttf +0 -0
  184. package/skills/canvas-design/canvas-fonts/NationalPark-Bold.ttf +0 -0
  185. package/skills/canvas-design/canvas-fonts/NationalPark-OFL.txt +93 -0
  186. package/skills/canvas-design/canvas-fonts/NationalPark-Regular.ttf +0 -0
  187. package/skills/canvas-design/canvas-fonts/NothingYouCouldDo-OFL.txt +93 -0
  188. package/skills/canvas-design/canvas-fonts/NothingYouCouldDo-Regular.ttf +0 -0
  189. package/skills/canvas-design/canvas-fonts/Outfit-Bold.ttf +0 -0
  190. package/skills/canvas-design/canvas-fonts/Outfit-OFL.txt +93 -0
  191. package/skills/canvas-design/canvas-fonts/Outfit-Regular.ttf +0 -0
  192. package/skills/canvas-design/canvas-fonts/PixelifySans-Medium.ttf +0 -0
  193. package/skills/canvas-design/canvas-fonts/PixelifySans-OFL.txt +93 -0
  194. package/skills/canvas-design/canvas-fonts/PoiretOne-OFL.txt +93 -0
  195. package/skills/canvas-design/canvas-fonts/PoiretOne-Regular.ttf +0 -0
  196. package/skills/canvas-design/canvas-fonts/RedHatMono-Bold.ttf +0 -0
  197. package/skills/canvas-design/canvas-fonts/RedHatMono-OFL.txt +93 -0
  198. package/skills/canvas-design/canvas-fonts/RedHatMono-Regular.ttf +0 -0
  199. package/skills/canvas-design/canvas-fonts/Silkscreen-OFL.txt +93 -0
  200. package/skills/canvas-design/canvas-fonts/Silkscreen-Regular.ttf +0 -0
  201. package/skills/canvas-design/canvas-fonts/SmoochSans-Medium.ttf +0 -0
  202. package/skills/canvas-design/canvas-fonts/SmoochSans-OFL.txt +93 -0
  203. package/skills/canvas-design/canvas-fonts/Tektur-Medium.ttf +0 -0
  204. package/skills/canvas-design/canvas-fonts/Tektur-OFL.txt +93 -0
  205. package/skills/canvas-design/canvas-fonts/Tektur-Regular.ttf +0 -0
  206. package/skills/canvas-design/canvas-fonts/WorkSans-Bold.ttf +0 -0
  207. package/skills/canvas-design/canvas-fonts/WorkSans-BoldItalic.ttf +0 -0
  208. package/skills/canvas-design/canvas-fonts/WorkSans-Italic.ttf +0 -0
  209. package/skills/canvas-design/canvas-fonts/WorkSans-OFL.txt +93 -0
  210. package/skills/canvas-design/canvas-fonts/WorkSans-Regular.ttf +0 -0
  211. package/skills/canvas-design/canvas-fonts/YoungSerif-OFL.txt +93 -0
  212. package/skills/canvas-design/canvas-fonts/YoungSerif-Regular.ttf +0 -0
  213. package/skills/caveman/SKILL.md +74 -0
  214. package/skills/changelog-generator/SKILL.md +104 -0
  215. package/skills/cisco-ios-patterns/SKILL.md +163 -0
  216. package/skills/ckm:design-system/SKILL.md +3 -0
  217. package/skills/ckm:slides/SKILL.md +3 -0
  218. package/skills/claude-red/SKILL.md +35 -0
  219. package/skills/click-path-audit/SKILL.md +244 -0
  220. package/skills/clickhouse-io/SKILL.md +439 -0
  221. package/skills/codebase-onboarding/SKILL.md +233 -0
  222. package/skills/coding-standards/SKILL.md +549 -0
  223. package/skills/compose-multiplatform-patterns/SKILL.md +299 -0
  224. package/skills/computational-architecture/SKILL.md +31 -0
  225. package/skills/content-engine/SKILL.md +131 -0
  226. package/skills/content-research-writer/SKILL.md +538 -0
  227. package/skills/continuous-agent-loop/SKILL.md +45 -0
  228. package/skills/cost-aware-llm-pipeline/SKILL.md +183 -0
  229. package/skills/cpp-coding-standards/SKILL.md +723 -0
  230. package/skills/crosspost/SKILL.md +111 -0
  231. package/skills/css-container-queries/SKILL.md +132 -0
  232. package/skills/customer-billing-ops/SKILL.md +140 -0
  233. package/skills/customs-trade-compliance/SKILL.md +263 -0
  234. package/skills/dart-flutter-patterns/SKILL.md +563 -0
  235. package/skills/dashboard-builder/SKILL.md +108 -0
  236. package/skills/data-scraper-agent/SKILL.md +764 -0
  237. package/skills/database-migrations/SKILL.md +429 -0
  238. package/skills/deep-research/SKILL.md +159 -0
  239. package/skills/defi-amm-security/SKILL.md +166 -0
  240. package/skills/deployment-patterns/SKILL.md +427 -0
  241. package/skills/design-auditor/README.md +504 -0
  242. package/skills/design-auditor/README_KR.md +327 -0
  243. package/skills/design-auditor/SKILL.md +8 -0
  244. package/skills/design-auditor/references/animation.md +300 -0
  245. package/skills/design-auditor/references/color.md +241 -0
  246. package/skills/design-auditor/references/corner-radius.md +331 -0
  247. package/skills/design-auditor/references/elevation.md +249 -0
  248. package/skills/design-auditor/references/ethics.md +877 -0
  249. package/skills/design-auditor/references/figma-mcp.md +377 -0
  250. package/skills/design-auditor/references/heuristics.md +730 -0
  251. package/skills/design-auditor/references/i18n.md +254 -0
  252. package/skills/design-auditor/references/iconography.md +201 -0
  253. package/skills/design-auditor/references/microcopy.md +227 -0
  254. package/skills/design-auditor/references/navigation.md +311 -0
  255. package/skills/design-auditor/references/spacing.md +313 -0
  256. package/skills/design-auditor/references/states.md +254 -0
  257. package/skills/design-auditor/references/tokens.md +286 -0
  258. package/skills/design-auditor/references/typography.md +273 -0
  259. package/skills/design-system/SKILL.md +82 -0
  260. package/skills/design-tokens-system/SKILL.md +156 -0
  261. package/skills/development-workflows/SKILL.md +7 -0
  262. package/skills/development-workflows/cross-model-workflow/assets/cross-model-workflow.png +0 -0
  263. package/skills/development-workflows/cross-model-workflow/cross-model-workflow.md +55 -0
  264. package/skills/development-workflows/rpi/.claude/agents/code-reviewer.md +21 -0
  265. package/skills/development-workflows/rpi/.claude/agents/constitutional-validator.md +288 -0
  266. package/skills/development-workflows/rpi/.claude/agents/documentation-analyst-writer.md +64 -0
  267. package/skills/development-workflows/rpi/.claude/agents/product-manager.md +15 -0
  268. package/skills/development-workflows/rpi/.claude/agents/requirement-parser.md +238 -0
  269. package/skills/development-workflows/rpi/.claude/agents/senior-software-engineer.md +15 -0
  270. package/skills/development-workflows/rpi/.claude/agents/technical-cto-advisor.md +201 -0
  271. package/skills/development-workflows/rpi/.claude/agents/ux-designer.md +14 -0
  272. package/skills/development-workflows/rpi/.claude/commands/rpi/implement.md +634 -0
  273. package/skills/development-workflows/rpi/.claude/commands/rpi/plan.md +416 -0
  274. package/skills/development-workflows/rpi/.claude/commands/rpi/research.md +381 -0
  275. package/skills/development-workflows/rpi/rpi-workflow.md +101 -0
  276. package/skills/development-workflows/rpi/rpi-workflow.svg +178 -0
  277. package/skills/django-celery/SKILL.md +457 -0
  278. package/skills/django-patterns/SKILL.md +734 -0
  279. package/skills/django-security/SKILL.md +593 -0
  280. package/skills/django-tdd/SKILL.md +729 -0
  281. package/skills/dmux-workflows/SKILL.md +7 -0
  282. package/skills/docker-patterns/SKILL.md +364 -0
  283. package/skills/documentation-lookup/SKILL.md +90 -0
  284. package/skills/domain-name-brainstormer/SKILL.md +212 -0
  285. package/skills/dribbble/SKILL.md +160 -0
  286. package/skills/dribbble/metadata.json +16 -0
  287. package/skills/e2e-testing/SKILL.md +326 -0
  288. package/skills/empirical-research/SKILL.md +22 -0
  289. package/skills/error-handling/SKILL.md +376 -0
  290. package/skills/eval-harness/SKILL.md +270 -0
  291. package/skills/evm-token-decimals/SKILL.md +130 -0
  292. package/skills/fastapi-patterns/SKILL.md +327 -0
  293. package/skills/finance-billing-ops/SKILL.md +127 -0
  294. package/skills/flox-environments/SKILL.md +496 -0
  295. package/skills/flutter-dart-code-review/SKILL.md +435 -0
  296. package/skills/foundation-models-on-device/SKILL.md +243 -0
  297. package/skills/framer/SKILL.md +159 -0
  298. package/skills/framer/metadata.json +16 -0
  299. package/skills/frontend-design/LICENSE.txt +21 -0
  300. package/skills/frontend-design/README.md +71 -0
  301. package/skills/frontend-design/SKILL.md +115 -0
  302. package/skills/frontend-design-direction/SKILL.md +92 -0
  303. package/skills/frontend-patterns/SKILL.md +7 -0
  304. package/skills/frontend-slides/SKILL.md +184 -0
  305. package/skills/frontend-slides/STYLE_PRESETS.md +330 -0
  306. package/skills/frontend-slides/animation-patterns.md +122 -0
  307. package/skills/frontend-slides/html-template.md +419 -0
  308. package/skills/frontend-slides/scripts/export-pdf.sh +418 -0
  309. package/skills/frontend-slides/scripts/extract-pptx.py +96 -0
  310. package/skills/frontend-slides/viewport-base.css +153 -0
  311. package/skills/gateguard/SKILL.md +125 -0
  312. package/skills/git-workflow/SKILL.md +715 -0
  313. package/skills/github-ops/SKILL.md +144 -0
  314. package/skills/golang-patterns/SKILL.md +674 -0
  315. package/skills/golang-testing/SKILL.md +720 -0
  316. package/skills/graphify/__init__.py +28 -0
  317. package/skills/graphify/__main__.py +2899 -0
  318. package/skills/graphify/analyze.py +608 -0
  319. package/skills/graphify/benchmark.py +152 -0
  320. package/skills/graphify/build.py +366 -0
  321. package/skills/graphify/cache.py +244 -0
  322. package/skills/graphify/callflow_html.py +2014 -0
  323. package/skills/graphify/cluster.py +267 -0
  324. package/skills/graphify/dedup.py +414 -0
  325. package/skills/graphify/detect.py +1018 -0
  326. package/skills/graphify/export.py +1264 -0
  327. package/skills/graphify/extract.py +6646 -0
  328. package/skills/graphify/global_graph.py +155 -0
  329. package/skills/graphify/google_workspace.py +223 -0
  330. package/skills/graphify/hooks.py +304 -0
  331. package/skills/graphify/ingest.py +331 -0
  332. package/skills/graphify/llm.py +1111 -0
  333. package/skills/graphify/manifest.py +4 -0
  334. package/skills/graphify/prs.py +746 -0
  335. package/skills/graphify/report.py +203 -0
  336. package/skills/graphify/security.py +242 -0
  337. package/skills/graphify/serve.py +899 -0
  338. package/skills/graphify/skill-aider.md +1229 -0
  339. package/skills/graphify/skill-claw.md +1229 -0
  340. package/skills/graphify/skill-codex.md +1290 -0
  341. package/skills/graphify/skill-copilot.md +1317 -0
  342. package/skills/graphify/skill-droid.md +1285 -0
  343. package/skills/graphify/skill-kiro.md +1228 -0
  344. package/skills/graphify/skill-opencode.md +1340 -0
  345. package/skills/graphify/skill-pi.md +1228 -0
  346. package/skills/graphify/skill-trae.md +1254 -0
  347. package/skills/graphify/skill-vscode.md +258 -0
  348. package/skills/graphify/skill-windows.md +1434 -0
  349. package/skills/graphify/skill.md +1086 -0
  350. package/skills/graphify/transcribe.py +184 -0
  351. package/skills/graphify/tree_html.py +580 -0
  352. package/skills/graphify/validate.py +72 -0
  353. package/skills/graphify/watch.py +728 -0
  354. package/skills/graphify/wiki.py +259 -0
  355. package/skills/growth-marketing/SKILL.md +26 -0
  356. package/skills/gsap-scrolltrigger/SKILL.md +8 -0
  357. package/skills/healthcare-cdss-patterns/SKILL.md +245 -0
  358. package/skills/healthcare-emr-patterns/SKILL.md +159 -0
  359. package/skills/healthcare-phi-compliance/SKILL.md +145 -0
  360. package/skills/hexagonal-architecture/SKILL.md +276 -0
  361. package/skills/hipaa-compliance/SKILL.md +78 -0
  362. package/skills/homelab-network-setup/SKILL.md +129 -0
  363. package/skills/imhcode/SKILL.md +183 -0
  364. package/skills/imhcode-brainstorm/SKILL.md +37 -0
  365. package/skills/imhcode-codebase-knowledge/SKILL.md +37 -0
  366. package/skills/imhcode-codebase-knowledge/assets/templates/ARCHITECTURE.md +49 -0
  367. package/skills/imhcode-codebase-knowledge/assets/templates/CONCERNS.md +56 -0
  368. package/skills/imhcode-codebase-knowledge/assets/templates/CONVENTIONS.md +52 -0
  369. package/skills/imhcode-codebase-knowledge/assets/templates/INTEGRATIONS.md +48 -0
  370. package/skills/imhcode-codebase-knowledge/assets/templates/STACK.md +56 -0
  371. package/skills/imhcode-codebase-knowledge/assets/templates/STRUCTURE.md +44 -0
  372. package/skills/imhcode-codebase-knowledge/assets/templates/TESTING.md +57 -0
  373. package/skills/imhcode-codebase-knowledge/references/inquiry-checkpoints.md +70 -0
  374. package/skills/imhcode-codebase-knowledge/references/stack-detection.md +131 -0
  375. package/skills/imhcode-codebase-knowledge/scripts/scan.py +712 -0
  376. package/skills/imhcode-dispatch/SKILL.md +31 -0
  377. package/skills/imhcode-saas-architect/SKILL.md +55 -0
  378. package/skills/imhcode-security/SKILL.md +42 -0
  379. package/skills/imhcode-sprint-planner/SKILL.md +42 -0
  380. package/skills/inventory-demand-planning/SKILL.md +247 -0
  381. package/skills/investor-materials/SKILL.md +96 -0
  382. package/skills/investor-outreach/SKILL.md +91 -0
  383. package/skills/ios-icon-gen/SKILL.md +157 -0
  384. package/skills/ios-icon-gen/scripts/generate_icons.swift +258 -0
  385. package/skills/ios-icon-gen/scripts/iconify_gen.sh +235 -0
  386. package/skills/java-coding-standards/SKILL.md +383 -0
  387. package/skills/jpa-patterns/SKILL.md +151 -0
  388. package/skills/kotlin-ktor-patterns/SKILL.md +689 -0
  389. package/skills/kotlin-patterns/SKILL.md +711 -0
  390. package/skills/laravel-patterns/SKILL.md +415 -0
  391. package/skills/laravel-plugin-discovery/SKILL.md +229 -0
  392. package/skills/laravel-security/SKILL.md +285 -0
  393. package/skills/laravel-tdd/SKILL.md +283 -0
  394. package/skills/laravel-verification/SKILL.md +179 -0
  395. package/skills/lightweight-3d-effects/SKILL.md +8 -0
  396. package/skills/linear/SKILL.md +156 -0
  397. package/skills/linear/metadata.json +16 -0
  398. package/skills/liquid-glass-design/SKILL.md +279 -0
  399. package/skills/llm-trading-agent-security/SKILL.md +146 -0
  400. package/skills/locomotive-scroll/SKILL.md +8 -0
  401. package/skills/logistics-exception-management/SKILL.md +222 -0
  402. package/skills/lottie-animations/SKILL.md +8 -0
  403. package/skills/make-interfaces-feel-better/SKILL.md +151 -0
  404. package/skills/mcp-server-patterns/SKILL.md +69 -0
  405. package/skills/meeting-insights-analyzer/SKILL.md +327 -0
  406. package/skills/micro-interactions/SKILL.md +178 -0
  407. package/skills/mle-workflow/SKILL.md +346 -0
  408. package/skills/mobbin/SKILL.md +157 -0
  409. package/skills/mobbin/metadata.json +16 -0
  410. package/skills/mobile-app-design/README.md +302 -0
  411. package/skills/mobile-app-design/SKILL.md +330 -0
  412. package/skills/mobile-app-design/examples/design-system-config.ts +685 -0
  413. package/skills/mobile-app-design/examples/form-validation-example.tsx +501 -0
  414. package/skills/mobile-app-design/examples/optimized-list-example.tsx +384 -0
  415. package/skills/mobile-app-design/examples/profile-screen-example.tsx +450 -0
  416. package/skills/mobile-app-design/references/accessibility-checklist.md +525 -0
  417. package/skills/mobile-app-design/references/android-guidelines.md +582 -0
  418. package/skills/mobile-app-design/references/common-mistakes.md +834 -0
  419. package/skills/mobile-app-design/references/ios-guidelines.md +421 -0
  420. package/skills/mobile-app-design/references/performance-patterns.md +705 -0
  421. package/skills/mobile-app-design/references/platform-differences.md +353 -0
  422. package/skills/mobile-app-design/references/ui-libraries.md +729 -0
  423. package/skills/mobile-app-design/scripts/accessibility-audit.sh +281 -0
  424. package/skills/mobile-app-design/scripts/check-contrast.py +188 -0
  425. package/skills/mobile-app-design/scripts/validate-touch-targets.sh +228 -0
  426. package/skills/mobile-app-ui-design/INDEX.md +131 -0
  427. package/skills/mobile-app-ui-design/README.md +135 -0
  428. package/skills/mobile-app-ui-design/SKILL.md +7 -0
  429. package/skills/mobile-app-ui-design/references/industry-conventions.md +151 -0
  430. package/skills/modern-web-design/SKILL.md +8 -0
  431. package/skills/motion-advanced/SKILL.md +596 -0
  432. package/skills/motion-design/SKILL.md +21 -0
  433. package/skills/motion-foundations/SKILL.md +299 -0
  434. package/skills/motion-framer/SKILL.md +8 -0
  435. package/skills/motion-patterns/SKILL.md +435 -0
  436. package/skills/motion-ui/SKILL.md +575 -0
  437. package/skills/mysql-patterns/SKILL.md +412 -0
  438. package/skills/nestjs-patterns/SKILL.md +230 -0
  439. package/skills/network-config-validation/SKILL.md +210 -0
  440. package/skills/nextjs-turbopack/SKILL.md +44 -0
  441. package/skills/nodejs-keccak256/SKILL.md +102 -0
  442. package/skills/nothing-design/SKILL.md +177 -0
  443. package/skills/nothing-design/references/components.md +153 -0
  444. package/skills/nothing-design/references/platform-mapping.md +64 -0
  445. package/skills/nothing-design/references/tokens.md +142 -0
  446. package/skills/nuxt4-patterns/SKILL.md +100 -0
  447. package/skills/openclaw-persona-forge/SKILL.md +288 -0
  448. package/skills/openclaw-persona-forge/gacha.py +224 -0
  449. package/skills/openclaw-persona-forge/gacha.sh +5 -0
  450. package/skills/openclaw-persona-forge/references/avatar-style.md +124 -0
  451. package/skills/openclaw-persona-forge/references/boundary-rules.md +53 -0
  452. package/skills/openclaw-persona-forge/references/error-handling.md +53 -0
  453. package/skills/openclaw-persona-forge/references/identity-tension.md +48 -0
  454. package/skills/openclaw-persona-forge/references/naming-system.md +39 -0
  455. package/skills/openclaw-persona-forge/references/output-template.md +166 -0
  456. package/skills/orchestration-workflow/SKILL.md +199 -0
  457. package/skills/orchestration-workflow/orchestration-workflow.gif +0 -0
  458. package/skills/orchestration-workflow/orchestration-workflow.md +199 -0
  459. package/skills/orchestration-workflow/orchestration-workflow.svg +78 -0
  460. package/skills/orchestration-workflow/output.md +13 -0
  461. package/skills/orchestration-workflow/weather.svg +6 -0
  462. package/skills/pixijs-2d/SKILL.md +8 -0
  463. package/skills/plan-orchestrate/SKILL.md +262 -0
  464. package/skills/plankton-code-quality/SKILL.md +236 -0
  465. package/skills/postgres-patterns/SKILL.md +147 -0
  466. package/skills/ppt-mckinsey/SKILL.md +27 -0
  467. package/skills/prisma-patterns/SKILL.md +371 -0
  468. package/skills/product-capability/SKILL.md +141 -0
  469. package/skills/product-lens/SKILL.md +92 -0
  470. package/skills/production-audit/SKILL.md +206 -0
  471. package/skills/project-flow-ops/SKILL.md +111 -0
  472. package/skills/prompt-optimizer/SKILL.md +398 -0
  473. package/skills/python-patterns/SKILL.md +750 -0
  474. package/skills/python-testing/SKILL.md +816 -0
  475. package/skills/pytorch-patterns/SKILL.md +396 -0
  476. package/skills/quarkus-patterns/SKILL.md +722 -0
  477. package/skills/radix-ui-primitives/SKILL.md +86 -0
  478. package/skills/react-native-best-practices/SKILL.md +83 -0
  479. package/skills/react-native-brownfield-migration/SKILL.md +115 -0
  480. package/skills/react-spring-physics/SKILL.md +8 -0
  481. package/skills/react-three-fiber/SKILL.md +8 -0
  482. package/skills/recsys-pipeline-architect/SKILL.md +114 -0
  483. package/skills/redis-patterns/SKILL.md +403 -0
  484. package/skills/remotion-video-creation/SKILL.md +43 -0
  485. package/skills/remotion-video-creation/rules/3d.md +86 -0
  486. package/skills/remotion-video-creation/rules/animations.md +29 -0
  487. package/skills/remotion-video-creation/rules/assets/charts-bar-chart.tsx +173 -0
  488. package/skills/remotion-video-creation/rules/assets/text-animations-typewriter.tsx +100 -0
  489. package/skills/remotion-video-creation/rules/assets/text-animations-word-highlight.tsx +108 -0
  490. package/skills/remotion-video-creation/rules/assets.md +78 -0
  491. package/skills/remotion-video-creation/rules/audio.md +172 -0
  492. package/skills/remotion-video-creation/rules/calculate-metadata.md +104 -0
  493. package/skills/remotion-video-creation/rules/can-decode.md +75 -0
  494. package/skills/remotion-video-creation/rules/charts.md +58 -0
  495. package/skills/remotion-video-creation/rules/compositions.md +146 -0
  496. package/skills/remotion-video-creation/rules/display-captions.md +126 -0
  497. package/skills/remotion-video-creation/rules/extract-frames.md +229 -0
  498. package/skills/remotion-video-creation/rules/fonts.md +152 -0
  499. package/skills/remotion-video-creation/rules/get-audio-duration.md +58 -0
  500. package/skills/remotion-video-creation/rules/get-video-dimensions.md +68 -0
  501. package/skills/remotion-video-creation/rules/get-video-duration.md +58 -0
  502. package/skills/remotion-video-creation/rules/gifs.md +138 -0
  503. package/skills/remotion-video-creation/rules/images.md +130 -0
  504. package/skills/remotion-video-creation/rules/import-srt-captions.md +67 -0
  505. package/skills/remotion-video-creation/rules/lottie.md +67 -0
  506. package/skills/remotion-video-creation/rules/measuring-dom-nodes.md +34 -0
  507. package/skills/remotion-video-creation/rules/measuring-text.md +143 -0
  508. package/skills/remotion-video-creation/rules/sequencing.md +106 -0
  509. package/skills/remotion-video-creation/rules/tailwind.md +11 -0
  510. package/skills/remotion-video-creation/rules/text-animations.md +20 -0
  511. package/skills/remotion-video-creation/rules/timing.md +179 -0
  512. package/skills/remotion-video-creation/rules/transcribe-captions.md +19 -0
  513. package/skills/remotion-video-creation/rules/transitions.md +122 -0
  514. package/skills/remotion-video-creation/rules/trimming.md +52 -0
  515. package/skills/remotion-video-creation/rules/videos.md +171 -0
  516. package/skills/repo-scan/SKILL.md +78 -0
  517. package/skills/responsive-email-templates/SKILL.md +110 -0
  518. package/skills/rive-interactive/SKILL.md +8 -0
  519. package/skills/rust-patterns/SKILL.md +499 -0
  520. package/skills/rust-testing/SKILL.md +500 -0
  521. package/skills/scroll-reveal-libraries/SKILL.md +8 -0
  522. package/skills/security-bounty-hunter/SKILL.md +99 -0
  523. package/skills/security-review/SKILL.md +503 -0
  524. package/skills/security-review/cloud-infrastructure-security.md +361 -0
  525. package/skills/security-scan/SKILL.md +165 -0
  526. package/skills/seo/SKILL.md +154 -0
  527. package/skills/seo-growth/SKILL.md +34 -0
  528. package/skills/shadcn-ui-patterns/SKILL.md +123 -0
  529. package/skills/spline-interactive/SKILL.md +8 -0
  530. package/skills/springboot-patterns/SKILL.md +314 -0
  531. package/skills/springboot-security/SKILL.md +272 -0
  532. package/skills/springboot-tdd/SKILL.md +158 -0
  533. package/skills/strategic-compact/SKILL.md +131 -0
  534. package/skills/stripe/SKILL.md +168 -0
  535. package/skills/stripe/metadata.json +16 -0
  536. package/skills/swift-actor-persistence/SKILL.md +143 -0
  537. package/skills/swift-concurrency-6-2/SKILL.md +216 -0
  538. package/skills/swiftui-patterns/SKILL.md +259 -0
  539. package/skills/systematic-debugging/CREATION-LOG.md +119 -0
  540. package/skills/systematic-debugging/SKILL.md +296 -0
  541. package/skills/systematic-debugging/condition-based-waiting-example.ts +158 -0
  542. package/skills/systematic-debugging/condition-based-waiting.md +115 -0
  543. package/skills/systematic-debugging/defense-in-depth.md +122 -0
  544. package/skills/systematic-debugging/find-polluter.sh +63 -0
  545. package/skills/systematic-debugging/root-cause-tracing.md +169 -0
  546. package/skills/systematic-debugging/test-academic.md +14 -0
  547. package/skills/systematic-debugging/test-pressure-1.md +58 -0
  548. package/skills/systematic-debugging/test-pressure-2.md +68 -0
  549. package/skills/systematic-debugging/test-pressure-3.md +69 -0
  550. package/skills/tailwindcss-v4/SKILL.md +107 -0
  551. package/skills/tdd-workflow/SKILL.md +463 -0
  552. package/skills/team-builder/SKILL.md +168 -0
  553. package/skills/test-driven-development/SKILL.md +371 -0
  554. package/skills/test-driven-development/testing-anti-patterns.md +299 -0
  555. package/skills/theme-factory/LICENSE.txt +202 -0
  556. package/skills/theme-factory/SKILL.md +59 -0
  557. package/skills/theme-factory/theme-showcase.pdf +0 -0
  558. package/skills/theme-factory/themes/arctic-frost.md +19 -0
  559. package/skills/theme-factory/themes/botanical-garden.md +19 -0
  560. package/skills/theme-factory/themes/desert-rose.md +19 -0
  561. package/skills/theme-factory/themes/forest-canopy.md +19 -0
  562. package/skills/theme-factory/themes/golden-hour.md +19 -0
  563. package/skills/theme-factory/themes/midnight-galaxy.md +19 -0
  564. package/skills/theme-factory/themes/modern-minimalist.md +19 -0
  565. package/skills/theme-factory/themes/ocean-depths.md +19 -0
  566. package/skills/theme-factory/themes/sunset-boulevard.md +19 -0
  567. package/skills/theme-factory/themes/tech-innovation.md +19 -0
  568. package/skills/threejs-webgl/SKILL.md +8 -0
  569. package/skills/trailofbits-auditing/SKILL.md +30 -0
  570. package/skills/twitter-algorithm-optimizer/SKILL.md +327 -0
  571. package/skills/typeui-cli/SKILL.md +90 -0
  572. package/skills/typeui-main/DESIGN.md +114 -0
  573. package/skills/typeui-main/LICENSE.md +9 -0
  574. package/skills/typeui-main/README.md +84 -0
  575. package/skills/typeui-main/REGISTRY.md +60 -0
  576. package/skills/typeui-main/SKILL.md +114 -0
  577. package/skills/typeui-main/package-lock.json +2123 -0
  578. package/skills/typeui-main/package.json +56 -0
  579. package/skills/typeui-main/skills/typeui-cli/SKILL.md +90 -0
  580. package/skills/typeui-main/src/cli.ts +282 -0
  581. package/skills/typeui-main/src/config.ts +28 -0
  582. package/skills/typeui-main/src/domain/designSystemSchema.ts +77 -0
  583. package/skills/typeui-main/src/generation/existingDesignSystem.ts +228 -0
  584. package/skills/typeui-main/src/generation/randomDesignSystem.ts +348 -0
  585. package/skills/typeui-main/src/generation/runDesignGeneration.ts +31 -0
  586. package/skills/typeui-main/src/generation/runGeneration.ts +36 -0
  587. package/skills/typeui-main/src/generation/runPull.ts +62 -0
  588. package/skills/typeui-main/src/io/updateSkillFile.ts +118 -0
  589. package/skills/typeui-main/src/prompts/designSystem.ts +755 -0
  590. package/skills/typeui-main/src/prompts/registry.ts +72 -0
  591. package/skills/typeui-main/src/registry/registryClient.ts +246 -0
  592. package/skills/typeui-main/src/renderers/claudeRenderer.ts +10 -0
  593. package/skills/typeui-main/src/renderers/codexRenderer.ts +10 -0
  594. package/skills/typeui-main/src/renderers/cursorRenderer.ts +10 -0
  595. package/skills/typeui-main/src/renderers/index.ts +14 -0
  596. package/skills/typeui-main/src/renderers/openCodeRenderer.ts +10 -0
  597. package/skills/typeui-main/src/renderers/shared.ts +252 -0
  598. package/skills/typeui-main/src/skillMetadata.ts +19 -0
  599. package/skills/typeui-main/src/types.ts +204 -0
  600. package/skills/typeui-main/src/ui/banner.ts +20 -0
  601. package/skills/typeui-main/test/existingDesignSystem.test.ts +67 -0
  602. package/skills/typeui-main/test/orchestrator.test.ts +113 -0
  603. package/skills/typeui-main/test/randomDesignSystem.test.ts +20 -0
  604. package/skills/typeui-main/test/registryClient.test.ts +408 -0
  605. package/skills/typeui-main/test/renderers.test.ts +41 -0
  606. package/skills/typeui-main/test/runDesignGeneration.test.ts +52 -0
  607. package/skills/typeui-main/test/runPull.test.ts +92 -0
  608. package/skills/typeui-main/test/skillMetadata.test.ts +11 -0
  609. package/skills/typeui-main/test/updateSkillFile.test.ts +160 -0
  610. package/skills/typeui-main/tsconfig.json +16 -0
  611. package/skills/ui-demo/SKILL.md +465 -0
  612. package/skills/ui-to-vue/SKILL.md +134 -0
  613. package/skills/ui-ux-pro-max/.claude/skills/banner-design/SKILL.md +192 -0
  614. package/skills/ui-ux-pro-max/.claude/skills/banner-design/references/banner-sizes-and-styles.md +118 -0
  615. package/skills/ui-ux-pro-max/.claude/skills/brand/SKILL.md +97 -0
  616. package/skills/ui-ux-pro-max/.claude/skills/brand/references/approval-checklist.md +169 -0
  617. package/skills/ui-ux-pro-max/.claude/skills/brand/references/asset-organization.md +157 -0
  618. package/skills/ui-ux-pro-max/.claude/skills/brand/references/brand-guideline-template.md +140 -0
  619. package/skills/ui-ux-pro-max/.claude/skills/brand/references/color-palette-management.md +186 -0
  620. package/skills/ui-ux-pro-max/.claude/skills/brand/references/consistency-checklist.md +94 -0
  621. package/skills/ui-ux-pro-max/.claude/skills/brand/references/logo-usage-rules.md +185 -0
  622. package/skills/ui-ux-pro-max/.claude/skills/brand/references/messaging-framework.md +85 -0
  623. package/skills/ui-ux-pro-max/.claude/skills/brand/references/typography-specifications.md +214 -0
  624. package/skills/ui-ux-pro-max/.claude/skills/brand/references/update.md +118 -0
  625. package/skills/ui-ux-pro-max/.claude/skills/brand/references/visual-identity.md +96 -0
  626. package/skills/ui-ux-pro-max/.claude/skills/brand/references/voice-framework.md +88 -0
  627. package/skills/ui-ux-pro-max/.claude/skills/brand/scripts/extract-colors.cjs +341 -0
  628. package/skills/ui-ux-pro-max/.claude/skills/brand/scripts/inject-brand-context.cjs +349 -0
  629. package/skills/ui-ux-pro-max/.claude/skills/brand/scripts/sync-brand-to-tokens.cjs +266 -0
  630. package/skills/ui-ux-pro-max/.claude/skills/brand/scripts/validate-asset.cjs +387 -0
  631. package/skills/ui-ux-pro-max/.claude/skills/brand/templates/brand-guidelines-starter.md +275 -0
  632. package/skills/ui-ux-pro-max/.claude/skills/design/SKILL.md +302 -0
  633. package/skills/ui-ux-pro-max/.claude/skills/design/data/cip/deliverables.csv +51 -0
  634. package/skills/ui-ux-pro-max/.claude/skills/design/data/cip/industries.csv +21 -0
  635. package/skills/ui-ux-pro-max/.claude/skills/design/data/cip/mockup-contexts.csv +21 -0
  636. package/skills/ui-ux-pro-max/.claude/skills/design/data/cip/styles.csv +21 -0
  637. package/skills/ui-ux-pro-max/.claude/skills/design/data/icon/styles.csv +16 -0
  638. package/skills/ui-ux-pro-max/.claude/skills/design/data/logo/colors.csv +56 -0
  639. package/skills/ui-ux-pro-max/.claude/skills/design/data/logo/industries.csv +56 -0
  640. package/skills/ui-ux-pro-max/.claude/skills/design/data/logo/styles.csv +56 -0
  641. package/skills/ui-ux-pro-max/.claude/skills/design/references/banner-sizes-and-styles.md +118 -0
  642. package/skills/ui-ux-pro-max/.claude/skills/design/references/cip-deliverable-guide.md +95 -0
  643. package/skills/ui-ux-pro-max/.claude/skills/design/references/cip-design.md +121 -0
  644. package/skills/ui-ux-pro-max/.claude/skills/design/references/cip-prompt-engineering.md +84 -0
  645. package/skills/ui-ux-pro-max/.claude/skills/design/references/cip-style-guide.md +68 -0
  646. package/skills/ui-ux-pro-max/.claude/skills/design/references/design-routing.md +207 -0
  647. package/skills/ui-ux-pro-max/.claude/skills/design/references/icon-design.md +122 -0
  648. package/skills/ui-ux-pro-max/.claude/skills/design/references/logo-color-psychology.md +101 -0
  649. package/skills/ui-ux-pro-max/.claude/skills/design/references/logo-design.md +92 -0
  650. package/skills/ui-ux-pro-max/.claude/skills/design/references/logo-prompt-engineering.md +158 -0
  651. package/skills/ui-ux-pro-max/.claude/skills/design/references/logo-style-guide.md +109 -0
  652. package/skills/ui-ux-pro-max/.claude/skills/design/references/slides-copywriting-formulas.md +84 -0
  653. package/skills/ui-ux-pro-max/.claude/skills/design/references/slides-create.md +4 -0
  654. package/skills/ui-ux-pro-max/.claude/skills/design/references/slides-html-template.md +295 -0
  655. package/skills/ui-ux-pro-max/.claude/skills/design/references/slides-layout-patterns.md +137 -0
  656. package/skills/ui-ux-pro-max/.claude/skills/design/references/slides-strategies.md +94 -0
  657. package/skills/ui-ux-pro-max/.claude/skills/design/references/slides.md +42 -0
  658. package/skills/ui-ux-pro-max/.claude/skills/design/references/social-photos-design.md +329 -0
  659. package/skills/ui-ux-pro-max/.claude/skills/design/scripts/cip/core.py +215 -0
  660. package/skills/ui-ux-pro-max/.claude/skills/design/scripts/cip/generate.py +484 -0
  661. package/skills/ui-ux-pro-max/.claude/skills/design/scripts/cip/render-html.py +424 -0
  662. package/skills/ui-ux-pro-max/.claude/skills/design/scripts/cip/search.py +127 -0
  663. package/skills/ui-ux-pro-max/.claude/skills/design/scripts/icon/generate.py +487 -0
  664. package/skills/ui-ux-pro-max/.claude/skills/design/scripts/logo/core.py +175 -0
  665. package/skills/ui-ux-pro-max/.claude/skills/design/scripts/logo/generate.py +362 -0
  666. package/skills/ui-ux-pro-max/.claude/skills/design/scripts/logo/search.py +114 -0
  667. package/skills/ui-ux-pro-max/.claude/skills/design-system/SKILL.md +244 -0
  668. package/skills/ui-ux-pro-max/.claude/skills/design-system/data/slide-backgrounds.csv +11 -0
  669. package/skills/ui-ux-pro-max/.claude/skills/design-system/data/slide-charts.csv +26 -0
  670. package/skills/ui-ux-pro-max/.claude/skills/design-system/data/slide-color-logic.csv +14 -0
  671. package/skills/ui-ux-pro-max/.claude/skills/design-system/data/slide-copy.csv +26 -0
  672. package/skills/ui-ux-pro-max/.claude/skills/design-system/data/slide-layout-logic.csv +16 -0
  673. package/skills/ui-ux-pro-max/.claude/skills/design-system/data/slide-layouts.csv +26 -0
  674. package/skills/ui-ux-pro-max/.claude/skills/design-system/data/slide-strategies.csv +16 -0
  675. package/skills/ui-ux-pro-max/.claude/skills/design-system/data/slide-typography.csv +15 -0
  676. package/skills/ui-ux-pro-max/.claude/skills/design-system/references/component-specs.md +236 -0
  677. package/skills/ui-ux-pro-max/.claude/skills/design-system/references/component-tokens.md +214 -0
  678. package/skills/ui-ux-pro-max/.claude/skills/design-system/references/primitive-tokens.md +203 -0
  679. package/skills/ui-ux-pro-max/.claude/skills/design-system/references/semantic-tokens.md +215 -0
  680. package/skills/ui-ux-pro-max/.claude/skills/design-system/references/states-and-variants.md +241 -0
  681. package/skills/ui-ux-pro-max/.claude/skills/design-system/references/tailwind-integration.md +251 -0
  682. package/skills/ui-ux-pro-max/.claude/skills/design-system/references/token-architecture.md +224 -0
  683. package/skills/ui-ux-pro-max/.claude/skills/design-system/scripts/embed-tokens.cjs +99 -0
  684. package/skills/ui-ux-pro-max/.claude/skills/design-system/scripts/fetch-background.py +317 -0
  685. package/skills/ui-ux-pro-max/.claude/skills/design-system/scripts/generate-slide.py +753 -0
  686. package/skills/ui-ux-pro-max/.claude/skills/design-system/scripts/generate-tokens.cjs +205 -0
  687. package/skills/ui-ux-pro-max/.claude/skills/design-system/scripts/html-token-validator.py +327 -0
  688. package/skills/ui-ux-pro-max/.claude/skills/design-system/scripts/search-slides.py +218 -0
  689. package/skills/ui-ux-pro-max/.claude/skills/design-system/scripts/slide-token-validator.py +35 -0
  690. package/skills/ui-ux-pro-max/.claude/skills/design-system/scripts/slide_search_core.py +453 -0
  691. package/skills/ui-ux-pro-max/.claude/skills/design-system/scripts/validate-tokens.cjs +251 -0
  692. package/skills/ui-ux-pro-max/.claude/skills/design-system/templates/design-tokens-starter.json +143 -0
  693. package/skills/ui-ux-pro-max/.claude/skills/slides/SKILL.md +42 -0
  694. package/skills/ui-ux-pro-max/.claude/skills/slides/references/copywriting-formulas.md +84 -0
  695. package/skills/ui-ux-pro-max/.claude/skills/slides/references/create.md +4 -0
  696. package/skills/ui-ux-pro-max/.claude/skills/slides/references/html-template.md +295 -0
  697. package/skills/ui-ux-pro-max/.claude/skills/slides/references/layout-patterns.md +137 -0
  698. package/skills/ui-ux-pro-max/.claude/skills/slides/references/slide-strategies.md +94 -0
  699. package/skills/ui-ux-pro-max/.claude/skills/ui-styling/LICENSE.txt +202 -0
  700. package/skills/ui-ux-pro-max/.claude/skills/ui-styling/SKILL.md +324 -0
  701. package/skills/ui-ux-pro-max/.claude/skills/ui-styling/canvas-fonts/ArsenalSC-OFL.txt +93 -0
  702. package/skills/ui-ux-pro-max/.claude/skills/ui-styling/canvas-fonts/ArsenalSC-Regular.ttf +0 -0
  703. package/skills/ui-ux-pro-max/.claude/skills/ui-styling/canvas-fonts/BigShoulders-Bold.ttf +0 -0
  704. package/skills/ui-ux-pro-max/.claude/skills/ui-styling/canvas-fonts/BigShoulders-OFL.txt +93 -0
  705. package/skills/ui-ux-pro-max/.claude/skills/ui-styling/canvas-fonts/BigShoulders-Regular.ttf +0 -0
  706. package/skills/ui-ux-pro-max/.claude/skills/ui-styling/canvas-fonts/Boldonse-OFL.txt +93 -0
  707. package/skills/ui-ux-pro-max/.claude/skills/ui-styling/canvas-fonts/Boldonse-Regular.ttf +0 -0
  708. package/skills/ui-ux-pro-max/.claude/skills/ui-styling/canvas-fonts/BricolageGrotesque-Bold.ttf +0 -0
  709. package/skills/ui-ux-pro-max/.claude/skills/ui-styling/canvas-fonts/BricolageGrotesque-OFL.txt +93 -0
  710. package/skills/ui-ux-pro-max/.claude/skills/ui-styling/canvas-fonts/BricolageGrotesque-Regular.ttf +0 -0
  711. package/skills/ui-ux-pro-max/.claude/skills/ui-styling/canvas-fonts/CrimsonPro-Bold.ttf +0 -0
  712. package/skills/ui-ux-pro-max/.claude/skills/ui-styling/canvas-fonts/CrimsonPro-Italic.ttf +0 -0
  713. package/skills/ui-ux-pro-max/.claude/skills/ui-styling/canvas-fonts/CrimsonPro-OFL.txt +93 -0
  714. package/skills/ui-ux-pro-max/.claude/skills/ui-styling/canvas-fonts/CrimsonPro-Regular.ttf +0 -0
  715. package/skills/ui-ux-pro-max/.claude/skills/ui-styling/canvas-fonts/DMMono-OFL.txt +93 -0
  716. package/skills/ui-ux-pro-max/.claude/skills/ui-styling/canvas-fonts/DMMono-Regular.ttf +0 -0
  717. package/skills/ui-ux-pro-max/.claude/skills/ui-styling/canvas-fonts/EricaOne-OFL.txt +94 -0
  718. package/skills/ui-ux-pro-max/.claude/skills/ui-styling/canvas-fonts/EricaOne-Regular.ttf +0 -0
  719. package/skills/ui-ux-pro-max/.claude/skills/ui-styling/canvas-fonts/GeistMono-Bold.ttf +0 -0
  720. package/skills/ui-ux-pro-max/.claude/skills/ui-styling/canvas-fonts/GeistMono-OFL.txt +93 -0
  721. package/skills/ui-ux-pro-max/.claude/skills/ui-styling/canvas-fonts/GeistMono-Regular.ttf +0 -0
  722. package/skills/ui-ux-pro-max/.claude/skills/ui-styling/canvas-fonts/Gloock-OFL.txt +93 -0
  723. package/skills/ui-ux-pro-max/.claude/skills/ui-styling/canvas-fonts/Gloock-Regular.ttf +0 -0
  724. package/skills/ui-ux-pro-max/.claude/skills/ui-styling/canvas-fonts/IBMPlexMono-Bold.ttf +0 -0
  725. package/skills/ui-ux-pro-max/.claude/skills/ui-styling/canvas-fonts/IBMPlexMono-OFL.txt +93 -0
  726. package/skills/ui-ux-pro-max/.claude/skills/ui-styling/canvas-fonts/IBMPlexMono-Regular.ttf +0 -0
  727. package/skills/ui-ux-pro-max/.claude/skills/ui-styling/canvas-fonts/IBMPlexSerif-Bold.ttf +0 -0
  728. package/skills/ui-ux-pro-max/.claude/skills/ui-styling/canvas-fonts/IBMPlexSerif-BoldItalic.ttf +0 -0
  729. package/skills/ui-ux-pro-max/.claude/skills/ui-styling/canvas-fonts/IBMPlexSerif-Italic.ttf +0 -0
  730. package/skills/ui-ux-pro-max/.claude/skills/ui-styling/canvas-fonts/IBMPlexSerif-Regular.ttf +0 -0
  731. package/skills/ui-ux-pro-max/.claude/skills/ui-styling/canvas-fonts/InstrumentSans-Bold.ttf +0 -0
  732. package/skills/ui-ux-pro-max/.claude/skills/ui-styling/canvas-fonts/InstrumentSans-BoldItalic.ttf +0 -0
  733. package/skills/ui-ux-pro-max/.claude/skills/ui-styling/canvas-fonts/InstrumentSans-Italic.ttf +0 -0
  734. package/skills/ui-ux-pro-max/.claude/skills/ui-styling/canvas-fonts/InstrumentSans-OFL.txt +93 -0
  735. package/skills/ui-ux-pro-max/.claude/skills/ui-styling/canvas-fonts/InstrumentSans-Regular.ttf +0 -0
  736. package/skills/ui-ux-pro-max/.claude/skills/ui-styling/canvas-fonts/InstrumentSerif-Italic.ttf +0 -0
  737. package/skills/ui-ux-pro-max/.claude/skills/ui-styling/canvas-fonts/InstrumentSerif-Regular.ttf +0 -0
  738. package/skills/ui-ux-pro-max/.claude/skills/ui-styling/canvas-fonts/Italiana-OFL.txt +93 -0
  739. package/skills/ui-ux-pro-max/.claude/skills/ui-styling/canvas-fonts/Italiana-Regular.ttf +0 -0
  740. package/skills/ui-ux-pro-max/.claude/skills/ui-styling/canvas-fonts/JetBrainsMono-Bold.ttf +0 -0
  741. package/skills/ui-ux-pro-max/.claude/skills/ui-styling/canvas-fonts/JetBrainsMono-OFL.txt +93 -0
  742. package/skills/ui-ux-pro-max/.claude/skills/ui-styling/canvas-fonts/JetBrainsMono-Regular.ttf +0 -0
  743. package/skills/ui-ux-pro-max/.claude/skills/ui-styling/canvas-fonts/Jura-Light.ttf +0 -0
  744. package/skills/ui-ux-pro-max/.claude/skills/ui-styling/canvas-fonts/Jura-Medium.ttf +0 -0
  745. package/skills/ui-ux-pro-max/.claude/skills/ui-styling/canvas-fonts/Jura-OFL.txt +93 -0
  746. package/skills/ui-ux-pro-max/.claude/skills/ui-styling/canvas-fonts/LibreBaskerville-OFL.txt +93 -0
  747. package/skills/ui-ux-pro-max/.claude/skills/ui-styling/canvas-fonts/LibreBaskerville-Regular.ttf +0 -0
  748. package/skills/ui-ux-pro-max/.claude/skills/ui-styling/canvas-fonts/Lora-Bold.ttf +0 -0
  749. package/skills/ui-ux-pro-max/.claude/skills/ui-styling/canvas-fonts/Lora-BoldItalic.ttf +0 -0
  750. package/skills/ui-ux-pro-max/.claude/skills/ui-styling/canvas-fonts/Lora-Italic.ttf +0 -0
  751. package/skills/ui-ux-pro-max/.claude/skills/ui-styling/canvas-fonts/Lora-OFL.txt +93 -0
  752. package/skills/ui-ux-pro-max/.claude/skills/ui-styling/canvas-fonts/Lora-Regular.ttf +0 -0
  753. package/skills/ui-ux-pro-max/.claude/skills/ui-styling/canvas-fonts/NationalPark-Bold.ttf +0 -0
  754. package/skills/ui-ux-pro-max/.claude/skills/ui-styling/canvas-fonts/NationalPark-OFL.txt +93 -0
  755. package/skills/ui-ux-pro-max/.claude/skills/ui-styling/canvas-fonts/NationalPark-Regular.ttf +0 -0
  756. package/skills/ui-ux-pro-max/.claude/skills/ui-styling/canvas-fonts/NothingYouCouldDo-OFL.txt +93 -0
  757. package/skills/ui-ux-pro-max/.claude/skills/ui-styling/canvas-fonts/NothingYouCouldDo-Regular.ttf +0 -0
  758. package/skills/ui-ux-pro-max/.claude/skills/ui-styling/canvas-fonts/Outfit-Bold.ttf +0 -0
  759. package/skills/ui-ux-pro-max/.claude/skills/ui-styling/canvas-fonts/Outfit-OFL.txt +93 -0
  760. package/skills/ui-ux-pro-max/.claude/skills/ui-styling/canvas-fonts/Outfit-Regular.ttf +0 -0
  761. package/skills/ui-ux-pro-max/.claude/skills/ui-styling/canvas-fonts/PixelifySans-Medium.ttf +0 -0
  762. package/skills/ui-ux-pro-max/.claude/skills/ui-styling/canvas-fonts/PixelifySans-OFL.txt +93 -0
  763. package/skills/ui-ux-pro-max/.claude/skills/ui-styling/canvas-fonts/PoiretOne-OFL.txt +93 -0
  764. package/skills/ui-ux-pro-max/.claude/skills/ui-styling/canvas-fonts/PoiretOne-Regular.ttf +0 -0
  765. package/skills/ui-ux-pro-max/.claude/skills/ui-styling/canvas-fonts/RedHatMono-Bold.ttf +0 -0
  766. package/skills/ui-ux-pro-max/.claude/skills/ui-styling/canvas-fonts/RedHatMono-OFL.txt +93 -0
  767. package/skills/ui-ux-pro-max/.claude/skills/ui-styling/canvas-fonts/RedHatMono-Regular.ttf +0 -0
  768. package/skills/ui-ux-pro-max/.claude/skills/ui-styling/canvas-fonts/Silkscreen-OFL.txt +93 -0
  769. package/skills/ui-ux-pro-max/.claude/skills/ui-styling/canvas-fonts/Silkscreen-Regular.ttf +0 -0
  770. package/skills/ui-ux-pro-max/.claude/skills/ui-styling/canvas-fonts/SmoochSans-Medium.ttf +0 -0
  771. package/skills/ui-ux-pro-max/.claude/skills/ui-styling/canvas-fonts/SmoochSans-OFL.txt +93 -0
  772. package/skills/ui-ux-pro-max/.claude/skills/ui-styling/canvas-fonts/Tektur-Medium.ttf +0 -0
  773. package/skills/ui-ux-pro-max/.claude/skills/ui-styling/canvas-fonts/Tektur-OFL.txt +93 -0
  774. package/skills/ui-ux-pro-max/.claude/skills/ui-styling/canvas-fonts/Tektur-Regular.ttf +0 -0
  775. package/skills/ui-ux-pro-max/.claude/skills/ui-styling/canvas-fonts/WorkSans-Bold.ttf +0 -0
  776. package/skills/ui-ux-pro-max/.claude/skills/ui-styling/canvas-fonts/WorkSans-BoldItalic.ttf +0 -0
  777. package/skills/ui-ux-pro-max/.claude/skills/ui-styling/canvas-fonts/WorkSans-Italic.ttf +0 -0
  778. package/skills/ui-ux-pro-max/.claude/skills/ui-styling/canvas-fonts/WorkSans-OFL.txt +93 -0
  779. package/skills/ui-ux-pro-max/.claude/skills/ui-styling/canvas-fonts/WorkSans-Regular.ttf +0 -0
  780. package/skills/ui-ux-pro-max/.claude/skills/ui-styling/canvas-fonts/YoungSerif-OFL.txt +93 -0
  781. package/skills/ui-ux-pro-max/.claude/skills/ui-styling/canvas-fonts/YoungSerif-Regular.ttf +0 -0
  782. package/skills/ui-ux-pro-max/.claude/skills/ui-styling/references/canvas-design-system.md +320 -0
  783. package/skills/ui-ux-pro-max/.claude/skills/ui-styling/references/shadcn-accessibility.md +471 -0
  784. package/skills/ui-ux-pro-max/.claude/skills/ui-styling/references/shadcn-components.md +424 -0
  785. package/skills/ui-ux-pro-max/.claude/skills/ui-styling/references/shadcn-theming.md +373 -0
  786. package/skills/ui-ux-pro-max/.claude/skills/ui-styling/references/tailwind-customization.md +483 -0
  787. package/skills/ui-ux-pro-max/.claude/skills/ui-styling/references/tailwind-responsive.md +382 -0
  788. package/skills/ui-ux-pro-max/.claude/skills/ui-styling/references/tailwind-utilities.md +455 -0
  789. package/skills/ui-ux-pro-max/.claude/skills/ui-styling/scripts/.coverage +0 -0
  790. package/skills/ui-ux-pro-max/.claude/skills/ui-styling/scripts/requirements.txt +17 -0
  791. package/skills/ui-ux-pro-max/.claude/skills/ui-styling/scripts/shadcn_add.py +292 -0
  792. package/skills/ui-ux-pro-max/.claude/skills/ui-styling/scripts/tailwind_config_gen.py +456 -0
  793. package/skills/ui-ux-pro-max/.claude/skills/ui-styling/scripts/tests/coverage-ui.json +1 -0
  794. package/skills/ui-ux-pro-max/.claude/skills/ui-styling/scripts/tests/requirements.txt +3 -0
  795. package/skills/ui-ux-pro-max/.claude/skills/ui-styling/scripts/tests/test_shadcn_add.py +266 -0
  796. package/skills/ui-ux-pro-max/.claude/skills/ui-styling/scripts/tests/test_tailwind_config_gen.py +336 -0
  797. package/skills/ui-ux-pro-max/.claude/skills/ui-ux-pro-max/SKILL.md +659 -0
  798. package/skills/ui-ux-pro-max/.claude-plugin/marketplace.json +35 -0
  799. package/skills/ui-ux-pro-max/.claude-plugin/plugin.json +11 -0
  800. package/skills/ui-ux-pro-max/.github/workflows/claude-code-review.yml +44 -0
  801. package/skills/ui-ux-pro-max/.github/workflows/claude.yml +50 -0
  802. package/skills/ui-ux-pro-max/.github/workflows/python-package-conda.yml +34 -0
  803. package/skills/ui-ux-pro-max/CLAUDE.md +98 -0
  804. package/skills/ui-ux-pro-max/LICENSE +21 -0
  805. package/skills/ui-ux-pro-max/README.md +513 -0
  806. package/skills/ui-ux-pro-max/SKILL.md +292 -0
  807. package/skills/ui-ux-pro-max/cli/README.md +63 -0
  808. package/skills/ui-ux-pro-max/cli/assets/data/_sync_all.py +414 -0
  809. package/skills/ui-ux-pro-max/cli/assets/data/app-interface.csv +31 -0
  810. package/skills/ui-ux-pro-max/cli/assets/data/charts.csv +26 -0
  811. package/skills/ui-ux-pro-max/cli/assets/data/colors.csv +162 -0
  812. package/skills/ui-ux-pro-max/cli/assets/data/design.csv +1776 -0
  813. package/skills/ui-ux-pro-max/cli/assets/data/draft.csv +1779 -0
  814. package/skills/ui-ux-pro-max/cli/assets/data/google-fonts.csv +1924 -0
  815. package/skills/ui-ux-pro-max/cli/assets/data/icons.csv +105 -0
  816. package/skills/ui-ux-pro-max/cli/assets/data/landing.csv +31 -0
  817. package/skills/ui-ux-pro-max/cli/assets/data/products.csv +162 -0
  818. package/skills/ui-ux-pro-max/cli/assets/data/react-performance.csv +45 -0
  819. package/skills/ui-ux-pro-max/cli/assets/data/stacks/angular.csv +51 -0
  820. package/skills/ui-ux-pro-max/cli/assets/data/stacks/astro.csv +54 -0
  821. package/skills/ui-ux-pro-max/cli/assets/data/stacks/flutter.csv +53 -0
  822. package/skills/ui-ux-pro-max/cli/assets/data/stacks/html-tailwind.csv +56 -0
  823. package/skills/ui-ux-pro-max/cli/assets/data/stacks/jetpack-compose.csv +53 -0
  824. package/skills/ui-ux-pro-max/cli/assets/data/stacks/laravel.csv +51 -0
  825. package/skills/ui-ux-pro-max/cli/assets/data/stacks/nextjs.csv +53 -0
  826. package/skills/ui-ux-pro-max/cli/assets/data/stacks/nuxt-ui.csv +51 -0
  827. package/skills/ui-ux-pro-max/cli/assets/data/stacks/nuxtjs.csv +59 -0
  828. package/skills/ui-ux-pro-max/cli/assets/data/stacks/react-native.csv +52 -0
  829. package/skills/ui-ux-pro-max/cli/assets/data/stacks/react.csv +54 -0
  830. package/skills/ui-ux-pro-max/cli/assets/data/stacks/shadcn.csv +61 -0
  831. package/skills/ui-ux-pro-max/cli/assets/data/stacks/svelte.csv +54 -0
  832. package/skills/ui-ux-pro-max/cli/assets/data/stacks/swiftui.csv +51 -0
  833. package/skills/ui-ux-pro-max/cli/assets/data/stacks/threejs.csv +54 -0
  834. package/skills/ui-ux-pro-max/cli/assets/data/stacks/vue.csv +50 -0
  835. package/skills/ui-ux-pro-max/cli/assets/data/styles.csv +85 -0
  836. package/skills/ui-ux-pro-max/cli/assets/data/typography.csv +74 -0
  837. package/skills/ui-ux-pro-max/cli/assets/data/ui-reasoning.csv +162 -0
  838. package/skills/ui-ux-pro-max/cli/assets/data/ux-guidelines.csv +100 -0
  839. package/skills/ui-ux-pro-max/cli/assets/scripts/core.py +247 -0
  840. package/skills/ui-ux-pro-max/cli/assets/scripts/design_system.py +1148 -0
  841. package/skills/ui-ux-pro-max/cli/assets/scripts/search.py +114 -0
  842. package/skills/ui-ux-pro-max/cli/assets/templates/base/quick-reference.md +297 -0
  843. package/skills/ui-ux-pro-max/cli/assets/templates/base/skill-content.md +353 -0
  844. package/skills/ui-ux-pro-max/cli/assets/templates/platforms/agent.json +21 -0
  845. package/skills/ui-ux-pro-max/cli/assets/templates/platforms/augment.json +18 -0
  846. package/skills/ui-ux-pro-max/cli/assets/templates/platforms/claude.json +21 -0
  847. package/skills/ui-ux-pro-max/cli/assets/templates/platforms/codebuddy.json +21 -0
  848. package/skills/ui-ux-pro-max/cli/assets/templates/platforms/codex.json +21 -0
  849. package/skills/ui-ux-pro-max/cli/assets/templates/platforms/continue.json +21 -0
  850. package/skills/ui-ux-pro-max/cli/assets/templates/platforms/copilot.json +21 -0
  851. package/skills/ui-ux-pro-max/cli/assets/templates/platforms/cursor.json +21 -0
  852. package/skills/ui-ux-pro-max/cli/assets/templates/platforms/droid.json +21 -0
  853. package/skills/ui-ux-pro-max/cli/assets/templates/platforms/gemini.json +21 -0
  854. package/skills/ui-ux-pro-max/cli/assets/templates/platforms/kilocode.json +21 -0
  855. package/skills/ui-ux-pro-max/cli/assets/templates/platforms/kiro.json +21 -0
  856. package/skills/ui-ux-pro-max/cli/assets/templates/platforms/opencode.json +21 -0
  857. package/skills/ui-ux-pro-max/cli/assets/templates/platforms/qoder.json +21 -0
  858. package/skills/ui-ux-pro-max/cli/assets/templates/platforms/roocode.json +21 -0
  859. package/skills/ui-ux-pro-max/cli/assets/templates/platforms/trae.json +21 -0
  860. package/skills/ui-ux-pro-max/cli/assets/templates/platforms/warp.json +18 -0
  861. package/skills/ui-ux-pro-max/cli/assets/templates/platforms/windsurf.json +21 -0
  862. package/skills/ui-ux-pro-max/cli/bun.lock +77 -0
  863. package/skills/ui-ux-pro-max/cli/package-lock.json +370 -0
  864. package/skills/ui-ux-pro-max/cli/package.json +48 -0
  865. package/skills/ui-ux-pro-max/cli/src/commands/init.ts +216 -0
  866. package/skills/ui-ux-pro-max/cli/src/commands/uninstall.ts +135 -0
  867. package/skills/ui-ux-pro-max/cli/src/commands/update.ts +36 -0
  868. package/skills/ui-ux-pro-max/cli/src/commands/versions.ts +42 -0
  869. package/skills/ui-ux-pro-max/cli/src/index.ts +83 -0
  870. package/skills/ui-ux-pro-max/cli/src/types/index.ts +68 -0
  871. package/skills/ui-ux-pro-max/cli/src/utils/detect.ts +120 -0
  872. package/skills/ui-ux-pro-max/cli/src/utils/extract.ts +149 -0
  873. package/skills/ui-ux-pro-max/cli/src/utils/github.ts +104 -0
  874. package/skills/ui-ux-pro-max/cli/src/utils/logger.ts +11 -0
  875. package/skills/ui-ux-pro-max/cli/src/utils/template.ts +243 -0
  876. package/skills/ui-ux-pro-max/cli/tsconfig.json +17 -0
  877. package/skills/ui-ux-pro-max/data/charts.csv +26 -0
  878. package/skills/ui-ux-pro-max/data/colors.csv +97 -0
  879. package/skills/ui-ux-pro-max/data/icons.csv +101 -0
  880. package/skills/ui-ux-pro-max/data/landing.csv +31 -0
  881. package/skills/ui-ux-pro-max/data/products.csv +97 -0
  882. package/skills/ui-ux-pro-max/data/react-performance.csv +45 -0
  883. package/skills/ui-ux-pro-max/data/stacks/astro.csv +54 -0
  884. package/skills/ui-ux-pro-max/data/stacks/flutter.csv +53 -0
  885. package/skills/ui-ux-pro-max/data/stacks/html-tailwind.csv +56 -0
  886. package/skills/ui-ux-pro-max/data/stacks/jetpack-compose.csv +53 -0
  887. package/skills/ui-ux-pro-max/data/stacks/nextjs.csv +53 -0
  888. package/skills/ui-ux-pro-max/data/stacks/nuxt-ui.csv +51 -0
  889. package/skills/ui-ux-pro-max/data/stacks/nuxtjs.csv +59 -0
  890. package/skills/ui-ux-pro-max/data/stacks/react-native.csv +52 -0
  891. package/skills/ui-ux-pro-max/data/stacks/react.csv +54 -0
  892. package/skills/ui-ux-pro-max/data/stacks/shadcn.csv +61 -0
  893. package/skills/ui-ux-pro-max/data/stacks/svelte.csv +54 -0
  894. package/skills/ui-ux-pro-max/data/stacks/swiftui.csv +51 -0
  895. package/skills/ui-ux-pro-max/data/stacks/vue.csv +50 -0
  896. package/skills/ui-ux-pro-max/data/styles.csv +68 -0
  897. package/skills/ui-ux-pro-max/data/typography.csv +58 -0
  898. package/skills/ui-ux-pro-max/data/ui-reasoning.csv +101 -0
  899. package/skills/ui-ux-pro-max/data/ux-guidelines.csv +100 -0
  900. package/skills/ui-ux-pro-max/data/web-interface.csv +31 -0
  901. package/skills/ui-ux-pro-max/docs//344/270/211/344/270/252 data-scripts-templates /347/232/204/345/214/272/345/210/253.md" +30 -0
  902. package/skills/ui-ux-pro-max/preview/xiaomaomi-app.html +411 -0
  903. package/skills/ui-ux-pro-max/screenshots/website.png +0 -0
  904. package/skills/ui-ux-pro-max/scripts/core.py +253 -0
  905. package/skills/ui-ux-pro-max/scripts/design_system.py +1067 -0
  906. package/skills/ui-ux-pro-max/scripts/search.py +114 -0
  907. package/skills/ui-ux-pro-max/skill.json +41 -0
  908. package/skills/ui-ux-pro-max/src/ui-ux-pro-max/data/_sync_all.py +414 -0
  909. package/skills/ui-ux-pro-max/src/ui-ux-pro-max/data/app-interface.csv +31 -0
  910. package/skills/ui-ux-pro-max/src/ui-ux-pro-max/data/charts.csv +26 -0
  911. package/skills/ui-ux-pro-max/src/ui-ux-pro-max/data/colors.csv +162 -0
  912. package/skills/ui-ux-pro-max/src/ui-ux-pro-max/data/design.csv +1776 -0
  913. package/skills/ui-ux-pro-max/src/ui-ux-pro-max/data/draft.csv +1779 -0
  914. package/skills/ui-ux-pro-max/src/ui-ux-pro-max/data/google-fonts.csv +1924 -0
  915. package/skills/ui-ux-pro-max/src/ui-ux-pro-max/data/icons.csv +106 -0
  916. package/skills/ui-ux-pro-max/src/ui-ux-pro-max/data/landing.csv +35 -0
  917. package/skills/ui-ux-pro-max/src/ui-ux-pro-max/data/products.csv +162 -0
  918. package/skills/ui-ux-pro-max/src/ui-ux-pro-max/data/react-performance.csv +45 -0
  919. package/skills/ui-ux-pro-max/src/ui-ux-pro-max/data/stacks/angular.csv +51 -0
  920. package/skills/ui-ux-pro-max/src/ui-ux-pro-max/data/stacks/astro.csv +54 -0
  921. package/skills/ui-ux-pro-max/src/ui-ux-pro-max/data/stacks/flutter.csv +53 -0
  922. package/skills/ui-ux-pro-max/src/ui-ux-pro-max/data/stacks/html-tailwind.csv +56 -0
  923. package/skills/ui-ux-pro-max/src/ui-ux-pro-max/data/stacks/jetpack-compose.csv +53 -0
  924. package/skills/ui-ux-pro-max/src/ui-ux-pro-max/data/stacks/laravel.csv +51 -0
  925. package/skills/ui-ux-pro-max/src/ui-ux-pro-max/data/stacks/nextjs.csv +53 -0
  926. package/skills/ui-ux-pro-max/src/ui-ux-pro-max/data/stacks/nuxt-ui.csv +51 -0
  927. package/skills/ui-ux-pro-max/src/ui-ux-pro-max/data/stacks/nuxtjs.csv +59 -0
  928. package/skills/ui-ux-pro-max/src/ui-ux-pro-max/data/stacks/react-native.csv +52 -0
  929. package/skills/ui-ux-pro-max/src/ui-ux-pro-max/data/stacks/react.csv +54 -0
  930. package/skills/ui-ux-pro-max/src/ui-ux-pro-max/data/stacks/shadcn.csv +61 -0
  931. package/skills/ui-ux-pro-max/src/ui-ux-pro-max/data/stacks/svelte.csv +54 -0
  932. package/skills/ui-ux-pro-max/src/ui-ux-pro-max/data/stacks/swiftui.csv +51 -0
  933. package/skills/ui-ux-pro-max/src/ui-ux-pro-max/data/stacks/threejs.csv +54 -0
  934. package/skills/ui-ux-pro-max/src/ui-ux-pro-max/data/stacks/vue.csv +50 -0
  935. package/skills/ui-ux-pro-max/src/ui-ux-pro-max/data/styles.csv +85 -0
  936. package/skills/ui-ux-pro-max/src/ui-ux-pro-max/data/typography.csv +74 -0
  937. package/skills/ui-ux-pro-max/src/ui-ux-pro-max/data/ui-reasoning.csv +162 -0
  938. package/skills/ui-ux-pro-max/src/ui-ux-pro-max/data/ux-guidelines.csv +100 -0
  939. package/skills/ui-ux-pro-max/src/ui-ux-pro-max/scripts/core.py +262 -0
  940. package/skills/ui-ux-pro-max/src/ui-ux-pro-max/scripts/design_system.py +1148 -0
  941. package/skills/ui-ux-pro-max/src/ui-ux-pro-max/scripts/search.py +114 -0
  942. package/skills/ui-ux-pro-max/src/ui-ux-pro-max/templates/base/quick-reference.md +297 -0
  943. package/skills/ui-ux-pro-max/src/ui-ux-pro-max/templates/base/skill-content.md +358 -0
  944. package/skills/ui-ux-pro-max/src/ui-ux-pro-max/templates/platforms/agent.json +21 -0
  945. package/skills/ui-ux-pro-max/src/ui-ux-pro-max/templates/platforms/augment.json +18 -0
  946. package/skills/ui-ux-pro-max/src/ui-ux-pro-max/templates/platforms/claude.json +21 -0
  947. package/skills/ui-ux-pro-max/src/ui-ux-pro-max/templates/platforms/codebuddy.json +21 -0
  948. package/skills/ui-ux-pro-max/src/ui-ux-pro-max/templates/platforms/codex.json +21 -0
  949. package/skills/ui-ux-pro-max/src/ui-ux-pro-max/templates/platforms/continue.json +21 -0
  950. package/skills/ui-ux-pro-max/src/ui-ux-pro-max/templates/platforms/copilot.json +21 -0
  951. package/skills/ui-ux-pro-max/src/ui-ux-pro-max/templates/platforms/cursor.json +21 -0
  952. package/skills/ui-ux-pro-max/src/ui-ux-pro-max/templates/platforms/droid.json +21 -0
  953. package/skills/ui-ux-pro-max/src/ui-ux-pro-max/templates/platforms/gemini.json +21 -0
  954. package/skills/ui-ux-pro-max/src/ui-ux-pro-max/templates/platforms/kilocode.json +21 -0
  955. package/skills/ui-ux-pro-max/src/ui-ux-pro-max/templates/platforms/kiro.json +21 -0
  956. package/skills/ui-ux-pro-max/src/ui-ux-pro-max/templates/platforms/opencode.json +21 -0
  957. package/skills/ui-ux-pro-max/src/ui-ux-pro-max/templates/platforms/qoder.json +21 -0
  958. package/skills/ui-ux-pro-max/src/ui-ux-pro-max/templates/platforms/roocode.json +21 -0
  959. package/skills/ui-ux-pro-max/src/ui-ux-pro-max/templates/platforms/trae.json +21 -0
  960. package/skills/ui-ux-pro-max/src/ui-ux-pro-max/templates/platforms/warp.json +18 -0
  961. package/skills/ui-ux-pro-max/src/ui-ux-pro-max/templates/platforms/windsurf.json +21 -0
  962. package/skills/uncloud/SKILL.md +343 -0
  963. package/skills/upgrading-react-native/SKILL.md +58 -0
  964. package/skills/using-git-worktrees/SKILL.md +215 -0
  965. package/skills/vercel/SKILL.md +161 -0
  966. package/skills/vercel/metadata.json +16 -0
  967. package/skills/verification-loop/SKILL.md +126 -0
  968. package/skills/view-transitions-api/SKILL.md +108 -0
  969. package/skills/vite-patterns/SKILL.md +449 -0
  970. package/skills/vue3-composition-patterns/SKILL.md +375 -0
  971. package/skills/waza-habits/SKILL.md +22 -0
  972. package/skills/webapp-testing/LICENSE.txt +202 -0
  973. package/skills/webapp-testing/SKILL.md +96 -0
  974. package/skills/webapp-testing/examples/console_logging.py +35 -0
  975. package/skills/webapp-testing/examples/element_discovery.py +40 -0
  976. package/skills/webapp-testing/examples/static_html_automation.py +33 -0
  977. package/skills/webapp-testing/scripts/with_server.py +106 -0
  978. package/src/orchestrator/builder.ts +234 -0
  979. package/src/orchestrator/executor.ts +386 -0
  980. package/src/orchestrator/index.ts +535 -0
  981. package/src/orchestrator/loader.ts +237 -0
  982. package/src/orchestrator/registry.ts +211 -0
  983. package/src/orchestrator/schema.ts +141 -0
  984. package/src/orchestrator/session.ts +128 -0
  985. package/src/orchestrator/types.ts +109 -0
  986. package/tsconfig.json +21 -0
@@ -0,0 +1,2899 @@
1
+ """graphify CLI - `graphify install` sets up the Claude Code skill."""
2
+ from __future__ import annotations
3
+ import json
4
+ import os
5
+ import platform
6
+ import re
7
+ import shutil
8
+ import sys
9
+ from pathlib import Path
10
+
11
+ try:
12
+ from importlib.metadata import version as _pkg_version
13
+ __version__ = _pkg_version("graphifyy")
14
+ except Exception:
15
+ __version__ = "unknown"
16
+
17
+ # Output directory — override with GRAPHIFY_OUT env var for worktrees or shared-output setups.
18
+ # Accepts a relative name ("graphify-out-feature") or an absolute path ("/shared/graphify-out").
19
+ _GRAPHIFY_OUT = os.environ.get("GRAPHIFY_OUT", "graphify-out")
20
+
21
+
22
+ def _default_graph_path() -> str:
23
+ return str(Path(_GRAPHIFY_OUT) / "graph.json")
24
+
25
+
26
+ def _check_skill_version(skill_dst: Path) -> None:
27
+ """Warn if the installed skill is from an older graphify version."""
28
+ version_file = skill_dst.parent / ".graphify_version"
29
+ if not version_file.exists():
30
+ return
31
+ if not skill_dst.exists():
32
+ print(" warning: skill dir exists but SKILL.md is missing. Run 'graphify install' to repair.")
33
+ return
34
+ installed = version_file.read_text(encoding="utf-8").strip()
35
+ if installed != __version__:
36
+ print(f" warning: skill is from graphify {installed}, package is {__version__}. Run 'graphify install' to update.", file=sys.stderr)
37
+
38
+
39
+ def _refresh_all_version_stamps() -> None:
40
+ """After a successful install, update .graphify_version in all other known skill dirs.
41
+
42
+ Prevents stale-version warnings from platforms that were installed previously
43
+ but not explicitly re-installed during this upgrade.
44
+ """
45
+ for cfg in _PLATFORM_CONFIG.values():
46
+ skill_dst = Path.home() / cfg["skill_dst"]
47
+ vf = skill_dst.parent / ".graphify_version"
48
+ if skill_dst.exists():
49
+ vf.write_text(__version__, encoding="utf-8")
50
+
51
+ _SETTINGS_HOOK = {
52
+ # Claude Code v2.1.117+ removed dedicated Grep/Glob tools; searches now go through Bash.
53
+ # We match on Bash and inspect the command string to avoid firing on every shell call.
54
+ "matcher": "Bash",
55
+ "hooks": [
56
+ {
57
+ "type": "command",
58
+ "command": (
59
+ "CMD=$(python3 -c \""
60
+ "import json,sys; d=json.load(sys.stdin); "
61
+ "print(d.get('tool_input',d).get('command',''))\" 2>/dev/null || true); "
62
+ "case \"$CMD\" in "
63
+ r"*grep*|*rg\ *|*ripgrep*|*find\ *|*fd\ *|*ack\ *|*ag\ *) "
64
+ " [ -f graphify-out/graph.json ] && "
65
+ r""" echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","additionalContext":"graphify: knowledge graph at graphify-out/. For focused questions, run `graphify query \"<question>\"` (scoped subgraph, usually much smaller than GRAPH_REPORT.md) instead of grepping raw files. Read GRAPH_REPORT.md only for broad architecture context."}}' """
66
+ " || true ;; "
67
+ "esac"
68
+ ),
69
+ }
70
+ ],
71
+ }
72
+
73
+ _SKILL_REGISTRATION = (
74
+ "\n# graphify\n"
75
+ "- **graphify** (`~/.claude/skills/graphify/SKILL.md`) "
76
+ "- any input to knowledge graph. Trigger: `/graphify`\n"
77
+ "When the user types `/graphify`, invoke the Skill tool "
78
+ "with `skill: \"graphify\"` before doing anything else.\n"
79
+ )
80
+
81
+
82
+ _PLATFORM_CONFIG: dict[str, dict] = {
83
+ "claude": {
84
+ "skill_file": "skill.md",
85
+ "skill_dst": Path(".claude") / "skills" / "graphify" / "SKILL.md",
86
+ "claude_md": True,
87
+ },
88
+ "codex": {
89
+ "skill_file": "skill-codex.md",
90
+ "skill_dst": Path(".agents") / "skills" / "graphify" / "SKILL.md",
91
+ "claude_md": False,
92
+ },
93
+ "opencode": {
94
+ "skill_file": "skill-opencode.md",
95
+ "skill_dst": Path(".config") / "opencode" / "skills" / "graphify" / "SKILL.md",
96
+ "claude_md": False,
97
+ },
98
+ "aider": {
99
+ "skill_file": "skill-aider.md",
100
+ "skill_dst": Path(".aider") / "graphify" / "SKILL.md",
101
+ "claude_md": False,
102
+ },
103
+ "copilot": {
104
+ "skill_file": "skill-copilot.md",
105
+ "skill_dst": Path(".copilot") / "skills" / "graphify" / "SKILL.md",
106
+ "claude_md": False,
107
+ },
108
+ "claw": {
109
+ "skill_file": "skill-claw.md",
110
+ "skill_dst": Path(".openclaw") / "skills" / "graphify" / "SKILL.md",
111
+ "claude_md": False,
112
+ },
113
+ "droid": {
114
+ "skill_file": "skill-droid.md",
115
+ "skill_dst": Path(".factory") / "skills" / "graphify" / "SKILL.md",
116
+ "claude_md": False,
117
+ },
118
+ "trae": {
119
+ "skill_file": "skill-trae.md",
120
+ "skill_dst": Path(".trae") / "skills" / "graphify" / "SKILL.md",
121
+ "claude_md": False,
122
+ },
123
+ "trae-cn": {
124
+ "skill_file": "skill-trae.md",
125
+ "skill_dst": Path(".trae-cn") / "skills" / "graphify" / "SKILL.md",
126
+ "claude_md": False,
127
+ },
128
+ "hermes": {
129
+ "skill_file": "skill-claw.md",
130
+ "skill_dst": Path(".hermes") / "skills" / "graphify" / "SKILL.md",
131
+ "claude_md": False,
132
+ },
133
+ "kiro": {
134
+ "skill_file": "skill-kiro.md",
135
+ "skill_dst": Path(".kiro") / "skills" / "graphify" / "SKILL.md",
136
+ "claude_md": False,
137
+ },
138
+ "pi": {
139
+ "skill_file": "skill-pi.md",
140
+ "skill_dst": Path(".pi") / "agent" / "skills" / "graphify" / "SKILL.md",
141
+ "claude_md": False,
142
+ },
143
+ "antigravity": {
144
+ "skill_file": "skill.md",
145
+ "skill_dst": Path(".agents") / "skills" / "graphify" / "SKILL.md",
146
+ "claude_md": False,
147
+ },
148
+ "antigravity-windows": {
149
+ "skill_file": "skill-windows.md",
150
+ "skill_dst": Path(".agents") / "skills" / "graphify" / "SKILL.md",
151
+ "claude_md": False,
152
+ },
153
+ "windows": {
154
+ "skill_file": "skill-windows.md",
155
+ "skill_dst": Path(".claude") / "skills" / "graphify" / "SKILL.md",
156
+ "claude_md": True,
157
+ },
158
+ "kimi": {
159
+ "skill_file": "skill.md",
160
+ "skill_dst": Path(".kimi") / "skills" / "graphify" / "SKILL.md",
161
+ "claude_md": False,
162
+ },
163
+ }
164
+
165
+
166
+ def _replace_or_append_section(content: str, marker: str, new_section: str) -> str:
167
+ """Idempotently update or append a graphify-owned section in shared files.
168
+
169
+ If ``marker`` is not in ``content``, append ``new_section`` to the end
170
+ (with a blank-line separator if there's existing content).
171
+
172
+ If ``marker`` IS in ``content``, replace the existing section in place.
173
+ The section runs from the first line containing ``marker`` to the line
174
+ before the next H2 heading (``## `` at line start), or to EOF if no later
175
+ H2 exists. This lets older installs receive the updated copy without
176
+ users having to uninstall and reinstall — important for the issue #580
177
+ fix where existing report-first text would otherwise silently linger.
178
+ """
179
+ if marker not in content:
180
+ if content.strip():
181
+ return content.rstrip() + "\n\n" + new_section.lstrip()
182
+ return new_section.lstrip()
183
+
184
+ lines = content.split("\n")
185
+ start = next((i for i, line in enumerate(lines) if marker in line), None)
186
+ if start is None:
187
+ return content.rstrip() + "\n\n" + new_section.lstrip()
188
+
189
+ end = len(lines)
190
+ for j in range(start + 1, len(lines)):
191
+ if lines[j].startswith("## "):
192
+ end = j
193
+ break
194
+
195
+ head = "\n".join(lines[:start]).rstrip()
196
+ tail = "\n".join(lines[end:]).lstrip()
197
+ section = new_section.strip()
198
+
199
+ parts: list[str] = []
200
+ if head:
201
+ parts.append(head)
202
+ parts.append(section)
203
+ if tail:
204
+ parts.append(tail)
205
+ out = "\n\n".join(parts)
206
+ if not out.endswith("\n"):
207
+ out += "\n"
208
+ return out
209
+
210
+
211
+ def install(platform: str = "claude") -> None:
212
+ if platform == "gemini":
213
+ gemini_install()
214
+ return
215
+ if platform == "cursor":
216
+ _cursor_install(Path("."))
217
+ return
218
+ # On Windows, antigravity needs the PowerShell skill, not the bash one
219
+ if platform == "antigravity" and sys.platform == "win32":
220
+ platform = "antigravity-windows"
221
+ if platform not in _PLATFORM_CONFIG:
222
+ print(
223
+ f"error: unknown platform '{platform}'. Choose from: {', '.join(_PLATFORM_CONFIG)}, gemini, cursor",
224
+ file=sys.stderr,
225
+ )
226
+ sys.exit(1)
227
+
228
+ cfg = _PLATFORM_CONFIG[platform]
229
+ skill_src = Path(__file__).parent / cfg["skill_file"]
230
+ if not skill_src.exists():
231
+ print(f"error: {cfg['skill_file']} not found in package - reinstall graphify", file=sys.stderr)
232
+ sys.exit(1)
233
+
234
+ import os as _os
235
+ if platform in ("claude", "windows") and _os.environ.get("CLAUDE_CONFIG_DIR"):
236
+ _claude_base = Path(_os.environ["CLAUDE_CONFIG_DIR"])
237
+ skill_dst = _claude_base / "skills" / "graphify" / "SKILL.md"
238
+ else:
239
+ skill_dst = Path.home() / cfg["skill_dst"]
240
+ skill_dst.parent.mkdir(parents=True, exist_ok=True)
241
+ tmp_dst = skill_dst.with_suffix(skill_dst.suffix + ".tmp")
242
+ try:
243
+ shutil.copy(skill_src, tmp_dst)
244
+ os.replace(tmp_dst, skill_dst)
245
+ except Exception:
246
+ try:
247
+ tmp_dst.unlink(missing_ok=True)
248
+ except OSError:
249
+ pass
250
+ raise
251
+ (skill_dst.parent / ".graphify_version").write_text(__version__, encoding="utf-8")
252
+ print(f" skill installed -> {skill_dst}")
253
+
254
+ if cfg["claude_md"]:
255
+ # Register in ~/.claude/CLAUDE.md (Claude Code only)
256
+ claude_md = Path.home() / ".claude" / "CLAUDE.md"
257
+ if claude_md.exists():
258
+ content = claude_md.read_text(encoding="utf-8")
259
+ if "graphify" in content:
260
+ print(f" CLAUDE.md -> already registered (no change)")
261
+ else:
262
+ claude_md.write_text(content.rstrip() + _SKILL_REGISTRATION, encoding="utf-8")
263
+ print(f" CLAUDE.md -> skill registered in {claude_md}")
264
+ else:
265
+ claude_md.parent.mkdir(parents=True, exist_ok=True)
266
+ claude_md.write_text(_SKILL_REGISTRATION.lstrip(), encoding="utf-8")
267
+ print(f" CLAUDE.md -> created at {claude_md}")
268
+
269
+ if platform == "opencode":
270
+ _install_opencode_plugin(Path("."))
271
+
272
+ # Refresh version stamps in all other previously-installed skill dirs so
273
+ # stale-version warnings don't fire for platforms not explicitly re-installed.
274
+ _refresh_all_version_stamps()
275
+
276
+ print()
277
+ print("Done. Open your AI coding assistant and type:")
278
+ print()
279
+ print(" /graphify .")
280
+ print()
281
+
282
+
283
+ def _print_install_usage() -> None:
284
+ platforms = ", ".join([*_PLATFORM_CONFIG, "gemini", "cursor"])
285
+ print("Usage: graphify install [--platform P|P]")
286
+ print(f"Platforms: {platforms}")
287
+
288
+
289
+ _CLAUDE_MD_SECTION = """\
290
+ ## graphify
291
+
292
+ This project has a knowledge graph at graphify-out/ with god nodes, community structure, and cross-file relationships.
293
+
294
+ Rules:
295
+ - For codebase questions, first run `graphify query "<question>"` when graphify-out/graph.json exists. Use `graphify path "<A>" "<B>"` for relationships and `graphify explain "<concept>"` for focused concepts. These return a scoped subgraph, usually much smaller than GRAPH_REPORT.md or raw grep output.
296
+ - If graphify-out/wiki/index.md exists, use it for broad navigation instead of raw source browsing.
297
+ - Read graphify-out/GRAPH_REPORT.md only for broad architecture review or when query/path/explain do not surface enough context.
298
+ - After modifying code, run `graphify update .` to keep the graph current (AST-only, no API cost).
299
+ """
300
+
301
+ _CLAUDE_MD_MARKER = "## graphify"
302
+
303
+ # AGENTS.md section for Codex, OpenCode, and OpenClaw.
304
+ # All three platforms read AGENTS.md in the project root for persistent instructions.
305
+ _AGENTS_MD_SECTION = """\
306
+ ## graphify
307
+
308
+ This project has a knowledge graph at graphify-out/ with god nodes, community structure, and cross-file relationships.
309
+
310
+ When the user types `/graphify`, invoke the `skill` tool with `skill: "graphify"` before doing anything else.
311
+
312
+ Rules:
313
+ - For codebase questions, first run `graphify query "<question>"` when graphify-out/graph.json exists. Use `graphify path "<A>" "<B>"` for relationships and `graphify explain "<concept>"` for focused concepts. These return a scoped subgraph, usually much smaller than GRAPH_REPORT.md or raw grep output.
314
+ - Dirty graphify-out/ files are expected after hooks or incremental updates; dirty graph files are not a reason to skip graphify. Only skip graphify if the task is about stale or incorrect graph output, or the user explicitly says not to use it.
315
+ - If graphify-out/wiki/index.md exists, use it for broad navigation instead of raw source browsing.
316
+ - Read graphify-out/GRAPH_REPORT.md only for broad architecture review or when query/path/explain do not surface enough context.
317
+ - After modifying code, run `graphify update .` to keep the graph current (AST-only, no API cost).
318
+ """
319
+
320
+ _AGENTS_MD_MARKER = "## graphify"
321
+
322
+ _GEMINI_MD_SECTION = """\
323
+ ## graphify
324
+
325
+ This project has a knowledge graph at graphify-out/ with god nodes, community structure, and cross-file relationships.
326
+
327
+ Rules:
328
+ - For codebase questions, first run `graphify query "<question>"` when graphify-out/graph.json exists. Use `graphify path "<A>" "<B>"` for relationships and `graphify explain "<concept>"` for focused concepts. These return a scoped subgraph, usually much smaller than GRAPH_REPORT.md or raw grep output.
329
+ - If graphify-out/wiki/index.md exists, use it for broad navigation instead of raw source browsing.
330
+ - Read graphify-out/GRAPH_REPORT.md only for broad architecture review or when query/path/explain do not surface enough context.
331
+ - After modifying code, run `graphify update .` to keep the graph current (AST-only, no API cost).
332
+ """
333
+
334
+ _GEMINI_MD_MARKER = "## graphify"
335
+
336
+ _GEMINI_HOOK = {
337
+ "matcher": "read_file|list_directory",
338
+ "hooks": [
339
+ {
340
+ "type": "command",
341
+ "command": (
342
+ 'python -c "'
343
+ "import sys,pathlib,json;"
344
+ "e=pathlib.Path('graphify-out/graph.json').exists();"
345
+ "d={'decision':'allow'};"
346
+ "e and d.update({'additionalContext':'graphify: knowledge graph at graphify-out/. For focused questions, run `graphify query \"<question>\"` (scoped subgraph, usually much smaller than GRAPH_REPORT.md) instead of grepping raw files. Read GRAPH_REPORT.md only for broad architecture context.'});"
347
+ "sys.stdout.write(json.dumps(d))"
348
+ '"'
349
+ ),
350
+ }
351
+ ],
352
+ }
353
+
354
+
355
+ def gemini_install(project_dir: Path | None = None) -> None:
356
+ """Copy skill file to ~/.gemini/skills/graphify/, write GEMINI.md section, and install BeforeTool hook."""
357
+ # Copy skill file to ~/.gemini/skills/graphify/SKILL.md
358
+ # On Windows, Gemini CLI prioritises ~/.agents/skills/ over ~/.gemini/skills/
359
+ skill_src = Path(__file__).parent / "skill.md"
360
+ if platform.system() == "Windows":
361
+ skill_dst = Path.home() / ".agents" / "skills" / "graphify" / "SKILL.md"
362
+ else:
363
+ skill_dst = Path.home() / ".gemini" / "skills" / "graphify" / "SKILL.md"
364
+ skill_dst.parent.mkdir(parents=True, exist_ok=True)
365
+ shutil.copy(skill_src, skill_dst)
366
+ (skill_dst.parent / ".graphify_version").write_text(__version__, encoding="utf-8")
367
+ print(f" skill installed -> {skill_dst}")
368
+
369
+ target = (project_dir or Path(".")) / "GEMINI.md"
370
+
371
+ if target.exists():
372
+ content = target.read_text(encoding="utf-8")
373
+ new_content = _replace_or_append_section(
374
+ content, _GEMINI_MD_MARKER, _GEMINI_MD_SECTION
375
+ )
376
+ else:
377
+ new_content = _GEMINI_MD_SECTION
378
+
379
+ if target.exists() and new_content == target.read_text(encoding="utf-8"):
380
+ print(f"graphify already configured in {target.resolve()} (no change)")
381
+ else:
382
+ target.write_text(new_content, encoding="utf-8")
383
+ print(f"graphify section written to {target.resolve()}")
384
+
385
+ # Always re-install the Gemini hook so an older payload (e.g. pre-issue-#580
386
+ # wording) is replaced on upgrade.
387
+ _install_gemini_hook(project_dir or Path("."))
388
+ print()
389
+ print("Gemini CLI will now check the knowledge graph before answering")
390
+ print("codebase questions and rebuild it after code changes.")
391
+
392
+
393
+ def _install_gemini_hook(project_dir: Path) -> None:
394
+ settings_path = project_dir / ".gemini" / "settings.json"
395
+ settings_path.parent.mkdir(parents=True, exist_ok=True)
396
+ try:
397
+ settings = json.loads(settings_path.read_text(encoding="utf-8")) if settings_path.exists() else {}
398
+ except json.JSONDecodeError:
399
+ settings = {}
400
+ before_tool = settings.setdefault("hooks", {}).setdefault("BeforeTool", [])
401
+ settings["hooks"]["BeforeTool"] = [h for h in before_tool if "graphify" not in str(h)]
402
+ settings["hooks"]["BeforeTool"].append(_GEMINI_HOOK)
403
+ settings_path.write_text(json.dumps(settings, indent=2), encoding="utf-8")
404
+ print(" .gemini/settings.json -> BeforeTool hook registered")
405
+
406
+
407
+ def _uninstall_gemini_hook(project_dir: Path) -> None:
408
+ settings_path = project_dir / ".gemini" / "settings.json"
409
+ if not settings_path.exists():
410
+ return
411
+ try:
412
+ settings = json.loads(settings_path.read_text(encoding="utf-8"))
413
+ except json.JSONDecodeError:
414
+ return
415
+ before_tool = settings.get("hooks", {}).get("BeforeTool", [])
416
+ filtered = [h for h in before_tool if "graphify" not in str(h)]
417
+ if len(filtered) == len(before_tool):
418
+ return
419
+ settings["hooks"]["BeforeTool"] = filtered
420
+ settings_path.write_text(json.dumps(settings, indent=2), encoding="utf-8")
421
+ print(" .gemini/settings.json -> BeforeTool hook removed")
422
+
423
+
424
+ def gemini_uninstall(project_dir: Path | None = None) -> None:
425
+ """Remove the graphify section from GEMINI.md, uninstall hook, and remove skill file."""
426
+ # Remove skill file (mirror the install path detection)
427
+ if platform.system() == "Windows":
428
+ skill_dst = Path.home() / ".agents" / "skills" / "graphify" / "SKILL.md"
429
+ else:
430
+ skill_dst = Path.home() / ".gemini" / "skills" / "graphify" / "SKILL.md"
431
+ if skill_dst.exists():
432
+ skill_dst.unlink()
433
+ print(f" skill removed -> {skill_dst}")
434
+ version_file = skill_dst.parent / ".graphify_version"
435
+ if version_file.exists():
436
+ version_file.unlink()
437
+ for d in (skill_dst.parent, skill_dst.parent.parent):
438
+ try:
439
+ d.rmdir()
440
+ except OSError:
441
+ break
442
+
443
+ target = (project_dir or Path(".")) / "GEMINI.md"
444
+ if not target.exists():
445
+ print("No GEMINI.md found in current directory - nothing to do")
446
+ return
447
+ content = target.read_text(encoding="utf-8")
448
+ if _GEMINI_MD_MARKER not in content:
449
+ print("graphify section not found in GEMINI.md - nothing to do")
450
+ return
451
+ cleaned = re.sub(r"\n*## graphify\n.*?(?=\n## |\Z)", "", content, flags=re.DOTALL).rstrip()
452
+ if cleaned:
453
+ target.write_text(cleaned + "\n", encoding="utf-8")
454
+ print(f"graphify section removed from {target.resolve()}")
455
+ else:
456
+ target.unlink()
457
+ print(f"GEMINI.md was empty after removal - deleted {target.resolve()}")
458
+ _uninstall_gemini_hook(project_dir or Path("."))
459
+
460
+
461
+ _VSCODE_INSTRUCTIONS_MARKER = "## graphify"
462
+ _VSCODE_INSTRUCTIONS_SECTION = """\
463
+ ## graphify
464
+
465
+ For any question about this repo's architecture, structure, components, or how to add/modify/find
466
+ code, your first action should be `graphify query "<question>"` when `graphify-out/graph.json`
467
+ exists. Use `graphify path "<A>" "<B>"` for relationship questions and `graphify explain "<concept>"`
468
+ for focused-concept questions. These return a scoped subgraph, usually much smaller than the full
469
+ report or raw grep output.
470
+
471
+ Triggers: "how do I…", "where is…", "what does … do", "add/modify a <component>",
472
+ "explain the architecture", or anything that depends on how files or classes relate.
473
+
474
+ If `graphify-out/wiki/index.md` exists, use it for broad navigation. Read `graphify-out/GRAPH_REPORT.md`
475
+ only for broad architecture review or when query/path/explain do not surface enough context. Only read
476
+ source files when (a) modifying/debugging specific code, (b) the graph lacks the needed detail, or
477
+ (c) the graph is missing or stale.
478
+
479
+ Type `/graphify` in Copilot Chat to build or update the graph.
480
+ """
481
+
482
+
483
+ def vscode_install(project_dir: Path | None = None) -> None:
484
+ """Install graphify skill for VS Code Copilot Chat + write .github/copilot-instructions.md."""
485
+ skill_src = Path(__file__).parent / "skill-vscode.md"
486
+ if not skill_src.exists():
487
+ skill_src = Path(__file__).parent / "skill-copilot.md"
488
+ skill_dst = Path.home() / ".copilot" / "skills" / "graphify" / "SKILL.md"
489
+ skill_dst.parent.mkdir(parents=True, exist_ok=True)
490
+ shutil.copy(skill_src, skill_dst)
491
+ (skill_dst.parent / ".graphify_version").write_text(__version__, encoding="utf-8")
492
+ print(f" skill installed -> {skill_dst}")
493
+
494
+ instructions = (project_dir or Path(".")) / ".github" / "copilot-instructions.md"
495
+ instructions.parent.mkdir(parents=True, exist_ok=True)
496
+ if instructions.exists():
497
+ content = instructions.read_text(encoding="utf-8")
498
+ new_content = _replace_or_append_section(
499
+ content, _VSCODE_INSTRUCTIONS_MARKER, _VSCODE_INSTRUCTIONS_SECTION
500
+ )
501
+ if new_content == content:
502
+ print(f" {instructions} -> already configured (no change)")
503
+ else:
504
+ instructions.write_text(new_content, encoding="utf-8")
505
+ print(f" {instructions} -> graphify section {'updated' if _VSCODE_INSTRUCTIONS_MARKER in content else 'added'}")
506
+ else:
507
+ instructions.write_text(_VSCODE_INSTRUCTIONS_SECTION, encoding="utf-8")
508
+ print(f" {instructions} -> created")
509
+
510
+ print()
511
+ print("VS Code Copilot Chat configured. Type /graphify in the chat panel to build the graph.")
512
+ print("Note: for GitHub Copilot CLI (terminal), use: graphify copilot install")
513
+
514
+
515
+ def vscode_uninstall(project_dir: Path | None = None) -> None:
516
+ """Remove graphify VS Code Copilot Chat skill and .github/copilot-instructions.md section."""
517
+ skill_dst = Path.home() / ".copilot" / "skills" / "graphify" / "SKILL.md"
518
+ if skill_dst.exists():
519
+ skill_dst.unlink()
520
+ print(f" skill removed -> {skill_dst}")
521
+ version_file = skill_dst.parent / ".graphify_version"
522
+ if version_file.exists():
523
+ version_file.unlink()
524
+ for d in (skill_dst.parent, skill_dst.parent.parent, skill_dst.parent.parent.parent):
525
+ try:
526
+ d.rmdir()
527
+ except OSError:
528
+ break
529
+
530
+ instructions = (project_dir or Path(".")) / ".github" / "copilot-instructions.md"
531
+ if not instructions.exists():
532
+ return
533
+ content = instructions.read_text(encoding="utf-8")
534
+ if _VSCODE_INSTRUCTIONS_MARKER not in content:
535
+ return
536
+ cleaned = re.sub(r"\n*## graphify\n.*?(?=\n## |\Z)", "", content, flags=re.DOTALL).rstrip()
537
+ if cleaned:
538
+ instructions.write_text(cleaned + "\n", encoding="utf-8")
539
+ print(f" graphify section removed from {instructions}")
540
+ else:
541
+ instructions.unlink()
542
+ print(f" {instructions} -> deleted (was empty after removal)")
543
+
544
+
545
+ _ANTIGRAVITY_RULES_PATH = Path(".agents") / "rules" / "graphify.md"
546
+ _ANTIGRAVITY_WORKFLOW_PATH = Path(".agents") / "workflows" / "graphify.md"
547
+
548
+ _ANTIGRAVITY_RULES = """\
549
+ ---
550
+ trigger: always_on
551
+ description: Consult the graphify knowledge graph at graphify-out/ for codebase and architecture questions.
552
+ ---
553
+
554
+ ## graphify
555
+
556
+ This project has a graphify knowledge graph at graphify-out/.
557
+
558
+ Rules:
559
+ - For codebase or architecture questions, when `graphify-out/graph.json` exists, first run `graphify query "<question>"` (CLI) or `query_graph` (MCP). Use `graphify path "<A>" "<B>"` / `shortest_path` for relationships and `graphify explain "<concept>"` / `get_node` for focused concepts. These return a scoped subgraph, usually much smaller than `GRAPH_REPORT.md` or raw grep output.
560
+ - If graphify-out/wiki/index.md exists, navigate it instead of reading raw files
561
+ - Read graphify-out/GRAPH_REPORT.md only for broad architecture review or when query/path/explain do not surface enough context
562
+ - After modifying code files in this session, run `graphify update .` to keep the graph current (AST-only, no API cost)
563
+ """
564
+
565
+ _ANTIGRAVITY_WORKFLOW = """\
566
+ ---
567
+ name: graphify
568
+ description: Turn any folder of files into a navigable knowledge graph
569
+ ---
570
+
571
+ # Workflow: graphify
572
+
573
+ Follow the graphify skill installed at ~/.agents/skills/graphify/SKILL.md to run the full pipeline.
574
+
575
+ If no path argument is given, use `.` (current directory).
576
+ """
577
+
578
+
579
+ _KIRO_STEERING = """\
580
+ ---
581
+ inclusion: always
582
+ ---
583
+
584
+ graphify: A knowledge graph of this project lives in `graphify-out/`. \
585
+ For codebase, architecture, or dependency questions, when `graphify-out/graph.json` exists, \
586
+ first run `graphify query "<question>"` (or `graphify path "<A>" "<B>"` / `graphify explain "<concept>"`). \
587
+ These return a scoped subgraph, usually much smaller than `GRAPH_REPORT.md` or raw grep output. \
588
+ Read `GRAPH_REPORT.md` only for broad architecture review or when those commands do not surface enough context.
589
+ """
590
+
591
+ _KIRO_STEERING_MARKER = "graphify: A knowledge graph of this project"
592
+
593
+
594
+ def _kiro_install(project_dir: Path) -> None:
595
+ """Write graphify skill + steering file for Kiro IDE/CLI."""
596
+ project_dir = project_dir or Path(".")
597
+
598
+ # Skill file → .kiro/skills/graphify/SKILL.md
599
+ skill_src = Path(__file__).parent / "skill-kiro.md"
600
+ skill_dst = project_dir / ".kiro" / "skills" / "graphify" / "SKILL.md"
601
+ skill_dst.parent.mkdir(parents=True, exist_ok=True)
602
+ skill_dst.write_text(skill_src.read_text(encoding="utf-8"), encoding="utf-8")
603
+ print(f" {skill_dst.relative_to(project_dir)} -> /graphify skill")
604
+
605
+ # Steering file → .kiro/steering/graphify.md (always-on)
606
+ steering_dir = project_dir / ".kiro" / "steering"
607
+ steering_dir.mkdir(parents=True, exist_ok=True)
608
+ steering_dst = steering_dir / "graphify.md"
609
+ if steering_dst.exists() and steering_dst.read_text(encoding="utf-8") == _KIRO_STEERING:
610
+ print(f" .kiro/steering/graphify.md -> already configured (no change)")
611
+ else:
612
+ # File is wholly graphify-owned. Overwrite on upgrade so older
613
+ # report-first wording does not silently linger (issue #580).
614
+ action = "updated" if steering_dst.exists() else "written"
615
+ steering_dst.write_text(_KIRO_STEERING, encoding="utf-8")
616
+ print(f" .kiro/steering/graphify.md -> always-on steering {action}")
617
+
618
+ print()
619
+ print("Kiro will now read the knowledge graph before every conversation.")
620
+ print("Use /graphify to build or update the graph.")
621
+
622
+
623
+ def _kiro_uninstall(project_dir: Path) -> None:
624
+ """Remove graphify skill + steering file for Kiro."""
625
+ project_dir = project_dir or Path(".")
626
+ removed = []
627
+
628
+ skill_dst = project_dir / ".kiro" / "skills" / "graphify" / "SKILL.md"
629
+ if skill_dst.exists():
630
+ skill_dst.unlink()
631
+ removed.append(str(skill_dst.relative_to(project_dir)))
632
+ # Remove parent dir if empty
633
+ try:
634
+ skill_dst.parent.rmdir()
635
+ except OSError:
636
+ pass
637
+
638
+ steering_dst = project_dir / ".kiro" / "steering" / "graphify.md"
639
+ if steering_dst.exists():
640
+ steering_dst.unlink()
641
+ removed.append(str(steering_dst.relative_to(project_dir)))
642
+
643
+ print("Removed: " + (", ".join(removed) if removed else "nothing to remove"))
644
+
645
+
646
+ def _antigravity_install(project_dir: Path) -> None:
647
+ """Install graphify for Google Antigravity: skill + .agents/rules + .agents/workflows."""
648
+ # 1. Copy skill file to ~/.agents/skills/graphify/SKILL.md
649
+ install(platform="antigravity")
650
+
651
+ # 1.5. Inject YAML frontmatter for native Antigravity tool discovery
652
+ skill_dst = _PLATFORM_CONFIG["antigravity"]["skill_dst"]
653
+ if skill_dst.exists():
654
+ content = skill_dst.read_text(encoding="utf-8")
655
+ if not content.startswith("---\n"):
656
+ frontmatter = "---\nname: graphify-manager\ndescription: Rebuild the code graph or perform manual CLI queries when MCP server is offline.\n---\n\n"
657
+ skill_dst.write_text(frontmatter + content, encoding="utf-8")
658
+
659
+ # 2. Write .agents/rules/graphify.md
660
+ rules_path = project_dir / _ANTIGRAVITY_RULES_PATH
661
+ rules_path.parent.mkdir(parents=True, exist_ok=True)
662
+ if rules_path.exists():
663
+ existing = rules_path.read_text(encoding="utf-8")
664
+ if _ANTIGRAVITY_RULES.strip() != existing.strip():
665
+ rules_path.write_text(_ANTIGRAVITY_RULES, encoding="utf-8")
666
+ print(f"graphify rule updated at {rules_path.resolve()}")
667
+ else:
668
+ print(f"graphify rule already configured at {rules_path.resolve()} (no change)")
669
+ else:
670
+ rules_path.write_text(_ANTIGRAVITY_RULES, encoding="utf-8")
671
+ print(f"graphify rule written to {rules_path.resolve()}")
672
+
673
+ # 3. Write .agents/workflows/graphify.md
674
+ wf_path = project_dir / _ANTIGRAVITY_WORKFLOW_PATH
675
+ wf_path.parent.mkdir(parents=True, exist_ok=True)
676
+ if wf_path.exists():
677
+ existing = wf_path.read_text(encoding="utf-8")
678
+ if _ANTIGRAVITY_WORKFLOW.strip() != existing.strip():
679
+ wf_path.write_text(_ANTIGRAVITY_WORKFLOW, encoding="utf-8")
680
+ print(f"graphify workflow updated at {wf_path.resolve()}")
681
+ else:
682
+ print(f"graphify workflow already configured at {wf_path.resolve()} (no change)")
683
+ else:
684
+ wf_path.write_text(_ANTIGRAVITY_WORKFLOW, encoding="utf-8")
685
+ print(f"graphify workflow written to {wf_path.resolve()}")
686
+
687
+ print()
688
+ print("Antigravity will now check the knowledge graph before answering")
689
+ print("codebase questions. Run /graphify first to build the graph.")
690
+ print()
691
+ print("To enable full MCP architecture navigation, add this to ~/.gemini/antigravity/mcp_config.json:")
692
+ print(' "graphify": {')
693
+ print(' "command": "uv",')
694
+ print(' "args": ["run", "--with", "graphifyy", "--with", "mcp", "-m", "graphify.serve", "${workspace.path}/graphify-out/graph.json"]')
695
+ print(' }')
696
+
697
+
698
+ def _antigravity_uninstall(project_dir: Path) -> None:
699
+ """Remove graphify Antigravity rules, workflow, and skill files."""
700
+ # Remove rules file
701
+ rules_path = project_dir / _ANTIGRAVITY_RULES_PATH
702
+ if rules_path.exists():
703
+ rules_path.unlink()
704
+ print(f"graphify rule removed from {rules_path.resolve()}")
705
+ else:
706
+ print("No graphify Antigravity rule found - nothing to do")
707
+
708
+ # Remove workflow file
709
+ wf_path = project_dir / _ANTIGRAVITY_WORKFLOW_PATH
710
+ if wf_path.exists():
711
+ wf_path.unlink()
712
+ print(f"graphify workflow removed from {wf_path.resolve()}")
713
+
714
+ # Remove skill file
715
+ skill_dst = _PLATFORM_CONFIG["antigravity"]["skill_dst"]
716
+ if skill_dst.exists():
717
+ skill_dst.unlink()
718
+ print(f"graphify skill removed from {skill_dst}")
719
+ version_file = skill_dst.parent / ".graphify_version"
720
+ if version_file.exists():
721
+ version_file.unlink()
722
+ for d in (skill_dst.parent, skill_dst.parent.parent, skill_dst.parent.parent.parent):
723
+ try:
724
+ d.rmdir()
725
+ except OSError:
726
+ break
727
+
728
+
729
+ _CURSOR_RULE_PATH = Path(".cursor") / "rules" / "graphify.mdc"
730
+ _CURSOR_RULE = """\
731
+ ---
732
+ description: graphify knowledge graph context
733
+ alwaysApply: true
734
+ ---
735
+
736
+ This project has a graphify knowledge graph at graphify-out/.
737
+
738
+ - For codebase or architecture questions, when `graphify-out/graph.json` exists, first run `graphify query "<question>"` (or `graphify path "<A>" "<B>"` / `graphify explain "<concept>"`). These return a scoped subgraph, usually much smaller than `GRAPH_REPORT.md` or raw grep output.
739
+ - If graphify-out/wiki/index.md exists, navigate it instead of reading raw files
740
+ - Read graphify-out/GRAPH_REPORT.md only for broad architecture review or when query/path/explain do not surface enough context
741
+ - After modifying code files in this session, run `graphify update .` to keep the graph current (AST-only, no API cost)
742
+ """
743
+
744
+
745
+ def _cursor_install(project_dir: Path) -> None:
746
+ """Write .cursor/rules/graphify.mdc with alwaysApply: true."""
747
+ rule_path = (project_dir or Path(".")) / _CURSOR_RULE_PATH
748
+ rule_path.parent.mkdir(parents=True, exist_ok=True)
749
+ if rule_path.exists() and rule_path.read_text(encoding="utf-8") == _CURSOR_RULE:
750
+ print(f"graphify rule at {rule_path} already configured (no change)")
751
+ return
752
+ # File is wholly graphify-owned. Overwrite on upgrade so older
753
+ # report-first wording does not silently linger (issue #580).
754
+ action = "updated" if rule_path.exists() else "written"
755
+ rule_path.write_text(_CURSOR_RULE, encoding="utf-8")
756
+ print(f"graphify rule {action} at {rule_path.resolve()}")
757
+ print()
758
+ print("Cursor will now always include the knowledge graph context.")
759
+ print("Run /graphify . first to build the graph if you haven't already.")
760
+
761
+
762
+ def _cursor_uninstall(project_dir: Path) -> None:
763
+ """Remove .cursor/rules/graphify.mdc."""
764
+ rule_path = (project_dir or Path(".")) / _CURSOR_RULE_PATH
765
+ if not rule_path.exists():
766
+ print("No graphify Cursor rule found - nothing to do")
767
+ return
768
+ rule_path.unlink()
769
+ print(f"graphify Cursor rule removed from {rule_path.resolve()}")
770
+
771
+
772
+ # OpenCode tool.execute.before plugin — fires before every tool call.
773
+ # Injects a graph reminder into bash command output when graph.json exists.
774
+ _OPENCODE_PLUGIN_JS = """\
775
+ // graphify OpenCode plugin
776
+ // Injects a knowledge graph reminder before bash tool calls when the graph exists.
777
+ import { existsSync } from "fs";
778
+ import { join } from "path";
779
+
780
+ export const GraphifyPlugin = async ({ directory }) => {
781
+ let reminded = false;
782
+
783
+ return {
784
+ "tool.execute.before": async (input, output) => {
785
+ if (reminded) return;
786
+ if (!existsSync(join(directory, "graphify-out", "graph.json"))) return;
787
+
788
+ if (input.tool === "bash") {
789
+ output.args.command =
790
+ 'echo "[graphify] knowledge graph at graphify-out/. For focused questions, run \\`graphify query \\"<question>\\"\\` (scoped subgraph, usually much smaller than GRAPH_REPORT.md) instead of grepping raw files. Read GRAPH_REPORT.md only for broad architecture context." && ' +
791
+ output.args.command;
792
+ reminded = true;
793
+ }
794
+ },
795
+ };
796
+ };
797
+ """
798
+
799
+ _OPENCODE_PLUGIN_PATH = Path(".opencode") / "plugins" / "graphify.js"
800
+ _OPENCODE_CONFIG_PATH = Path(".opencode") / "opencode.json"
801
+
802
+
803
+ def _install_opencode_plugin(project_dir: Path) -> None:
804
+ """Write graphify.js plugin and register it in opencode.json."""
805
+ plugin_file = project_dir / _OPENCODE_PLUGIN_PATH
806
+ plugin_file.parent.mkdir(parents=True, exist_ok=True)
807
+ plugin_file.write_text(_OPENCODE_PLUGIN_JS, encoding="utf-8")
808
+ print(f" {_OPENCODE_PLUGIN_PATH} -> tool.execute.before hook written")
809
+
810
+ config_file = project_dir / _OPENCODE_CONFIG_PATH
811
+ if config_file.exists():
812
+ try:
813
+ config = json.loads(config_file.read_text(encoding="utf-8"))
814
+ except json.JSONDecodeError:
815
+ config = {}
816
+ else:
817
+ config = {}
818
+
819
+ plugins = config.setdefault("plugin", [])
820
+ entry = _OPENCODE_PLUGIN_PATH.as_posix()
821
+ if entry not in plugins:
822
+ plugins.append(entry)
823
+ config_file.write_text(json.dumps(config, indent=2), encoding="utf-8")
824
+ print(f" {_OPENCODE_CONFIG_PATH} -> plugin registered")
825
+ else:
826
+ print(f" {_OPENCODE_CONFIG_PATH} -> plugin already registered (no change)")
827
+
828
+
829
+ def _uninstall_opencode_plugin(project_dir: Path) -> None:
830
+ """Remove graphify.js plugin and deregister from opencode.json."""
831
+ plugin_file = project_dir / _OPENCODE_PLUGIN_PATH
832
+ if plugin_file.exists():
833
+ plugin_file.unlink()
834
+ print(f" {_OPENCODE_PLUGIN_PATH} -> removed")
835
+
836
+ config_file = project_dir / _OPENCODE_CONFIG_PATH
837
+ if not config_file.exists():
838
+ return
839
+ try:
840
+ config = json.loads(config_file.read_text(encoding="utf-8"))
841
+ except json.JSONDecodeError:
842
+ return
843
+ plugins = config.get("plugin", [])
844
+ entry = _OPENCODE_PLUGIN_PATH.as_posix()
845
+ if entry in plugins:
846
+ plugins.remove(entry)
847
+ if not plugins:
848
+ config.pop("plugin")
849
+ config_file.write_text(json.dumps(config, indent=2), encoding="utf-8")
850
+ print(f" {_OPENCODE_CONFIG_PATH} -> plugin deregistered")
851
+
852
+
853
+ _CODEX_HOOK = {
854
+ "hooks": {
855
+ "PreToolUse": [
856
+ {
857
+ "matcher": "Bash",
858
+ "hooks": [
859
+ {
860
+ "type": "command",
861
+ # Use the graphify CLI itself so the hook is shell-agnostic:
862
+ # no [ -f ] bash syntax, no python3 vs python Conda issue,
863
+ # no JSON escaping inside PowerShell strings. Works on
864
+ # Windows (PowerShell/cmd.exe), macOS, and Linux.
865
+ "command": "graphify hook-check",
866
+ }
867
+ ],
868
+ }
869
+ ]
870
+ }
871
+ }
872
+
873
+
874
+ def _resolve_graphify_exe() -> str:
875
+ """Return the absolute path to the graphify executable.
876
+
877
+ Falls back to bare 'graphify' if resolution fails. Using an absolute path
878
+ ensures the hook works in environments where the venv Scripts/ directory is
879
+ not on PATH (e.g. VS Code Codex extension on Windows).
880
+ """
881
+ import shutil
882
+ found = shutil.which("graphify")
883
+ if found:
884
+ return found
885
+ # Derive from sys.executable: same Scripts/ (Windows) or bin/ (Unix) dir
886
+ scripts_dir = Path(sys.executable).parent
887
+ for name in ("graphify.exe", "graphify"):
888
+ candidate = scripts_dir / name
889
+ if candidate.exists():
890
+ return str(candidate)
891
+ return "graphify"
892
+
893
+
894
+ def _install_codex_hook(project_dir: Path) -> None:
895
+ """Add graphify PreToolUse hook to .codex/hooks.json."""
896
+ hooks_path = project_dir / ".codex" / "hooks.json"
897
+ hooks_path.parent.mkdir(parents=True, exist_ok=True)
898
+
899
+ if hooks_path.exists():
900
+ try:
901
+ existing = json.loads(hooks_path.read_text(encoding="utf-8"))
902
+ except json.JSONDecodeError:
903
+ existing = {}
904
+ else:
905
+ existing = {}
906
+
907
+ graphify_exe = _resolve_graphify_exe()
908
+ hook_entry = {
909
+ "hooks": {
910
+ "PreToolUse": [
911
+ {
912
+ "matcher": "Bash",
913
+ "hooks": [{"type": "command", "command": f"{graphify_exe} hook-check"}],
914
+ }
915
+ ]
916
+ }
917
+ }
918
+
919
+ pre_tool = existing.setdefault("hooks", {}).setdefault("PreToolUse", [])
920
+ existing["hooks"]["PreToolUse"] = [h for h in pre_tool if "graphify" not in str(h)]
921
+ existing["hooks"]["PreToolUse"].extend(hook_entry["hooks"]["PreToolUse"])
922
+ hooks_path.write_text(json.dumps(existing, indent=2), encoding="utf-8")
923
+ print(f" .codex/hooks.json -> PreToolUse hook registered ({graphify_exe} hook-check)")
924
+
925
+
926
+ def _uninstall_codex_hook(project_dir: Path) -> None:
927
+ """Remove graphify PreToolUse hook from .codex/hooks.json."""
928
+ hooks_path = project_dir / ".codex" / "hooks.json"
929
+ if not hooks_path.exists():
930
+ return
931
+ try:
932
+ existing = json.loads(hooks_path.read_text(encoding="utf-8"))
933
+ except json.JSONDecodeError:
934
+ return
935
+ pre_tool = existing.get("hooks", {}).get("PreToolUse", [])
936
+ filtered = [h for h in pre_tool if "graphify" not in str(h)]
937
+ existing["hooks"]["PreToolUse"] = filtered
938
+ hooks_path.write_text(json.dumps(existing, indent=2), encoding="utf-8")
939
+ print(f" .codex/hooks.json -> PreToolUse hook removed")
940
+
941
+
942
+ def _agents_install(project_dir: Path, platform: str) -> None:
943
+ """Write the graphify section to the local AGENTS.md (Codex/OpenCode/OpenClaw)."""
944
+ target = (project_dir or Path(".")) / "AGENTS.md"
945
+
946
+ if target.exists():
947
+ content = target.read_text(encoding="utf-8")
948
+ new_content = _replace_or_append_section(
949
+ content, _AGENTS_MD_MARKER, _AGENTS_MD_SECTION
950
+ )
951
+ else:
952
+ new_content = _AGENTS_MD_SECTION
953
+
954
+ if target.exists() and new_content == target.read_text(encoding="utf-8"):
955
+ print(f"graphify already configured in {target.resolve()} (no change)")
956
+ else:
957
+ target.write_text(new_content, encoding="utf-8")
958
+ print(f"graphify section written to {target.resolve()}")
959
+
960
+ if platform == "codex":
961
+ _install_codex_hook(project_dir or Path("."))
962
+ elif platform == "opencode":
963
+ _install_opencode_plugin(project_dir or Path("."))
964
+
965
+ print()
966
+ print(f"{platform.capitalize()} will now check the knowledge graph before answering")
967
+ print("codebase questions and rebuild it after code changes.")
968
+ if platform not in ("codex", "opencode"):
969
+ print()
970
+ print("Note: unlike Claude Code, there is no PreToolUse hook equivalent for")
971
+ print(f"{platform.capitalize()} — the AGENTS.md rules are the always-on mechanism.")
972
+
973
+
974
+ def _agents_uninstall(project_dir: Path, platform: str = "") -> None:
975
+ """Remove the graphify section from the local AGENTS.md."""
976
+ target = (project_dir or Path(".")) / "AGENTS.md"
977
+
978
+ if not target.exists():
979
+ print("No AGENTS.md found in current directory - nothing to do")
980
+ return
981
+
982
+ content = target.read_text(encoding="utf-8")
983
+ if _AGENTS_MD_MARKER not in content:
984
+ print("graphify section not found in AGENTS.md - nothing to do")
985
+ return
986
+
987
+ cleaned = re.sub(
988
+ r"\n*## graphify\n.*?(?=\n## |\Z)",
989
+ "",
990
+ content,
991
+ flags=re.DOTALL,
992
+ ).rstrip()
993
+ if cleaned:
994
+ target.write_text(cleaned + "\n", encoding="utf-8")
995
+ print(f"graphify section removed from {target.resolve()}")
996
+ else:
997
+ target.unlink()
998
+ print(f"AGENTS.md was empty after removal - deleted {target.resolve()}")
999
+
1000
+ if platform == "opencode":
1001
+ _uninstall_opencode_plugin(project_dir or Path("."))
1002
+
1003
+
1004
+ def claude_install(project_dir: Path | None = None) -> None:
1005
+ """Write the graphify section to the local CLAUDE.md."""
1006
+ target = (project_dir or Path(".")) / "CLAUDE.md"
1007
+
1008
+ if target.exists():
1009
+ content = target.read_text(encoding="utf-8")
1010
+ new_content = _replace_or_append_section(
1011
+ content, _CLAUDE_MD_MARKER, _CLAUDE_MD_SECTION
1012
+ )
1013
+ else:
1014
+ new_content = _CLAUDE_MD_SECTION
1015
+
1016
+ if target.exists() and new_content == target.read_text(encoding="utf-8"):
1017
+ print(f"graphify already configured in {target.resolve()} (no change)")
1018
+ else:
1019
+ target.write_text(new_content, encoding="utf-8")
1020
+ print(f"graphify section written to {target.resolve()}")
1021
+
1022
+ # Always re-install the Claude Code PreToolUse hook so an old hook
1023
+ # payload (e.g. pre-issue-#580 wording) is replaced on upgrade.
1024
+ _install_claude_hook(project_dir or Path("."))
1025
+
1026
+ print()
1027
+ print("Claude Code will now check the knowledge graph before answering")
1028
+ print("codebase questions and rebuild it after code changes.")
1029
+
1030
+
1031
+ def _install_claude_hook(project_dir: Path) -> None:
1032
+ """Add graphify PreToolUse hook to .claude/settings.json."""
1033
+ settings_path = project_dir / ".claude" / "settings.json"
1034
+ settings_path.parent.mkdir(parents=True, exist_ok=True)
1035
+
1036
+ if settings_path.exists():
1037
+ try:
1038
+ settings = json.loads(settings_path.read_text(encoding="utf-8"))
1039
+ except json.JSONDecodeError:
1040
+ settings = {}
1041
+ else:
1042
+ settings = {}
1043
+
1044
+ hooks = settings.setdefault("hooks", {})
1045
+ pre_tool = hooks.setdefault("PreToolUse", [])
1046
+
1047
+ hooks["PreToolUse"] = [h for h in pre_tool if not (h.get("matcher") in ("Glob|Grep", "Bash") and "graphify" in str(h))]
1048
+ hooks["PreToolUse"].append(_SETTINGS_HOOK)
1049
+ settings_path.write_text(json.dumps(settings, indent=2), encoding="utf-8")
1050
+ print(f" .claude/settings.json -> PreToolUse hook registered")
1051
+
1052
+
1053
+ def _uninstall_claude_hook(project_dir: Path) -> None:
1054
+ """Remove graphify PreToolUse hook from .claude/settings.json."""
1055
+ settings_path = project_dir / ".claude" / "settings.json"
1056
+ if not settings_path.exists():
1057
+ return
1058
+ try:
1059
+ settings = json.loads(settings_path.read_text(encoding="utf-8"))
1060
+ except json.JSONDecodeError:
1061
+ return
1062
+ pre_tool = settings.get("hooks", {}).get("PreToolUse", [])
1063
+ filtered = [h for h in pre_tool if not (h.get("matcher") in ("Glob|Grep", "Bash") and "graphify" in str(h))]
1064
+ if len(filtered) == len(pre_tool):
1065
+ return
1066
+ settings["hooks"]["PreToolUse"] = filtered
1067
+ settings_path.write_text(json.dumps(settings, indent=2), encoding="utf-8")
1068
+ print(f" .claude/settings.json -> PreToolUse hook removed")
1069
+
1070
+
1071
+ def uninstall_all(project_dir: Path | None = None, purge: bool = False) -> None:
1072
+ """Remove graphify from every platform detected in the current project."""
1073
+ pd = project_dir or Path(".")
1074
+ print("Uninstalling graphify from all detected platforms...\n")
1075
+
1076
+ # Skill-file / config-section uninstallers
1077
+ claude_uninstall(pd)
1078
+ gemini_uninstall(pd)
1079
+ vscode_uninstall(pd)
1080
+ _cursor_uninstall(pd)
1081
+ _kiro_uninstall(pd)
1082
+ _antigravity_uninstall(pd)
1083
+ # AGENTS.md covers: codex, aider, opencode, claw, droid, trae, trae-cn, hermes, copilot
1084
+ _agents_uninstall(pd)
1085
+ _uninstall_opencode_plugin(pd)
1086
+ _uninstall_codex_hook(pd)
1087
+
1088
+ # Git hook
1089
+ try:
1090
+ from graphify.hooks import uninstall as hook_uninstall
1091
+ result = hook_uninstall(pd)
1092
+ if result:
1093
+ print(result)
1094
+ except Exception:
1095
+ pass
1096
+
1097
+ if purge:
1098
+ import shutil as _shutil
1099
+ out = pd / "graphify-out"
1100
+ if out.exists():
1101
+ _shutil.rmtree(out)
1102
+ print(f"\n graphify-out/ -> deleted (--purge)")
1103
+ else:
1104
+ print("\n graphify-out/ -> not found (nothing to purge)")
1105
+
1106
+ print("\nDone. Run 'pip uninstall graphifyy' to remove the package itself.")
1107
+
1108
+
1109
+ def claude_uninstall(project_dir: Path | None = None) -> None:
1110
+ """Remove the graphify section from the local CLAUDE.md."""
1111
+ target = (project_dir or Path(".")) / "CLAUDE.md"
1112
+
1113
+ if not target.exists():
1114
+ print("No CLAUDE.md found in current directory - nothing to do")
1115
+ return
1116
+
1117
+ content = target.read_text(encoding="utf-8")
1118
+ if _CLAUDE_MD_MARKER not in content:
1119
+ print("graphify section not found in CLAUDE.md - nothing to do")
1120
+ return
1121
+
1122
+ # Remove the ## graphify section: from the marker to the next ## heading or EOF
1123
+ cleaned = re.sub(
1124
+ r"\n*## graphify\n.*?(?=\n## |\Z)",
1125
+ "",
1126
+ content,
1127
+ flags=re.DOTALL,
1128
+ ).rstrip()
1129
+ if cleaned:
1130
+ target.write_text(cleaned + "\n", encoding="utf-8")
1131
+ print(f"graphify section removed from {target.resolve()}")
1132
+ else:
1133
+ target.unlink()
1134
+ print(f"CLAUDE.md was empty after removal - deleted {target.resolve()}")
1135
+
1136
+ _uninstall_claude_hook(project_dir or Path("."))
1137
+
1138
+
1139
+ def _clone_repo(url: str, branch: str | None = None, out_dir: Path | None = None) -> Path:
1140
+ """Clone a GitHub repo to a local cache dir and return the path.
1141
+
1142
+ Clones into ~/.graphify/repos/<owner>/<repo> by default so repeated
1143
+ runs on the same URL reuse the existing clone (git pull instead of clone).
1144
+ """
1145
+ import subprocess as _sp
1146
+ import re as _re
1147
+
1148
+ # Normalise URL — strip trailing .git if present
1149
+ url = url.rstrip("/")
1150
+ if not url.endswith(".git"):
1151
+ git_url = url + ".git"
1152
+ else:
1153
+ git_url = url
1154
+ url = url[:-4]
1155
+
1156
+ # Extract owner/repo from URL
1157
+ m = _re.search(r"github\.com[:/]([^/]+)/([^/]+?)(?:\.git)?$", url)
1158
+ if not m:
1159
+ print(f"error: not a recognised GitHub URL: {url}", file=sys.stderr)
1160
+ sys.exit(1)
1161
+ owner, repo = m.group(1), m.group(2)
1162
+
1163
+ if out_dir:
1164
+ dest = out_dir
1165
+ else:
1166
+ dest = Path.home() / ".graphify" / "repos" / owner / repo
1167
+
1168
+ if branch and branch.startswith("-"):
1169
+ print(f"error: invalid branch name: {branch!r}", file=sys.stderr)
1170
+ sys.exit(1)
1171
+
1172
+ if dest.exists():
1173
+ print(f"Repo already cloned at {dest} — pulling latest...", flush=True)
1174
+ cmd = ["git", "-C", str(dest), "pull"]
1175
+ if branch:
1176
+ cmd += ["origin", "--", branch]
1177
+ result = _sp.run(cmd, capture_output=True, text=True)
1178
+ if result.returncode != 0:
1179
+ print(f"warning: git pull failed:\n{result.stderr}", file=sys.stderr)
1180
+ else:
1181
+ dest.parent.mkdir(parents=True, exist_ok=True)
1182
+ print(f"Cloning {url} → {dest} ...", flush=True)
1183
+ cmd = ["git", "clone", "--depth", "1"]
1184
+ if branch:
1185
+ cmd += ["--branch", branch]
1186
+ cmd += ["--", git_url, str(dest)]
1187
+ result = _sp.run(cmd, capture_output=True, text=True)
1188
+ if result.returncode != 0:
1189
+ print(f"error: git clone failed:\n{result.stderr}", file=sys.stderr)
1190
+ sys.exit(1)
1191
+
1192
+ print(f"Ready at: {dest}", flush=True)
1193
+ return dest
1194
+
1195
+
1196
+ def main() -> None:
1197
+ # Check all known skill install locations for a stale version stamp.
1198
+ # Skip during install/uninstall (hook writes trigger a fresh check anyway).
1199
+ # Skip during hook-check — it runs on every editor tool use and must be silent.
1200
+ # Deduplicate paths so platforms sharing the same install dir don't warn twice.
1201
+ _silent_cmds = {"install", "uninstall", "hook-check"}
1202
+ if not any(arg in _silent_cmds for arg in sys.argv):
1203
+ for skill_dst in {Path.home() / cfg["skill_dst"] for cfg in _PLATFORM_CONFIG.values()}:
1204
+ _check_skill_version(skill_dst)
1205
+
1206
+ if len(sys.argv) >= 2 and sys.argv[1] in ("-v", "--version", "version"):
1207
+ print(f"graphify {__version__}")
1208
+ return
1209
+
1210
+ if len(sys.argv) < 2 or sys.argv[1] in ("-h", "--help", "-?"):
1211
+ print("Usage: graphify <command>")
1212
+ print()
1213
+ print("Commands:")
1214
+ print(" install [--platform P] copy skill to platform config dir (claude|windows|codex|opencode|aider|claw|droid|trae|trae-cn|gemini|cursor|antigravity|hermes|kiro|pi)")
1215
+ print(" uninstall remove graphify from all detected platforms in one shot")
1216
+ print(" --purge also delete graphify-out/ directory")
1217
+ print(" path \"A\" \"B\" shortest path between two nodes in graph.json")
1218
+ print(" --graph <path> path to graph.json (default graphify-out/graph.json)")
1219
+ print(" explain \"X\" plain-language explanation of a node and its neighbors")
1220
+ print(" --graph <path> path to graph.json (default graphify-out/graph.json)")
1221
+ print(" clone <github-url> clone a GitHub repo locally and print its path for /graphify")
1222
+ print(" merge-driver <base> <current> <other> git merge driver: union-merge two graph.json files (set up via hook install)")
1223
+ print(" merge-graphs <g1> <g2> merge two or more graph.json files into one cross-repo graph")
1224
+ print(" --out <path> output path (default: graphify-out/merged-graph.json)")
1225
+ print(" --branch <branch> checkout a specific branch (default: repo default)")
1226
+ print(" --out <dir> clone to a custom directory (default: ~/.graphify/repos/<owner>/<repo>)")
1227
+ print(" add <url> fetch a URL and save it to ./raw, then update the graph")
1228
+ print(" --author \"Name\" tag the author of the content")
1229
+ print(" --contributor \"Name\" tag who added it to the corpus")
1230
+ print(" --dir <path> target directory (default: ./raw)")
1231
+ print(" watch <path> watch a folder and rebuild the graph on code changes")
1232
+ print(" update <path> re-extract code files and update the graph (no LLM needed)")
1233
+ print(" --force overwrite graph.json even if the rebuild has fewer nodes")
1234
+ print(" (also: GRAPHIFY_FORCE=1 env var; use after refactors that delete code)")
1235
+ print(" --no-cluster skip clustering, write raw extraction only")
1236
+ print(" cluster-only <path> rerun clustering on an existing graph.json and regenerate report")
1237
+ print(" --no-viz skip graph.html generation (useful for >5000 node graphs / CI)")
1238
+ print(" --graph <path> path to graph.json (default <path>/graphify-out/graph.json)")
1239
+ print(" query \"<question>\" BFS traversal of graph.json for a question")
1240
+ print(" --dfs use depth-first instead of breadth-first")
1241
+ print(" --context C explicit edge-context filter (repeatable)")
1242
+ print(" --budget N cap output at N tokens (default 2000)")
1243
+ print(" --graph <path> path to graph.json (default graphify-out/graph.json)")
1244
+ print(" save-result save a Q&A result to graphify-out/memory/ for graph feedback loop")
1245
+ print(" --question Q the question asked")
1246
+ print(" --answer A the answer to save")
1247
+ print(" --type T query type: query|path_query|explain (default: query)")
1248
+ print(" --nodes N1 N2 ... source node labels cited in the answer")
1249
+ print(" --memory-dir DIR memory directory (default: graphify-out/memory)")
1250
+ print(" check-update <path> check needs_update flag and notify if semantic re-extraction is pending (cron-safe)")
1251
+ print(" tree emit a D3 v7 collapsible-tree HTML for graph.json")
1252
+ print(" --graph PATH path to graph.json (default graphify-out/graph.json)")
1253
+ print(" --output HTML output path (default graphify-out/GRAPH_TREE.html)")
1254
+ print(" --root PATH filesystem root for the hierarchy")
1255
+ print(" --max-children N cap children per node (default 200)")
1256
+ print(" --top-k-edges N per-symbol outbound edges in inspector (default 12)")
1257
+ print(" --label NAME project label in header")
1258
+ print(" extract <path> headless full extraction (AST + semantic LLM) for CI/scripts")
1259
+ print(" --backend B gemini|kimi|claude|openai|deepseek|ollama (default: whichever API key is set)")
1260
+ print(" --model M override backend default model")
1261
+ print(" --max-workers N AST extraction subprocess count (default: cpu_count)")
1262
+ print(" --token-budget N per-chunk token cap for semantic extraction (default: 60000)")
1263
+ print(" --max-concurrency N parallel semantic chunks in flight (default: 4; set 1 for local LLMs)")
1264
+ print(" --api-timeout S per-request timeout in seconds for the LLM client (default: 600)")
1265
+ print(" --out DIR output dir (default: <path>); writes <DIR>/graphify-out/")
1266
+ print(" --google-workspace export .gdoc/.gsheet/.gslides shortcuts via gws before extraction")
1267
+ print(" --no-cluster skip clustering, write raw extraction only")
1268
+ print(" --global also merge the resulting graph into the global graph")
1269
+ print(" --as <tag> repo tag for --global (default: target directory name)")
1270
+ print(" global add <graph.json> add/update a project graph in the global graph (~/.graphify/global-graph.json)")
1271
+ print(" --as <tag> repo tag (default: parent directory name)")
1272
+ print(" global remove <tag> remove a repo's nodes from the global graph")
1273
+ print(" global list list repos in the global graph")
1274
+ print(" global path print path to the global graph file")
1275
+ print(" benchmark [graph.json] measure token reduction vs naive full-corpus approach")
1276
+ print(" export callflow-html emit Mermaid-based architecture/call-flow HTML")
1277
+ print(" hook install install post-commit/post-checkout git hooks (all platforms)")
1278
+ print(" hook uninstall remove git hooks")
1279
+ print(" hook status check if git hooks are installed")
1280
+ print(" gemini install write GEMINI.md section + BeforeTool hook (Gemini CLI)")
1281
+ print(" gemini uninstall remove GEMINI.md section + BeforeTool hook")
1282
+ print(" cursor install write .cursor/rules/graphify.mdc (Cursor)")
1283
+ print(" cursor uninstall remove .cursor/rules/graphify.mdc")
1284
+ print(" claude install write graphify section to CLAUDE.md + PreToolUse hook (Claude Code)")
1285
+ print(" claude uninstall remove graphify section from CLAUDE.md + PreToolUse hook")
1286
+ print(" codex install write graphify section to AGENTS.md (Codex)")
1287
+ print(" codex uninstall remove graphify section from AGENTS.md")
1288
+ print(" opencode install write graphify section to AGENTS.md + tool.execute.before plugin (OpenCode)")
1289
+ print(" opencode uninstall remove graphify section from AGENTS.md + plugin")
1290
+ print(" aider install write graphify section to AGENTS.md (Aider)")
1291
+ print(" aider uninstall remove graphify section from AGENTS.md")
1292
+ print(" copilot install copy graphify skill to ~/.copilot/skills (GitHub Copilot CLI)")
1293
+ print(" copilot uninstall remove graphify skill from ~/.copilot/skills")
1294
+ print(" vscode install configure VS Code Copilot Chat (skill + .github/copilot-instructions.md)")
1295
+ print(" vscode uninstall remove VS Code Copilot Chat configuration")
1296
+ print(" claw install write graphify section to AGENTS.md (OpenClaw)")
1297
+ print(" claw uninstall remove graphify section from AGENTS.md")
1298
+ print(" droid install write graphify section to AGENTS.md (Factory Droid)")
1299
+ print(" droid uninstall remove graphify section from AGENTS.md")
1300
+ print(" trae install write graphify section to AGENTS.md (Trae)")
1301
+ print(" trae uninstall remove graphify section from AGENTS.md")
1302
+ print(" trae-cn install write graphify section to AGENTS.md (Trae CN)")
1303
+ print(" trae-cn uninstall remove graphify section from AGENTS.md")
1304
+ print(" antigravity install write .agents/rules + .agents/workflows + skill (Google Antigravity)")
1305
+ print(" antigravity uninstall remove .agents/rules, .agents/workflows, and skill")
1306
+ print(" hermes install write skill to ~/.hermes/skills/graphify/ (Hermes)")
1307
+ print(" hermes uninstall remove skill from ~/.hermes/skills/graphify/")
1308
+ print(" kiro install write skill to .kiro/skills/graphify/ + steering file (Kiro IDE/CLI)")
1309
+ print(" kiro uninstall remove skill + steering file")
1310
+ print(" pi install write skill to ~/.pi/agent/skills/graphify/ (Pi coding agent)")
1311
+ print(" pi uninstall remove skill from ~/.pi/agent/skills/graphify/")
1312
+ print()
1313
+ return
1314
+
1315
+ cmd = sys.argv[1]
1316
+
1317
+ # Universal help guard: -h/--help/-? anywhere after the command shows help
1318
+ # and stops — prevents flags from silently triggering destructive subcommands
1319
+ # (e.g. "cursor install --help" was silently installing into Cursor, #821).
1320
+ # Exempt: free-text commands (user string may contain these tokens), and
1321
+ # "install"/"uninstall" which have their own per-subcommand help handlers.
1322
+ _FREE_TEXT_CMDS = {"query", "explain", "path", "save-result", "install", "uninstall"}
1323
+ if cmd not in _FREE_TEXT_CMDS and any(a in {"-h", "--help", "-?"} for a in sys.argv[2:]):
1324
+ print(f"Run 'graphify --help' for full usage.")
1325
+ return
1326
+
1327
+ if cmd == "install":
1328
+ # Default to windows platform on Windows, claude elsewhere
1329
+ default_platform = "windows" if platform.system() == "Windows" else "claude"
1330
+ selected_platform: str | None = None
1331
+ args = sys.argv[2:]
1332
+ i = 0
1333
+ while i < len(args):
1334
+ arg = args[i]
1335
+ if arg in ("-h", "--help"):
1336
+ _print_install_usage()
1337
+ return
1338
+ if arg.startswith("--platform="):
1339
+ candidate = arg.split("=", 1)[1]
1340
+ if selected_platform and selected_platform != candidate:
1341
+ print("error: specify install platform only once", file=sys.stderr)
1342
+ sys.exit(1)
1343
+ selected_platform = candidate
1344
+ i += 1
1345
+ elif arg == "--platform":
1346
+ if i + 1 >= len(args):
1347
+ print("error: --platform requires a value", file=sys.stderr)
1348
+ sys.exit(1)
1349
+ candidate = args[i + 1]
1350
+ if selected_platform and selected_platform != candidate:
1351
+ print("error: specify install platform only once", file=sys.stderr)
1352
+ sys.exit(1)
1353
+ selected_platform = candidate
1354
+ i += 2
1355
+ elif arg.startswith("-"):
1356
+ print(f"error: unknown install option '{arg}'", file=sys.stderr)
1357
+ sys.exit(1)
1358
+ else:
1359
+ if selected_platform and selected_platform != arg:
1360
+ print("error: specify install platform only once", file=sys.stderr)
1361
+ sys.exit(1)
1362
+ selected_platform = arg
1363
+ i += 1
1364
+ chosen_platform = selected_platform or default_platform
1365
+ install(platform=chosen_platform)
1366
+ elif cmd == "uninstall":
1367
+ purge = "--purge" in sys.argv[2:]
1368
+ uninstall_all(purge=purge)
1369
+ elif cmd == "claude":
1370
+ subcmd = sys.argv[2] if len(sys.argv) > 2 else ""
1371
+ if subcmd == "install":
1372
+ claude_install()
1373
+ elif subcmd == "uninstall":
1374
+ claude_uninstall()
1375
+ else:
1376
+ print("Usage: graphify claude [install|uninstall]", file=sys.stderr)
1377
+ sys.exit(1)
1378
+ elif cmd == "gemini":
1379
+ subcmd = sys.argv[2] if len(sys.argv) > 2 else ""
1380
+ if subcmd == "install":
1381
+ gemini_install()
1382
+ elif subcmd == "uninstall":
1383
+ gemini_uninstall()
1384
+ else:
1385
+ print("Usage: graphify gemini [install|uninstall]", file=sys.stderr)
1386
+ sys.exit(1)
1387
+ elif cmd == "cursor":
1388
+ subcmd = sys.argv[2] if len(sys.argv) > 2 else ""
1389
+ if subcmd == "install":
1390
+ _cursor_install(Path("."))
1391
+ elif subcmd == "uninstall":
1392
+ _cursor_uninstall(Path("."))
1393
+ else:
1394
+ print("Usage: graphify cursor [install|uninstall]", file=sys.stderr)
1395
+ sys.exit(1)
1396
+ elif cmd == "vscode":
1397
+ subcmd = sys.argv[2] if len(sys.argv) > 2 else ""
1398
+ if subcmd == "install":
1399
+ vscode_install()
1400
+ elif subcmd == "uninstall":
1401
+ vscode_uninstall()
1402
+ else:
1403
+ print("Usage: graphify vscode [install|uninstall]", file=sys.stderr)
1404
+ sys.exit(1)
1405
+ elif cmd == "copilot":
1406
+ subcmd = sys.argv[2] if len(sys.argv) > 2 else ""
1407
+ if subcmd == "install":
1408
+ install(platform="copilot")
1409
+ elif subcmd == "uninstall":
1410
+ skill_dst = Path.home() / _PLATFORM_CONFIG["copilot"]["skill_dst"]
1411
+ removed = []
1412
+ if skill_dst.exists():
1413
+ skill_dst.unlink()
1414
+ removed.append(f"skill removed: {skill_dst}")
1415
+ version_file = skill_dst.parent / ".graphify_version"
1416
+ if version_file.exists():
1417
+ version_file.unlink()
1418
+ for d in (skill_dst.parent, skill_dst.parent.parent, skill_dst.parent.parent.parent):
1419
+ try:
1420
+ d.rmdir()
1421
+ except OSError:
1422
+ break
1423
+ print("; ".join(removed) if removed else "nothing to remove")
1424
+ else:
1425
+ print("Usage: graphify copilot [install|uninstall]", file=sys.stderr)
1426
+ sys.exit(1)
1427
+ elif cmd == "kiro":
1428
+ subcmd = sys.argv[2] if len(sys.argv) > 2 else ""
1429
+ if subcmd == "install":
1430
+ _kiro_install(Path("."))
1431
+ elif subcmd == "uninstall":
1432
+ _kiro_uninstall(Path("."))
1433
+ else:
1434
+ print("Usage: graphify kiro [install|uninstall]", file=sys.stderr)
1435
+ sys.exit(1)
1436
+ elif cmd == "pi":
1437
+ subcmd = sys.argv[2] if len(sys.argv) > 2 else ""
1438
+ if subcmd == "install":
1439
+ install("pi")
1440
+ elif subcmd == "uninstall":
1441
+ skill_dst = Path.home() / ".pi" / "agent" / "skills" / "graphify" / "SKILL.md"
1442
+ if skill_dst.exists():
1443
+ skill_dst.unlink()
1444
+ print(f" skill removed -> {skill_dst}")
1445
+ version_file = skill_dst.parent / ".graphify_version"
1446
+ if version_file.exists():
1447
+ version_file.unlink()
1448
+ for d in (skill_dst.parent, skill_dst.parent.parent, skill_dst.parent.parent.parent):
1449
+ try:
1450
+ d.rmdir()
1451
+ except OSError:
1452
+ break
1453
+ else:
1454
+ print("Usage: graphify pi [install|uninstall]", file=sys.stderr)
1455
+ sys.exit(1)
1456
+ elif cmd in ("aider", "codex", "opencode", "claw", "droid", "trae", "trae-cn", "hermes"):
1457
+ subcmd = sys.argv[2] if len(sys.argv) > 2 else ""
1458
+ if subcmd == "install":
1459
+ _agents_install(Path("."), cmd)
1460
+ elif subcmd == "uninstall":
1461
+ _agents_uninstall(Path("."), platform=cmd)
1462
+ if cmd == "codex":
1463
+ _uninstall_codex_hook(Path("."))
1464
+ else:
1465
+ print(f"Usage: graphify {cmd} [install|uninstall]", file=sys.stderr)
1466
+ sys.exit(1)
1467
+ elif cmd == "antigravity":
1468
+ subcmd = sys.argv[2] if len(sys.argv) > 2 else ""
1469
+ if subcmd == "install":
1470
+ _antigravity_install(Path("."))
1471
+ elif subcmd == "uninstall":
1472
+ _antigravity_uninstall(Path("."))
1473
+ else:
1474
+ print("Usage: graphify antigravity [install|uninstall]", file=sys.stderr)
1475
+ sys.exit(1)
1476
+ elif cmd == "prs":
1477
+ from graphify.prs import cmd_prs
1478
+ cmd_prs(sys.argv[2:])
1479
+ elif cmd == "hook":
1480
+ from graphify.hooks import install as hook_install, uninstall as hook_uninstall, status as hook_status
1481
+ subcmd = sys.argv[2] if len(sys.argv) > 2 else ""
1482
+ if subcmd == "install":
1483
+ print(hook_install(Path(".")))
1484
+ elif subcmd == "uninstall":
1485
+ print(hook_uninstall(Path(".")))
1486
+ elif subcmd == "status":
1487
+ print(hook_status(Path(".")))
1488
+ else:
1489
+ print("Usage: graphify hook [install|uninstall|status]", file=sys.stderr)
1490
+ sys.exit(1)
1491
+ elif cmd == "query":
1492
+ if len(sys.argv) < 3:
1493
+ print("Usage: graphify query \"<question>\" [--dfs] [--context C] [--budget N] [--graph path]", file=sys.stderr)
1494
+ sys.exit(1)
1495
+ from graphify.serve import _query_graph_text
1496
+ from graphify.security import sanitize_label
1497
+ from networkx.readwrite import json_graph
1498
+ question = sys.argv[2]
1499
+ use_dfs = "--dfs" in sys.argv
1500
+ budget = 2000
1501
+ graph_path = _default_graph_path()
1502
+ context_filters: list[str] = []
1503
+ args = sys.argv[3:]
1504
+ i = 0
1505
+ while i < len(args):
1506
+ if args[i] == "--budget" and i + 1 < len(args):
1507
+ try:
1508
+ budget = int(args[i + 1])
1509
+ except ValueError:
1510
+ print(f"error: --budget must be an integer", file=sys.stderr)
1511
+ sys.exit(1)
1512
+ i += 2
1513
+ elif args[i].startswith("--budget="):
1514
+ try:
1515
+ budget = int(args[i].split("=", 1)[1])
1516
+ except ValueError:
1517
+ print(f"error: --budget must be an integer", file=sys.stderr)
1518
+ sys.exit(1)
1519
+ i += 1
1520
+ elif args[i] == "--context" and i + 1 < len(args):
1521
+ context_filters.append(args[i + 1])
1522
+ i += 2
1523
+ elif args[i].startswith("--context="):
1524
+ context_filters.append(args[i].split("=", 1)[1])
1525
+ i += 1
1526
+ elif args[i] == "--graph" and i + 1 < len(args):
1527
+ graph_path = args[i + 1]; i += 2
1528
+ else:
1529
+ i += 1
1530
+ gp = Path(graph_path).resolve()
1531
+ if not gp.exists():
1532
+ print(f"error: graph file not found: {gp}", file=sys.stderr)
1533
+ sys.exit(1)
1534
+ if not gp.suffix == ".json":
1535
+ print(f"error: graph file must be a .json file", file=sys.stderr)
1536
+ sys.exit(1)
1537
+ try:
1538
+ import json as _json
1539
+ import networkx as _nx
1540
+ _raw = _json.loads(gp.read_text(encoding="utf-8"))
1541
+ if "links" not in _raw and "edges" in _raw:
1542
+ _raw = dict(_raw, links=_raw["edges"])
1543
+ try:
1544
+ G = json_graph.node_link_graph(_raw, edges="links")
1545
+ except TypeError:
1546
+ G = json_graph.node_link_graph(_raw)
1547
+ except Exception as exc:
1548
+ print(f"error: could not load graph: {exc}", file=sys.stderr)
1549
+ sys.exit(1)
1550
+ print(
1551
+ _query_graph_text(
1552
+ G,
1553
+ question,
1554
+ mode="dfs" if use_dfs else "bfs",
1555
+ depth=2,
1556
+ token_budget=budget,
1557
+ context_filters=context_filters,
1558
+ )
1559
+ )
1560
+ elif cmd == "save-result":
1561
+ # graphify save-result --question Q --answer A --type T [--nodes N1 N2 ...]
1562
+ import argparse as _ap
1563
+ p = _ap.ArgumentParser(prog="graphify save-result")
1564
+ p.add_argument("--question", required=True)
1565
+ p.add_argument("--answer", required=True)
1566
+ p.add_argument("--type", dest="query_type", default="query")
1567
+ p.add_argument("--nodes", nargs="*", default=[])
1568
+ p.add_argument("--memory-dir", default="graphify-out/memory")
1569
+ opts = p.parse_args(sys.argv[2:])
1570
+ from graphify.ingest import save_query_result as _sqr
1571
+ out = _sqr(
1572
+ question=opts.question,
1573
+ answer=opts.answer,
1574
+ memory_dir=Path(opts.memory_dir),
1575
+ query_type=opts.query_type,
1576
+ source_nodes=opts.nodes or None,
1577
+ )
1578
+ print(f"Saved to {out}")
1579
+ elif cmd == "path":
1580
+ if len(sys.argv) < 4:
1581
+ print("Usage: graphify path \"<source>\" \"<target>\" [--graph path]", file=sys.stderr)
1582
+ sys.exit(1)
1583
+ from graphify.serve import _score_nodes
1584
+ from networkx.readwrite import json_graph
1585
+ import networkx as _nx
1586
+ source_label = sys.argv[2]
1587
+ target_label = sys.argv[3]
1588
+ graph_path = _default_graph_path()
1589
+ args = sys.argv[4:]
1590
+ for i, a in enumerate(args):
1591
+ if a == "--graph" and i + 1 < len(args):
1592
+ graph_path = args[i + 1]
1593
+ gp = Path(graph_path).resolve()
1594
+ if not gp.exists():
1595
+ print(f"error: graph file not found: {gp}", file=sys.stderr)
1596
+ sys.exit(1)
1597
+ _raw = json.loads(gp.read_text(encoding="utf-8"))
1598
+ if "links" not in _raw and "edges" in _raw:
1599
+ _raw = dict(_raw, links=_raw["edges"])
1600
+ # Force directed so the renderer can recover stored caller→callee direction.
1601
+ _raw = {**_raw, "directed": True}
1602
+ try:
1603
+ G = json_graph.node_link_graph(_raw, edges="links")
1604
+ except TypeError:
1605
+ G = json_graph.node_link_graph(_raw)
1606
+ src_scored = _score_nodes(G, [t.lower() for t in source_label.split()])
1607
+ tgt_scored = _score_nodes(G, [t.lower() for t in target_label.split()])
1608
+ if not src_scored:
1609
+ print(f"No node matching '{source_label}' found.", file=sys.stderr)
1610
+ sys.exit(1)
1611
+ if not tgt_scored:
1612
+ print(f"No node matching '{target_label}' found.", file=sys.stderr)
1613
+ sys.exit(1)
1614
+ src_nid, tgt_nid = src_scored[0][1], tgt_scored[0][1]
1615
+ # Ambiguity guard: when both queries resolve to the same node, the
1616
+ # shortest path is trivially zero hops, which is almost never what the
1617
+ # caller wanted (see bug #828).
1618
+ if src_nid == tgt_nid:
1619
+ print(
1620
+ f"'{source_label}' and '{target_label}' both resolved to the same "
1621
+ f"node '{src_nid}'. Use a more specific label or the exact node ID.",
1622
+ file=sys.stderr,
1623
+ )
1624
+ sys.exit(1)
1625
+ for _name, _scored in (("source", src_scored), ("target", tgt_scored)):
1626
+ if len(_scored) >= 2:
1627
+ _top, _runner = _scored[0][0], _scored[1][0]
1628
+ if _top > 0 and (_top - _runner) / _top < 0.10:
1629
+ print(
1630
+ f"warning: {_name} match was ambiguous "
1631
+ f"(top score {_top:g}, runner-up {_runner:g})",
1632
+ file=sys.stderr,
1633
+ )
1634
+ try:
1635
+ path_nodes = _nx.shortest_path(G.to_undirected(as_view=True), src_nid, tgt_nid)
1636
+ except (_nx.NetworkXNoPath, _nx.NodeNotFound):
1637
+ print(f"No path found between '{source_label}' and '{target_label}'.")
1638
+ sys.exit(0)
1639
+ hops = len(path_nodes) - 1
1640
+ segments = []
1641
+ from graphify.build import edge_data
1642
+ for i in range(len(path_nodes) - 1):
1643
+ u, v = path_nodes[i], path_nodes[i + 1]
1644
+ # Check which direction the stored edge points.
1645
+ if G.has_edge(u, v):
1646
+ edata = edge_data(G, u, v)
1647
+ forward = True
1648
+ else:
1649
+ edata = edge_data(G, v, u)
1650
+ forward = False
1651
+ rel = edata.get("relation", "")
1652
+ conf = edata.get("confidence", "")
1653
+ conf_str = f" [{conf}]" if conf else ""
1654
+ if i == 0:
1655
+ segments.append(G.nodes[u].get("label", u))
1656
+ if forward:
1657
+ segments.append(f"--{rel}{conf_str}--> {G.nodes[v].get('label', v)}")
1658
+ else:
1659
+ segments.append(f"<--{rel}{conf_str}-- {G.nodes[v].get('label', v)}")
1660
+ print(f"Shortest path ({hops} hops):\n " + " ".join(segments))
1661
+
1662
+ elif cmd == "explain":
1663
+ if len(sys.argv) < 3:
1664
+ print("Usage: graphify explain \"<node>\" [--graph path]", file=sys.stderr)
1665
+ sys.exit(1)
1666
+ from graphify.serve import _find_node
1667
+ from networkx.readwrite import json_graph
1668
+ label = sys.argv[2]
1669
+ graph_path = _default_graph_path()
1670
+ args = sys.argv[3:]
1671
+ for i, a in enumerate(args):
1672
+ if a == "--graph" and i + 1 < len(args):
1673
+ graph_path = args[i + 1]
1674
+ gp = Path(graph_path).resolve()
1675
+ if not gp.exists():
1676
+ print(f"error: graph file not found: {gp}", file=sys.stderr)
1677
+ sys.exit(1)
1678
+ _raw = json.loads(gp.read_text(encoding="utf-8"))
1679
+ if "links" not in _raw and "edges" in _raw:
1680
+ _raw = dict(_raw, links=_raw["edges"])
1681
+ # Force directed so the renderer can recover stored caller→callee direction.
1682
+ _raw = {**_raw, "directed": True}
1683
+ try:
1684
+ G = json_graph.node_link_graph(_raw, edges="links")
1685
+ except TypeError:
1686
+ G = json_graph.node_link_graph(_raw)
1687
+ matches = _find_node(G, label)
1688
+ if not matches:
1689
+ print(f"No node matching '{label}' found.")
1690
+ sys.exit(0)
1691
+ nid = matches[0]
1692
+ d = G.nodes[nid]
1693
+ print(f"Node: {d.get('label', nid)}")
1694
+ print(f" ID: {nid}")
1695
+ print(f" Source: {d.get('source_file', '')} {d.get('source_location', '')}".rstrip())
1696
+ print(f" Type: {d.get('file_type', '')}")
1697
+ print(f" Community: {d.get('community', '')}")
1698
+ print(f" Degree: {G.degree(nid)}")
1699
+ from graphify.build import edge_data
1700
+ connections: list[tuple[str, str, dict]] = [] # (direction, neighbor_id, edge_data)
1701
+ for nb in G.successors(nid):
1702
+ connections.append(("out", nb, edge_data(G, nid, nb)))
1703
+ for nb in G.predecessors(nid):
1704
+ connections.append(("in", nb, edge_data(G, nb, nid)))
1705
+ if connections:
1706
+ print(f"\nConnections ({len(connections)}):")
1707
+ connections.sort(key=lambda c: G.degree(c[1]), reverse=True)
1708
+ for direction, nb, edata in connections[:20]:
1709
+ rel = edata.get("relation", "")
1710
+ conf = edata.get("confidence", "")
1711
+ arrow = "-->" if direction == "out" else "<--"
1712
+ print(f" {arrow} {G.nodes[nb].get('label', nb)} [{rel}] [{conf}]")
1713
+ if len(connections) > 20:
1714
+ print(f" ... and {len(connections) - 20} more")
1715
+
1716
+ elif cmd == "add":
1717
+ if len(sys.argv) < 3:
1718
+ print("Usage: graphify add <url> [--author Name] [--contributor Name] [--dir ./raw]", file=sys.stderr)
1719
+ sys.exit(1)
1720
+ from graphify.ingest import ingest as _ingest
1721
+ url = sys.argv[2]
1722
+ author: str | None = None
1723
+ contributor: str | None = None
1724
+ target_dir = Path("raw")
1725
+ args = sys.argv[3:]
1726
+ i = 0
1727
+ while i < len(args):
1728
+ if args[i] == "--author" and i + 1 < len(args):
1729
+ author = args[i + 1]; i += 2
1730
+ elif args[i] == "--contributor" and i + 1 < len(args):
1731
+ contributor = args[i + 1]; i += 2
1732
+ elif args[i] == "--dir" and i + 1 < len(args):
1733
+ target_dir = Path(args[i + 1]); i += 2
1734
+ else:
1735
+ i += 1
1736
+ try:
1737
+ saved = _ingest(url, target_dir, author=author, contributor=contributor)
1738
+ print(f"Saved to {saved}")
1739
+ print("Run /graphify --update in your AI assistant to update the graph.")
1740
+ except Exception as exc:
1741
+ print(f"error: {exc}", file=sys.stderr)
1742
+ sys.exit(1)
1743
+
1744
+ elif cmd == "watch":
1745
+ watch_path = Path(sys.argv[2]) if len(sys.argv) > 2 else Path(".")
1746
+ if not watch_path.exists():
1747
+ print(f"error: path not found: {watch_path}", file=sys.stderr)
1748
+ sys.exit(1)
1749
+ from graphify.watch import watch as _watch
1750
+ try:
1751
+ _watch(watch_path)
1752
+ except ImportError as exc:
1753
+ print(f"error: {exc}", file=sys.stderr)
1754
+ sys.exit(1)
1755
+
1756
+ elif cmd == "cluster-only":
1757
+ # Mirror the tree/export arg-parsing pattern: walk argv so flags and
1758
+ # the optional positional path can appear in any order (#724).
1759
+ no_viz = "--no-viz" in sys.argv
1760
+ _min_cs_arg = next((a for a in sys.argv if a.startswith("--min-community-size=")), None)
1761
+ min_community_size = int(_min_cs_arg.split("=")[1]) if _min_cs_arg else 3
1762
+ args = sys.argv[2:]
1763
+ watch_path: Path | None = None
1764
+ graph_override: Path | None = None
1765
+ co_resolution: float = 1.0
1766
+ co_exclude_hubs: float | None = None
1767
+ i_arg = 0
1768
+ while i_arg < len(args):
1769
+ a = args[i_arg]
1770
+ if a == "--graph" and i_arg + 1 < len(args):
1771
+ graph_override = Path(args[i_arg + 1]); i_arg += 2
1772
+ elif a == "--resolution" and i_arg + 1 < len(args):
1773
+ co_resolution = float(args[i_arg + 1]); i_arg += 2
1774
+ elif a.startswith("--resolution="):
1775
+ co_resolution = float(a.split("=", 1)[1]); i_arg += 1
1776
+ elif a == "--exclude-hubs" and i_arg + 1 < len(args):
1777
+ co_exclude_hubs = float(args[i_arg + 1]); i_arg += 2
1778
+ elif a.startswith("--exclude-hubs="):
1779
+ co_exclude_hubs = float(a.split("=", 1)[1]); i_arg += 1
1780
+ elif a == "--no-viz" or a.startswith("--min-community-size="):
1781
+ i_arg += 1
1782
+ elif a.startswith("--"):
1783
+ i_arg += 1
1784
+ elif watch_path is None:
1785
+ watch_path = Path(a); i_arg += 1
1786
+ else:
1787
+ i_arg += 1
1788
+ if watch_path is None:
1789
+ watch_path = Path(".")
1790
+ graph_json = graph_override if graph_override is not None else watch_path / "graphify-out" / "graph.json"
1791
+ if not graph_json.exists():
1792
+ print(f"error: no graph found at {graph_json} — run /graphify first", file=sys.stderr)
1793
+ sys.exit(1)
1794
+ from networkx.readwrite import json_graph as _jg
1795
+ from graphify.build import build_from_json
1796
+ from graphify.cluster import cluster, score_all
1797
+ from graphify.analyze import god_nodes, surprising_connections, suggest_questions
1798
+ from graphify.report import generate
1799
+ from graphify.export import to_json, to_html
1800
+ print("Loading existing graph...")
1801
+ _raw = json.loads(graph_json.read_text(encoding="utf-8"))
1802
+ _directed = bool(_raw.get("directed", False))
1803
+ G = build_from_json(_raw, directed=_directed)
1804
+ print(f"Graph: {G.number_of_nodes()} nodes, {G.number_of_edges()} edges")
1805
+ print("Re-clustering...")
1806
+ communities = cluster(G, resolution=co_resolution, exclude_hubs_percentile=co_exclude_hubs)
1807
+ cohesion = score_all(G, communities)
1808
+ gods = god_nodes(G)
1809
+ surprises = surprising_connections(G, communities)
1810
+ out = watch_path / "graphify-out"
1811
+ labels_path = out / ".graphify_labels.json"
1812
+ if labels_path.exists():
1813
+ try:
1814
+ labels = {int(k): v for k, v in json.loads(labels_path.read_text(encoding="utf-8")).items()}
1815
+ except Exception:
1816
+ labels = {cid: f"Community {cid}" for cid in communities}
1817
+ else:
1818
+ labels = {cid: f"Community {cid}" for cid in communities}
1819
+ questions = suggest_questions(G, communities, labels)
1820
+ tokens = {"input": 0, "output": 0}
1821
+ from graphify.export import _git_head as _gh
1822
+ _commit = _gh()
1823
+ report = generate(G, communities, cohesion, labels, gods, surprises,
1824
+ {"warning": "cluster-only mode — file stats not available"},
1825
+ tokens, str(watch_path), suggested_questions=questions,
1826
+ min_community_size=min_community_size, built_at_commit=_commit)
1827
+ (out / "GRAPH_REPORT.md").write_text(report, encoding="utf-8")
1828
+ to_json(G, communities, str(out / "graph.json"))
1829
+ labels_path.write_text(json.dumps({str(k): v for k, v in labels.items()}, ensure_ascii=False), encoding="utf-8")
1830
+
1831
+ # Mirror watch.py pattern: gate to_html so core outputs (graph.json +
1832
+ # GRAPH_REPORT.md) always land. Honor --no-viz explicitly; otherwise
1833
+ # fall back to ValueError handling so an oversized graph doesn't crash
1834
+ # the CLI mid-write and leave a stale graph.html on disk.
1835
+ html_target = out / "graph.html"
1836
+ if no_viz:
1837
+ if html_target.exists():
1838
+ html_target.unlink()
1839
+ print(f"Done — {len(communities)} communities. GRAPH_REPORT.md and graph.json updated (--no-viz; graph.html removed).")
1840
+ else:
1841
+ try:
1842
+ to_html(G, communities, str(html_target), community_labels=labels or None)
1843
+ print(f"Done — {len(communities)} communities. GRAPH_REPORT.md, graph.json and graph.html updated.")
1844
+ except ValueError as viz_err:
1845
+ if html_target.exists():
1846
+ html_target.unlink()
1847
+ print(f"Skipped graph.html: {viz_err}")
1848
+ print(f"Done — {len(communities)} communities. GRAPH_REPORT.md and graph.json updated.")
1849
+
1850
+ elif cmd == "update":
1851
+ force = os.environ.get("GRAPHIFY_FORCE", "").lower() in ("1", "true", "yes")
1852
+ no_cluster = False
1853
+ args = sys.argv[2:]
1854
+ watch_arg: str | None = None
1855
+ for a in args:
1856
+ if a == "--force":
1857
+ force = True
1858
+ continue
1859
+ if a == "--no-cluster":
1860
+ no_cluster = True
1861
+ continue
1862
+ if a.startswith("-"):
1863
+ print(f"error: unknown update option: {a}", file=sys.stderr)
1864
+ sys.exit(2)
1865
+ if watch_arg is not None:
1866
+ print("error: update accepts at most one path argument", file=sys.stderr)
1867
+ sys.exit(2)
1868
+ watch_arg = a
1869
+
1870
+ if watch_arg is not None:
1871
+ watch_path = Path(watch_arg)
1872
+ else:
1873
+ # Try to recover the scan root saved by the last full build
1874
+ saved = Path(_GRAPHIFY_OUT) / ".graphify_root"
1875
+ if saved.exists():
1876
+ watch_path = Path(saved.read_text(encoding="utf-8").strip())
1877
+ else:
1878
+ watch_path = Path(".")
1879
+ if not watch_path.exists():
1880
+ print(f"error: path not found: {watch_path}", file=sys.stderr)
1881
+ sys.exit(1)
1882
+ from graphify.watch import _rebuild_code
1883
+ print(f"Re-extracting code files in {watch_path} (no LLM needed)...")
1884
+ # Interactive CLI: block on the per-repo lock rather than skip, so the
1885
+ # user sees their explicit `graphify update` complete instead of
1886
+ # exiting silently when a hook-driven rebuild happens to be running.
1887
+ ok = _rebuild_code(watch_path, force=force, no_cluster=no_cluster, block_on_lock=True)
1888
+ if ok:
1889
+ print("Code graph updated. For doc/paper/image changes run /graphify --update in your AI assistant.")
1890
+ if not (
1891
+ os.environ.get("GEMINI_API_KEY")
1892
+ or os.environ.get("GOOGLE_API_KEY")
1893
+ or os.environ.get("MOONSHOT_API_KEY")
1894
+ or os.environ.get("DEEPSEEK_API_KEY")
1895
+ or os.environ.get("GRAPHIFY_NO_TIPS")
1896
+ ):
1897
+ print("Tip: set GEMINI_API_KEY or GOOGLE_API_KEY to use Gemini for semantic extraction.")
1898
+ else:
1899
+ print("Nothing to update or rebuild failed — check output above.", file=sys.stderr)
1900
+ sys.exit(1)
1901
+
1902
+ elif cmd == "hook-check":
1903
+ # Codex Desktop rejects hookSpecificOutput.additionalContext on PreToolUse.
1904
+ # Keep this as a cross-platform no-op so installed hooks never break Bash
1905
+ # tool calls. Graph guidance reaches the agent via AGENTS.md / skill instead.
1906
+ sys.exit(0)
1907
+ elif cmd == "check-update":
1908
+ if len(sys.argv) < 3:
1909
+ print("Usage: graphify check-update <path>", file=sys.stderr)
1910
+ sys.exit(1)
1911
+ from graphify.watch import check_update
1912
+ check_update(Path(sys.argv[2]).resolve())
1913
+ sys.exit(0)
1914
+ elif cmd == "tree":
1915
+ # Emit a D3 v7 collapsible-tree HTML view of graph.json:
1916
+ # expand-all / collapse-all / reset-view buttons, multi-line
1917
+ # wrapText labels with separately-coloured name + count,
1918
+ # depth-based palette, click-to-toggle subtree, hover inspector
1919
+ # showing top-K outbound edges per symbol.
1920
+ from typing import Optional as _Opt
1921
+ from graphify.tree_html import write_tree_html, DEFAULT_MAX_CHILDREN
1922
+ graph_path = Path(_GRAPHIFY_OUT) / "graph.json"
1923
+ output_path: "_Opt[Path]" = None
1924
+ root: "_Opt[str]" = None
1925
+ max_children = DEFAULT_MAX_CHILDREN
1926
+ top_k_edges = 0
1927
+ project_label: "_Opt[str]" = None
1928
+ args = sys.argv[2:]
1929
+ i_arg = 0
1930
+ while i_arg < len(args):
1931
+ a = args[i_arg]
1932
+ if a == "--graph" and i_arg + 1 < len(args):
1933
+ graph_path = Path(args[i_arg + 1]); i_arg += 2
1934
+ elif a == "--output" and i_arg + 1 < len(args):
1935
+ output_path = Path(args[i_arg + 1]); i_arg += 2
1936
+ elif a == "--root" and i_arg + 1 < len(args):
1937
+ root = args[i_arg + 1]; i_arg += 2
1938
+ elif a == "--max-children" and i_arg + 1 < len(args):
1939
+ max_children = int(args[i_arg + 1]); i_arg += 2
1940
+ elif a == "--top-k-edges" and i_arg + 1 < len(args):
1941
+ top_k_edges = int(args[i_arg + 1]); i_arg += 2
1942
+ elif a == "--label" and i_arg + 1 < len(args):
1943
+ project_label = args[i_arg + 1]; i_arg += 2
1944
+ elif a in ("-h", "--help"):
1945
+ print("Usage: graphify tree [--graph PATH] [--output HTML]")
1946
+ print(" --graph PATH path to graph.json (default graphify-out/graph.json)")
1947
+ print(" --output HTML output path (default graphify-out/GRAPH_TREE.html)")
1948
+ print(" --root PATH filesystem root (default: longest common dir of all source_files)")
1949
+ print(" --max-children N cap visible children per node (default 200)")
1950
+ print(" --top-k-edges N pre-compute top-K outbound edges per symbol (default 12)")
1951
+ print(" --label NAME project label shown in the page header")
1952
+ return
1953
+ else:
1954
+ i_arg += 1
1955
+ if not graph_path.is_file():
1956
+ print(f"error: graph.json not found at {graph_path}", file=sys.stderr)
1957
+ sys.exit(1)
1958
+ if output_path is None:
1959
+ output_path = graph_path.parent / "GRAPH_TREE.html"
1960
+ out = write_tree_html(
1961
+ graph_path=graph_path, output_path=output_path,
1962
+ root=root, max_children=max_children,
1963
+ top_k_edges=top_k_edges, project_label=project_label,
1964
+ )
1965
+ size_kb = out.stat().st_size / 1024
1966
+ print(f"wrote {out} ({size_kb:.1f} KB)")
1967
+ print(f"open with: xdg-open {out} (or file://{out.resolve()})")
1968
+ sys.exit(0)
1969
+
1970
+ elif cmd == "merge-driver":
1971
+ # git merge driver for graph.json — takes (base, current, other) and writes
1972
+ # the union of current+other nodes/edges back to current. Exits 1 on
1973
+ # corrupt input so git surfaces the conflict instead of silently
1974
+ # accepting a poisoned merge (see F-005).
1975
+ # Usage: graphify merge-driver %O %A %B (set in .git/config merge driver)
1976
+ if len(sys.argv) < 5:
1977
+ print("Usage: graphify merge-driver <base> <current> <other>", file=sys.stderr)
1978
+ sys.exit(1)
1979
+ _base_path, _current_path, _other_path = sys.argv[2], sys.argv[3], sys.argv[4]
1980
+ # Hard caps so a malicious or corrupted graph.json cannot exhaust memory
1981
+ # at parse time. 50 MB / 100k nodes are well above any realistic graph
1982
+ # (typical graphs are <5 MB / <50k nodes); anything larger should fail
1983
+ # the merge so a human can investigate.
1984
+ _MERGE_MAX_BYTES = 50 * 1024 * 1024
1985
+ _MERGE_MAX_NODES = 100_000
1986
+ import networkx as _nx
1987
+ from networkx.readwrite import json_graph as _jg
1988
+ def _load_graph(p: str):
1989
+ path_obj = Path(p)
1990
+ try:
1991
+ size = path_obj.stat().st_size
1992
+ except OSError as exc:
1993
+ raise RuntimeError(f"cannot stat {p}: {exc}") from exc
1994
+ if size > _MERGE_MAX_BYTES:
1995
+ raise RuntimeError(
1996
+ f"graph.json {p} is {size} bytes, exceeds {_MERGE_MAX_BYTES}-byte cap"
1997
+ )
1998
+ data = json.loads(path_obj.read_text(encoding="utf-8"))
1999
+ try:
2000
+ return _jg.node_link_graph(data, edges="links"), data
2001
+ except TypeError:
2002
+ return _jg.node_link_graph(data), data
2003
+ try:
2004
+ G_cur, _ = _load_graph(_current_path)
2005
+ G_oth, _ = _load_graph(_other_path)
2006
+ except Exception as exc:
2007
+ print(f"[graphify merge-driver] error loading graphs: {exc}", file=sys.stderr)
2008
+ sys.exit(1) # surface the conflict so git doesn't accept a corrupt merge
2009
+ merged = _nx.compose(G_cur, G_oth)
2010
+ if merged.number_of_nodes() > _MERGE_MAX_NODES:
2011
+ print(
2012
+ f"[graphify merge-driver] merged graph has {merged.number_of_nodes()} nodes, "
2013
+ f"exceeds {_MERGE_MAX_NODES}-node cap; aborting merge.",
2014
+ file=sys.stderr,
2015
+ )
2016
+ sys.exit(1)
2017
+ try:
2018
+ out_data = _jg.node_link_data(merged, edges="links")
2019
+ except TypeError:
2020
+ out_data = _jg.node_link_data(merged)
2021
+ Path(_current_path).write_text(json.dumps(out_data, indent=2), encoding="utf-8")
2022
+ sys.exit(0)
2023
+
2024
+ elif cmd == "merge-graphs":
2025
+ # graphify merge-graphs graph1.json graph2.json ... --out merged.json
2026
+ args = sys.argv[2:]
2027
+ graph_paths: list[Path] = []
2028
+ out_path = Path(_GRAPHIFY_OUT) / "merged-graph.json"
2029
+ i = 0
2030
+ while i < len(args):
2031
+ if args[i] == "--out" and i + 1 < len(args):
2032
+ out_path = Path(args[i + 1]); i += 2
2033
+ else:
2034
+ graph_paths.append(Path(args[i])); i += 1
2035
+ if len(graph_paths) < 2:
2036
+ print("Usage: graphify merge-graphs <graph1.json> <graph2.json> [...] [--out merged.json]", file=sys.stderr)
2037
+ sys.exit(1)
2038
+ import networkx as _nx
2039
+ from networkx.readwrite import json_graph as _jg
2040
+ from graphify.build import prefix_graph_for_global as _prefix
2041
+ graphs = []
2042
+ for gp in graph_paths:
2043
+ if not gp.exists():
2044
+ print(f"error: not found: {gp}", file=sys.stderr)
2045
+ sys.exit(1)
2046
+ data = json.loads(gp.read_text(encoding="utf-8"))
2047
+ # Normalize edges/links key before loading — graphify writes "links"
2048
+ # via node_link_data but older runs may have used "edges" (#738).
2049
+ if "links" not in data and "edges" in data:
2050
+ data = dict(data, links=data["edges"])
2051
+ try:
2052
+ G = _jg.node_link_graph(data, edges="links")
2053
+ except TypeError:
2054
+ G = _jg.node_link_graph(data)
2055
+ graphs.append(G)
2056
+ merged = _nx.Graph()
2057
+ for G, gp in zip(graphs, graph_paths):
2058
+ repo_tag = gp.parent.parent.name # graphify-out/../ → repo dir name
2059
+ prefixed = _prefix(G, repo_tag)
2060
+ merged = _nx.compose(merged, prefixed)
2061
+ try:
2062
+ out_data = _jg.node_link_data(merged, edges="links")
2063
+ except TypeError:
2064
+ out_data = _jg.node_link_data(merged)
2065
+ out_path.parent.mkdir(parents=True, exist_ok=True)
2066
+ out_path.write_text(json.dumps(out_data, indent=2), encoding="utf-8")
2067
+ print(f"Merged {len(graphs)} graphs → {merged.number_of_nodes()} nodes, {merged.number_of_edges()} edges")
2068
+ print(f"Written to: {out_path}")
2069
+
2070
+ elif cmd == "clone":
2071
+ if len(sys.argv) < 3:
2072
+ print("Usage: graphify clone <github-url> [--branch <branch>] [--out <dir>]", file=sys.stderr)
2073
+ sys.exit(1)
2074
+ url = sys.argv[2]
2075
+ branch: str | None = None
2076
+ out_dir: Path | None = None
2077
+ args = sys.argv[3:]
2078
+ i = 0
2079
+ while i < len(args):
2080
+ if args[i] == "--branch" and i + 1 < len(args):
2081
+ branch = args[i + 1]; i += 2
2082
+ elif args[i] == "--out" and i + 1 < len(args):
2083
+ out_dir = Path(args[i + 1]); i += 2
2084
+ else:
2085
+ i += 1
2086
+ local_path = _clone_repo(url, branch=branch, out_dir=out_dir)
2087
+ print(local_path)
2088
+
2089
+ elif cmd == "export":
2090
+ subcmd = sys.argv[2] if len(sys.argv) > 2 else ""
2091
+ if subcmd not in ("html", "callflow-html", "obsidian", "wiki", "svg", "graphml", "neo4j"):
2092
+ print("Usage: graphify export <format>", file=sys.stderr)
2093
+ print(" html [--graph PATH] [--labels PATH] [--node-limit N] [--no-viz]", file=sys.stderr)
2094
+ print(" callflow-html [GRAPH|DIR] [--graph PATH] [--labels PATH] [--report PATH] [--sections PATH] [--output HTML]", file=sys.stderr)
2095
+ print(" [--lang auto|zh-CN|en] [--max-sections N] [--diagram-scale N]", file=sys.stderr)
2096
+ print(" obsidian [--graph PATH] [--labels PATH] [--dir PATH]", file=sys.stderr)
2097
+ print(" wiki [--graph PATH] [--labels PATH]", file=sys.stderr)
2098
+ print(" svg [--graph PATH] [--labels PATH]", file=sys.stderr)
2099
+ print(" graphml [--graph PATH]", file=sys.stderr)
2100
+ print(" neo4j [--graph PATH] [--push URI] [--user U] [--password P]", file=sys.stderr)
2101
+ print(" (or set NEO4J_PASSWORD instead of --password to keep it off argv)", file=sys.stderr)
2102
+ sys.exit(1)
2103
+
2104
+ # Parse shared args
2105
+ args = sys.argv[3:]
2106
+ graph_path = Path(_GRAPHIFY_OUT) / "graph.json"
2107
+ graph_path_explicit = False
2108
+ labels_path = Path(_GRAPHIFY_OUT) / ".graphify_labels.json"
2109
+ labels_path_explicit = False
2110
+ report_path = Path(_GRAPHIFY_OUT) / "GRAPH_REPORT.md"
2111
+ report_path_explicit = False
2112
+ sections_path: Path | None = None
2113
+ callflow_output: Path | None = None
2114
+ callflow_lang = "auto"
2115
+ callflow_max_sections = 15
2116
+ callflow_diagram_scale = 1.0
2117
+ callflow_max_diagram_nodes = 18
2118
+ callflow_max_diagram_edges = 24
2119
+ analysis_path = Path(_GRAPHIFY_OUT) / ".graphify_analysis.json"
2120
+ node_limit = 5000
2121
+ no_viz = False
2122
+ obsidian_dir = Path(_GRAPHIFY_OUT) / "obsidian"
2123
+ neo4j_uri: str | None = None
2124
+ neo4j_user = "neo4j"
2125
+ # F-031: prefer the NEO4J_PASSWORD env var so the password never
2126
+ # appears on argv (visible in `ps` output / shell history). The
2127
+ # explicit --password flag still overrides it for compatibility.
2128
+ neo4j_password: str | None = os.environ.get("NEO4J_PASSWORD") or None
2129
+ i = 0
2130
+ while i < len(args):
2131
+ a = args[i]
2132
+ if a == "--graph" and i + 1 < len(args):
2133
+ graph_path = Path(args[i + 1])
2134
+ graph_path_explicit = True
2135
+ i += 2
2136
+ elif a == "--labels" and i + 1 < len(args):
2137
+ labels_path = Path(args[i + 1])
2138
+ labels_path_explicit = True
2139
+ i += 2
2140
+ elif a == "--report" and i + 1 < len(args):
2141
+ report_path = Path(args[i + 1])
2142
+ report_path_explicit = True
2143
+ i += 2
2144
+ elif a == "--sections" and i + 1 < len(args):
2145
+ sections_path = Path(args[i + 1]); i += 2
2146
+ elif a == "--output" and i + 1 < len(args):
2147
+ callflow_output = Path(args[i + 1]).expanduser()
2148
+ if not callflow_output.is_absolute():
2149
+ callflow_output = Path.cwd() / callflow_output
2150
+ i += 2
2151
+ elif a == "--lang" and i + 1 < len(args):
2152
+ callflow_lang = args[i + 1]; i += 2
2153
+ elif a == "--max-sections" and i + 1 < len(args):
2154
+ callflow_max_sections = int(args[i + 1]); i += 2
2155
+ elif a == "--diagram-scale" and i + 1 < len(args):
2156
+ callflow_diagram_scale = float(args[i + 1]); i += 2
2157
+ elif a == "--max-diagram-nodes" and i + 1 < len(args):
2158
+ callflow_max_diagram_nodes = int(args[i + 1]); i += 2
2159
+ elif a == "--max-diagram-edges" and i + 1 < len(args):
2160
+ callflow_max_diagram_edges = int(args[i + 1]); i += 2
2161
+ elif a in ("-h", "--help") and subcmd == "callflow-html":
2162
+ print("Usage: graphify export callflow-html [GRAPH|DIR] [--graph PATH] [--labels PATH]")
2163
+ print(" --report PATH path to GRAPH_REPORT.md")
2164
+ print(" --sections PATH JSON section definitions")
2165
+ print(" --output HTML output path (default graphify-out/<project>-callflow.html)")
2166
+ print(" --lang LANG auto, zh-CN, en, etc. (default auto)")
2167
+ print(" --max-sections N maximum auto-derived sections (default 15)")
2168
+ print(" --diagram-scale N Mermaid diagram scale (default 1.0)")
2169
+ print(" --max-diagram-nodes N representative nodes per section (default 18)")
2170
+ print(" --max-diagram-edges N representative edges per section (default 24)")
2171
+ sys.exit(0)
2172
+ elif a == "--node-limit" and i + 1 < len(args):
2173
+ node_limit = int(args[i + 1]); i += 2
2174
+ elif a == "--no-viz":
2175
+ no_viz = True; i += 1
2176
+ elif a == "--dir" and i + 1 < len(args):
2177
+ obsidian_dir = Path(args[i + 1]); i += 2
2178
+ elif a == "--push" and i + 1 < len(args):
2179
+ neo4j_uri = args[i + 1]; i += 2
2180
+ elif a == "--user" and i + 1 < len(args):
2181
+ neo4j_user = args[i + 1]; i += 2
2182
+ elif a == "--password" and i + 1 < len(args):
2183
+ neo4j_password = args[i + 1]; i += 2
2184
+ elif subcmd == "callflow-html" and not a.startswith("-") and not graph_path_explicit:
2185
+ candidate = Path(a)
2186
+ if candidate.name == "graph.json" or candidate.suffix.lower() == ".json":
2187
+ graph_path = candidate
2188
+ elif (candidate / "graph.json").exists():
2189
+ graph_path = candidate / "graph.json"
2190
+ else:
2191
+ graph_path = candidate / _GRAPHIFY_OUT / "graph.json"
2192
+ graph_path_explicit = True
2193
+ i += 1
2194
+ else:
2195
+ i += 1
2196
+
2197
+ graph_path = graph_path.expanduser()
2198
+ if graph_path_explicit:
2199
+ graph_out_dir = graph_path.parent
2200
+ if not labels_path_explicit:
2201
+ labels_path = graph_out_dir / ".graphify_labels.json"
2202
+ if not report_path_explicit:
2203
+ report_path = graph_out_dir / "GRAPH_REPORT.md"
2204
+ labels_path = labels_path.expanduser()
2205
+ report_path = report_path.expanduser()
2206
+
2207
+ if not graph_path.exists():
2208
+ print(f"error: graph not found: {graph_path}. Run /graphify <path> first.", file=sys.stderr)
2209
+ sys.exit(1)
2210
+
2211
+ if subcmd == "callflow-html":
2212
+ from graphify.callflow_html import write_callflow_html as _write_callflow_html
2213
+ out = _write_callflow_html(
2214
+ graph=graph_path,
2215
+ report=report_path,
2216
+ labels=labels_path,
2217
+ sections=sections_path,
2218
+ output=callflow_output,
2219
+ lang=callflow_lang,
2220
+ max_sections=callflow_max_sections,
2221
+ diagram_scale=callflow_diagram_scale,
2222
+ max_diagram_nodes=callflow_max_diagram_nodes,
2223
+ max_diagram_edges=callflow_max_diagram_edges,
2224
+ verbose=True,
2225
+ )
2226
+ print(f"callflow HTML written - open in any browser: {out}")
2227
+ sys.exit(0)
2228
+
2229
+ from networkx.readwrite import json_graph as _jg
2230
+ from graphify.build import build_from_json as _bfj
2231
+
2232
+ _raw = json.loads(graph_path.read_text(encoding="utf-8"))
2233
+ if "links" not in _raw and "edges" in _raw:
2234
+ _raw = dict(_raw, links=_raw["edges"])
2235
+ try:
2236
+ G = _jg.node_link_graph(_raw, edges="links")
2237
+ except TypeError:
2238
+ G = _jg.node_link_graph(_raw)
2239
+
2240
+ # Load optional analysis/labels
2241
+ communities: dict[int, list[str]] = {}
2242
+ if analysis_path.exists():
2243
+ _an = json.loads(analysis_path.read_text(encoding="utf-8"))
2244
+ communities = {int(k): v for k, v in _an.get("communities", {}).items()}
2245
+ cohesion: dict[int, float] = {int(k): v for k, v in _an.get("cohesion", {}).items()}
2246
+ gods_data = _an.get("gods", [])
2247
+ else:
2248
+ cohesion = {}
2249
+ gods_data = []
2250
+
2251
+ labels: dict[int, str] = {}
2252
+ if labels_path.exists():
2253
+ labels = {int(k): v for k, v in json.loads(labels_path.read_text(encoding="utf-8")).items()}
2254
+
2255
+ out_dir = graph_path.parent
2256
+
2257
+ if subcmd == "html":
2258
+ from graphify.export import to_html as _to_html
2259
+ if no_viz:
2260
+ html_target = out_dir / "graph.html"
2261
+ if html_target.exists():
2262
+ html_target.unlink()
2263
+ print("--no-viz: skipped graph.html")
2264
+ else:
2265
+ _to_html(G, communities, str(out_dir / "graph.html"),
2266
+ community_labels=labels or None, node_limit=node_limit)
2267
+ if G.number_of_nodes() <= node_limit:
2268
+ print(f"graph.html written - open in any browser, no server needed")
2269
+
2270
+ elif subcmd == "obsidian":
2271
+ from graphify.export import to_obsidian as _to_obsidian, to_canvas as _to_canvas
2272
+ n = _to_obsidian(G, communities, str(obsidian_dir),
2273
+ community_labels=labels or None, cohesion=cohesion or None)
2274
+ print(f"Obsidian vault: {n} notes in {obsidian_dir}/")
2275
+ _to_canvas(G, communities, str(obsidian_dir / "graph.canvas"),
2276
+ community_labels=labels or None)
2277
+ print(f"Canvas: {obsidian_dir}/graph.canvas")
2278
+ print(f"Open {obsidian_dir}/ as a vault in Obsidian.")
2279
+
2280
+ elif subcmd == "wiki":
2281
+ from graphify.wiki import to_wiki as _to_wiki
2282
+ from graphify.analyze import god_nodes as _god_nodes
2283
+ if not communities:
2284
+ print(
2285
+ "error: .graphify_analysis.json is missing or empty — refusing to export wiki to prevent data loss.\n"
2286
+ "Run `graphify extract .` (or `graphify cluster-only .`) to regenerate community data first.",
2287
+ file=sys.stderr,
2288
+ )
2289
+ sys.exit(1)
2290
+ if not gods_data:
2291
+ gods_data = _god_nodes(G)
2292
+ n = _to_wiki(G, communities, str(out_dir / "wiki"),
2293
+ community_labels=labels or None, cohesion=cohesion or None,
2294
+ god_nodes_data=gods_data)
2295
+ print(f"Wiki: {n} articles written to {out_dir}/wiki/")
2296
+ print(f" {out_dir}/wiki/index.md -> agent entry point")
2297
+
2298
+ elif subcmd == "svg":
2299
+ from graphify.export import to_svg as _to_svg
2300
+ _to_svg(G, communities, str(out_dir / "graph.svg"),
2301
+ community_labels=labels or None)
2302
+ print(f"graph.svg written - embeds in Obsidian, Notion, GitHub READMEs")
2303
+
2304
+ elif subcmd == "graphml":
2305
+ from graphify.export import to_graphml as _to_graphml
2306
+ _to_graphml(G, communities, str(out_dir / "graph.graphml"))
2307
+ print(f"graph.graphml written - open in Gephi, yEd, or any GraphML tool")
2308
+
2309
+ elif subcmd == "neo4j":
2310
+ if neo4j_uri:
2311
+ from graphify.export import push_to_neo4j as _push
2312
+ if neo4j_password is None:
2313
+ print("error: --password required for --push", file=sys.stderr)
2314
+ sys.exit(1)
2315
+ result = _push(G, uri=neo4j_uri, user=neo4j_user,
2316
+ password=neo4j_password, communities=communities)
2317
+ print(f"Pushed to Neo4j: {result['nodes']} nodes, {result['edges']} edges")
2318
+ else:
2319
+ from graphify.export import to_cypher as _to_cypher
2320
+ _to_cypher(G, str(out_dir / "cypher.txt"))
2321
+ print(f"cypher.txt written - import with: cypher-shell < {out_dir}/cypher.txt")
2322
+
2323
+ elif cmd == "benchmark":
2324
+ from graphify.benchmark import run_benchmark, print_benchmark
2325
+ graph_path = sys.argv[2] if len(sys.argv) > 2 else "graphify-out/graph.json"
2326
+ # Try to load corpus_words from detect output
2327
+ corpus_words = None
2328
+ detect_path = Path(".graphify_detect.json")
2329
+ if detect_path.exists():
2330
+ try:
2331
+ detect_data = json.loads(detect_path.read_text(encoding="utf-8"))
2332
+ corpus_words = detect_data.get("total_words")
2333
+ except Exception:
2334
+ pass
2335
+ result = run_benchmark(graph_path, corpus_words=corpus_words)
2336
+ print_benchmark(result)
2337
+
2338
+ elif cmd == "global":
2339
+ subcmd = sys.argv[2] if len(sys.argv) > 2 else ""
2340
+ from graphify.global_graph import (
2341
+ global_add as _global_add,
2342
+ global_remove as _global_remove,
2343
+ global_list as _global_list,
2344
+ global_path as _global_path,
2345
+ )
2346
+ if subcmd == "add":
2347
+ # graphify global add <graph.json> [--as <tag>]
2348
+ args = sys.argv[3:]
2349
+ source = None
2350
+ tag = None
2351
+ i = 0
2352
+ while i < len(args):
2353
+ if args[i] == "--as" and i + 1 < len(args):
2354
+ tag = args[i + 1]; i += 2
2355
+ elif not source:
2356
+ source = Path(args[i]); i += 1
2357
+ else:
2358
+ i += 1
2359
+ if not source:
2360
+ print("Usage: graphify global add <graph.json> [--as <repo-tag>]", file=sys.stderr)
2361
+ sys.exit(1)
2362
+ tag = tag or source.parent.parent.name
2363
+ try:
2364
+ result = _global_add(source, tag)
2365
+ if result["skipped"]:
2366
+ print(f"'{tag}' unchanged since last add — global graph not modified.")
2367
+ else:
2368
+ print(f"Added '{tag}' to global graph: +{result['nodes_added']} nodes, "
2369
+ f"-{result['nodes_removed']} pruned. Global: {_global_path()}")
2370
+ except Exception as exc:
2371
+ print(f"error: {exc}", file=sys.stderr); sys.exit(1)
2372
+ elif subcmd == "remove":
2373
+ tag = sys.argv[3] if len(sys.argv) > 3 else ""
2374
+ if not tag:
2375
+ print("Usage: graphify global remove <repo-tag>", file=sys.stderr); sys.exit(1)
2376
+ try:
2377
+ removed = _global_remove(tag)
2378
+ print(f"Removed '{tag}' from global graph ({removed} nodes pruned).")
2379
+ except KeyError as exc:
2380
+ print(f"error: {exc}", file=sys.stderr); sys.exit(1)
2381
+ elif subcmd == "list":
2382
+ repos = _global_list()
2383
+ if not repos:
2384
+ print("Global graph is empty. Use 'graphify global add' to add a project.")
2385
+ else:
2386
+ print(f"Global graph: {_global_path()}")
2387
+ for tag, info in repos.items():
2388
+ print(f" {tag}: {info.get('node_count', '?')} nodes, added {info.get('added_at', '?')[:10]}")
2389
+ elif subcmd == "path":
2390
+ print(_global_path())
2391
+ else:
2392
+ print("Usage: graphify global [add|remove|list|path]", file=sys.stderr); sys.exit(1)
2393
+
2394
+ elif cmd == "extract":
2395
+ # Headless full-pipeline extraction for CI / scripts (#698).
2396
+ # Runs detect -> AST extraction on code -> semantic LLM extraction on
2397
+ # docs/papers/images -> merge -> build -> cluster -> write outputs.
2398
+ # Unlike the skill.md path (which runs through Claude Code subagents),
2399
+ # this calls extract_corpus_parallel directly using whichever backend
2400
+ # has an API key set.
2401
+ if len(sys.argv) < 3:
2402
+ print(
2403
+ "Usage: graphify extract <path> [--backend gemini|kimi|claude|openai|deepseek|ollama] "
2404
+ "[--model M] [--out DIR] [--google-workspace] [--no-cluster] "
2405
+ "[--max-workers N] [--token-budget N] [--max-concurrency N] "
2406
+ "[--api-timeout S]",
2407
+ file=sys.stderr,
2408
+ )
2409
+ sys.exit(1)
2410
+
2411
+ target = Path(sys.argv[2]).resolve()
2412
+ if not target.exists():
2413
+ print(f"error: path not found: {target}", file=sys.stderr)
2414
+ sys.exit(1)
2415
+
2416
+ backend: str | None = None
2417
+ model: str | None = None
2418
+ out_dir: Path | None = None
2419
+ no_cluster = False
2420
+ dedup_llm = False
2421
+ google_workspace = False
2422
+ global_merge = False
2423
+ global_repo_tag: str | None = None
2424
+ # Performance/tuning knobs (issue #792). None means "use library default".
2425
+ cli_max_workers: int | None = None
2426
+ cli_token_budget: int | None = None
2427
+ cli_max_concurrency: int | None = None
2428
+ cli_api_timeout: float | None = None
2429
+ # Clustering tuning knobs
2430
+ cli_resolution: float = 1.0
2431
+ cli_exclude_hubs: float | None = None
2432
+
2433
+ def _parse_int(name: str, raw: str) -> int:
2434
+ try:
2435
+ v = int(raw)
2436
+ except ValueError:
2437
+ print(f"error: {name} must be a positive integer (got {raw!r})", file=sys.stderr)
2438
+ sys.exit(2)
2439
+ if v <= 0:
2440
+ print(f"error: {name} must be > 0 (got {v})", file=sys.stderr)
2441
+ sys.exit(2)
2442
+ return v
2443
+
2444
+ def _parse_float(name: str, raw: str) -> float:
2445
+ try:
2446
+ v = float(raw)
2447
+ except ValueError:
2448
+ print(f"error: {name} must be a positive number (got {raw!r})", file=sys.stderr)
2449
+ sys.exit(2)
2450
+ if v <= 0:
2451
+ print(f"error: {name} must be > 0 (got {v})", file=sys.stderr)
2452
+ sys.exit(2)
2453
+ return v
2454
+
2455
+ args = sys.argv[3:]
2456
+ i = 0
2457
+ while i < len(args):
2458
+ a = args[i]
2459
+ if a == "--backend" and i + 1 < len(args):
2460
+ backend = args[i + 1]; i += 2
2461
+ elif a.startswith("--backend="):
2462
+ backend = a.split("=", 1)[1]; i += 1
2463
+ elif a == "--model" and i + 1 < len(args):
2464
+ model = args[i + 1]; i += 2
2465
+ elif a.startswith("--model="):
2466
+ model = a.split("=", 1)[1]; i += 1
2467
+ elif a == "--out" and i + 1 < len(args):
2468
+ out_dir = Path(args[i + 1]); i += 2
2469
+ elif a.startswith("--out="):
2470
+ out_dir = Path(a.split("=", 1)[1]); i += 1
2471
+ elif a == "--no-cluster":
2472
+ no_cluster = True; i += 1
2473
+ elif a == "--dedup-llm":
2474
+ dedup_llm = True; i += 1
2475
+ elif a == "--google-workspace":
2476
+ google_workspace = True; i += 1
2477
+ elif a == "--global":
2478
+ global_merge = True; i += 1
2479
+ elif a == "--as" and i + 1 < len(args):
2480
+ global_repo_tag = args[i + 1]; i += 2
2481
+ elif a == "--max-workers" and i + 1 < len(args):
2482
+ cli_max_workers = _parse_int("--max-workers", args[i + 1]); i += 2
2483
+ elif a.startswith("--max-workers="):
2484
+ cli_max_workers = _parse_int("--max-workers", a.split("=", 1)[1]); i += 1
2485
+ elif a == "--token-budget" and i + 1 < len(args):
2486
+ cli_token_budget = _parse_int("--token-budget", args[i + 1]); i += 2
2487
+ elif a.startswith("--token-budget="):
2488
+ cli_token_budget = _parse_int("--token-budget", a.split("=", 1)[1]); i += 1
2489
+ elif a == "--max-concurrency" and i + 1 < len(args):
2490
+ cli_max_concurrency = _parse_int("--max-concurrency", args[i + 1]); i += 2
2491
+ elif a.startswith("--max-concurrency="):
2492
+ cli_max_concurrency = _parse_int("--max-concurrency", a.split("=", 1)[1]); i += 1
2493
+ elif a == "--api-timeout" and i + 1 < len(args):
2494
+ cli_api_timeout = _parse_float("--api-timeout", args[i + 1]); i += 2
2495
+ elif a.startswith("--api-timeout="):
2496
+ cli_api_timeout = _parse_float("--api-timeout", a.split("=", 1)[1]); i += 1
2497
+ elif a == "--resolution" and i + 1 < len(args):
2498
+ cli_resolution = _parse_float("--resolution", args[i + 1]); i += 2
2499
+ elif a.startswith("--resolution="):
2500
+ cli_resolution = _parse_float("--resolution", a.split("=", 1)[1]); i += 1
2501
+ elif a == "--exclude-hubs" and i + 1 < len(args):
2502
+ cli_exclude_hubs = float(args[i + 1]); i += 2
2503
+ elif a.startswith("--exclude-hubs="):
2504
+ cli_exclude_hubs = float(a.split("=", 1)[1]); i += 1
2505
+ else:
2506
+ i += 1
2507
+
2508
+ # CLI flag wins over env var. Setting GRAPHIFY_API_TIMEOUT here so
2509
+ # _call_openai_compat picks it up without needing a new kwarg path.
2510
+ if cli_api_timeout is not None:
2511
+ os.environ["GRAPHIFY_API_TIMEOUT"] = str(cli_api_timeout)
2512
+ if cli_max_workers is not None:
2513
+ os.environ["GRAPHIFY_MAX_WORKERS"] = str(cli_max_workers)
2514
+
2515
+ # Backend resolution. If user did not pass --backend, sniff env.
2516
+ # If backend was explicitly requested, validate its key is present
2517
+ # and surface a clear error early — don't let extract_corpus_parallel
2518
+ # raise mid-run after we've spent time on AST extraction.
2519
+ from graphify.llm import (
2520
+ BACKENDS as _BACKENDS,
2521
+ detect_backend as _detect_backend,
2522
+ estimate_cost as _estimate_cost,
2523
+ extract_corpus_parallel as _extract_corpus_parallel,
2524
+ _format_backend_env_keys,
2525
+ _get_backend_api_key,
2526
+ )
2527
+ if backend is None:
2528
+ backend = _detect_backend()
2529
+ if backend is None:
2530
+ print(
2531
+ "error: no LLM API key found. Set GEMINI_API_KEY or GOOGLE_API_KEY "
2532
+ "(gemini), MOONSHOT_API_KEY (kimi), ANTHROPIC_API_KEY (claude), "
2533
+ "OPENAI_API_KEY (openai), DEEPSEEK_API_KEY (deepseek), "
2534
+ "or pass --backend.",
2535
+ file=sys.stderr,
2536
+ )
2537
+ sys.exit(1)
2538
+ if backend not in _BACKENDS:
2539
+ print(
2540
+ f"error: unknown backend '{backend}'. "
2541
+ f"Available: {', '.join(sorted(_BACKENDS))}",
2542
+ file=sys.stderr,
2543
+ )
2544
+ sys.exit(1)
2545
+ if not _get_backend_api_key(backend):
2546
+ # Ollama on a loopback URL ignores auth entirely; don't block
2547
+ # the run just because OLLAMA_API_KEY is unset (issue #792).
2548
+ # extract_files_direct already prints a warning and substitutes
2549
+ # a placeholder key in that case.
2550
+ allow_no_key = False
2551
+ if backend == "ollama":
2552
+ from urllib.parse import urlparse
2553
+ ollama_url = os.environ.get(
2554
+ "OLLAMA_BASE_URL",
2555
+ _BACKENDS["ollama"].get("base_url", ""),
2556
+ )
2557
+ try:
2558
+ host = (urlparse(ollama_url).hostname or "").lower()
2559
+ except Exception:
2560
+ host = ""
2561
+ allow_no_key = (
2562
+ host in ("localhost", "127.0.0.1", "::1")
2563
+ or host.startswith("127.")
2564
+ )
2565
+ elif backend == "bedrock":
2566
+ allow_no_key = bool(
2567
+ os.environ.get("AWS_PROFILE")
2568
+ or os.environ.get("AWS_REGION")
2569
+ or os.environ.get("AWS_DEFAULT_REGION")
2570
+ or os.environ.get("AWS_ACCESS_KEY_ID")
2571
+ )
2572
+ elif backend == "claude-cli":
2573
+ import shutil as _shutil
2574
+ allow_no_key = _shutil.which("claude") is not None
2575
+ if not allow_no_key:
2576
+ print(
2577
+ "error: backend 'claude-cli' requires the `claude` CLI on $PATH "
2578
+ "(install Claude Code and run `claude` once to authenticate).",
2579
+ file=sys.stderr,
2580
+ )
2581
+ sys.exit(1)
2582
+ if not allow_no_key:
2583
+ print(
2584
+ f"error: backend '{backend}' requires {_format_backend_env_keys(backend)} to be set.",
2585
+ file=sys.stderr,
2586
+ )
2587
+ sys.exit(1)
2588
+
2589
+ # Resolve output dir. The user-facing contract is "<out>/graphify-out/"
2590
+ # so a fresh checkout writes graphify-out/ at the project root, matching
2591
+ # the skill.md pipeline.
2592
+ out_root = (out_dir.resolve() if out_dir else target)
2593
+ graphify_out = out_root / "graphify-out"
2594
+ graphify_out.mkdir(parents=True, exist_ok=True)
2595
+
2596
+ from graphify.detect import (
2597
+ detect as _detect,
2598
+ detect_incremental as _detect_incremental,
2599
+ save_manifest as _save_manifest,
2600
+ )
2601
+ manifest_path = graphify_out / "manifest.json"
2602
+ existing_graph_path = graphify_out / "graph.json"
2603
+ incremental_mode = manifest_path.exists() and existing_graph_path.exists()
2604
+
2605
+ if incremental_mode:
2606
+ print(f"[graphify extract] incremental scan of {target}")
2607
+ detection = _detect_incremental(
2608
+ target,
2609
+ manifest_path=str(manifest_path),
2610
+ google_workspace=google_workspace or None,
2611
+ )
2612
+ else:
2613
+ print(f"[graphify extract] scanning {target}")
2614
+ detection = _detect(target, google_workspace=google_workspace or None)
2615
+
2616
+ files_by_type = detection.get("files", {})
2617
+ if incremental_mode:
2618
+ new_by_type = detection.get("new_files", {})
2619
+ code_files = [Path(p) for p in new_by_type.get("code", [])]
2620
+ doc_files = [Path(p) for p in new_by_type.get("document", [])]
2621
+ paper_files = [Path(p) for p in new_by_type.get("paper", [])]
2622
+ image_files = [Path(p) for p in new_by_type.get("image", [])]
2623
+ deleted_files = list(detection.get("deleted_files", []))
2624
+ unchanged_total = sum(len(v) for v in detection.get("unchanged_files", {}).values())
2625
+ else:
2626
+ code_files = [Path(p) for p in files_by_type.get("code", [])]
2627
+ doc_files = [Path(p) for p in files_by_type.get("document", [])]
2628
+ paper_files = [Path(p) for p in files_by_type.get("paper", [])]
2629
+ image_files = [Path(p) for p in files_by_type.get("image", [])]
2630
+ deleted_files = []
2631
+ unchanged_total = 0
2632
+
2633
+ semantic_files = doc_files + paper_files + image_files
2634
+ if incremental_mode:
2635
+ print(
2636
+ f"[graphify extract] {len(code_files)} code, {len(doc_files)} docs, "
2637
+ f"{len(paper_files)} papers, {len(image_files)} images changed; "
2638
+ f"{unchanged_total} unchanged; {len(deleted_files)} deleted"
2639
+ )
2640
+ else:
2641
+ print(
2642
+ f"[graphify extract] found {len(code_files)} code, "
2643
+ f"{len(doc_files)} docs, {len(paper_files)} papers, "
2644
+ f"{len(image_files)} images"
2645
+ )
2646
+
2647
+ # AST extraction on code files. Empty code list (docs-only corpus) is
2648
+ # the issue #698 case — skip cleanly instead of crashing inside extract().
2649
+ ast_result: dict = {"nodes": [], "edges": [], "input_tokens": 0, "output_tokens": 0}
2650
+ if code_files:
2651
+ from graphify.extract import extract as _ast_extract
2652
+ ast_kwargs: dict = {"cache_root": target}
2653
+ if cli_max_workers is not None:
2654
+ ast_kwargs["max_workers"] = cli_max_workers
2655
+ print(f"[graphify extract] AST extraction on {len(code_files)} code files...")
2656
+ try:
2657
+ ast_result = _ast_extract(code_files, **ast_kwargs)
2658
+ except Exception as exc:
2659
+ print(f"[graphify extract] AST extraction failed: {exc}", file=sys.stderr)
2660
+ ast_result = {"nodes": [], "edges": [], "input_tokens": 0, "output_tokens": 0}
2661
+
2662
+ # Semantic extraction on docs/papers/images. Check cache first.
2663
+ from graphify.cache import (
2664
+ check_semantic_cache as _check_semantic_cache,
2665
+ save_semantic_cache as _save_semantic_cache,
2666
+ )
2667
+ sem_result: dict = {
2668
+ "nodes": [], "edges": [], "hyperedges": [],
2669
+ "input_tokens": 0, "output_tokens": 0,
2670
+ }
2671
+ sem_cache_hits = 0
2672
+ sem_cache_misses = 0
2673
+ if semantic_files:
2674
+ sem_paths_str = [str(p) for p in semantic_files]
2675
+ cached_nodes, cached_edges, cached_hyperedges, uncached_paths = (
2676
+ _check_semantic_cache(sem_paths_str, root=target)
2677
+ )
2678
+ sem_cache_hits = len(semantic_files) - len(uncached_paths)
2679
+ sem_cache_misses = len(uncached_paths)
2680
+ sem_result["nodes"].extend(cached_nodes)
2681
+ sem_result["edges"].extend(cached_edges)
2682
+ sem_result["hyperedges"].extend(cached_hyperedges)
2683
+ if sem_cache_hits:
2684
+ print(f"[graphify extract] semantic cache: {sem_cache_hits} hit / {sem_cache_misses} miss")
2685
+
2686
+ if uncached_paths:
2687
+ print(f"[graphify extract] semantic extraction on {len(uncached_paths)} files via {backend}...")
2688
+ corpus_kwargs: dict = {
2689
+ "backend": backend,
2690
+ "model": model,
2691
+ "root": target,
2692
+ }
2693
+ if cli_token_budget is not None:
2694
+ corpus_kwargs["token_budget"] = cli_token_budget
2695
+ if cli_max_concurrency is not None:
2696
+ corpus_kwargs["max_concurrency"] = cli_max_concurrency
2697
+
2698
+ # Minimal progress callback so the CLI is no longer silent
2699
+ # during long local-inference runs (issue #792 addendum).
2700
+ _total_chunks = {"n": 0}
2701
+ def _progress(idx: int, total: int, _result: dict) -> None:
2702
+ _total_chunks["n"] = total
2703
+ print(
2704
+ f"[graphify extract] chunk {idx + 1}/{total} done",
2705
+ flush=True,
2706
+ )
2707
+ corpus_kwargs["on_chunk_done"] = _progress
2708
+
2709
+ try:
2710
+ fresh = _extract_corpus_parallel(
2711
+ [Path(p) for p in uncached_paths],
2712
+ **corpus_kwargs,
2713
+ )
2714
+ except ImportError as exc:
2715
+ print(f"error: {exc}", file=sys.stderr)
2716
+ sys.exit(1)
2717
+ except Exception as exc:
2718
+ print(
2719
+ f"[graphify extract] semantic extraction failed: {exc}",
2720
+ file=sys.stderr,
2721
+ )
2722
+ fresh = {"nodes": [], "edges": [], "hyperedges": [], "input_tokens": 0, "output_tokens": 0}
2723
+ try:
2724
+ _save_semantic_cache(
2725
+ fresh.get("nodes", []),
2726
+ fresh.get("edges", []),
2727
+ fresh.get("hyperedges", []),
2728
+ root=target,
2729
+ )
2730
+ except Exception as exc:
2731
+ print(f"[graphify extract] warning: could not write semantic cache: {exc}", file=sys.stderr)
2732
+ sem_result["nodes"].extend(fresh.get("nodes", []))
2733
+ sem_result["edges"].extend(fresh.get("edges", []))
2734
+ sem_result["hyperedges"].extend(fresh.get("hyperedges", []))
2735
+ sem_result["input_tokens"] += fresh.get("input_tokens", 0)
2736
+ sem_result["output_tokens"] += fresh.get("output_tokens", 0)
2737
+
2738
+ # Merge AST + semantic. Order matters for deduplication: passing AST
2739
+ # first means semantic node attributes win on collision (richer labels
2740
+ # for symbols also referenced in docs). Hyperedges only come from the
2741
+ # semantic side.
2742
+ merged: dict = {
2743
+ "nodes": list(ast_result.get("nodes", [])) + list(sem_result.get("nodes", [])),
2744
+ "edges": list(ast_result.get("edges", [])) + list(sem_result.get("edges", [])),
2745
+ "hyperedges": list(sem_result.get("hyperedges", [])),
2746
+ "input_tokens": ast_result.get("input_tokens", 0) + sem_result.get("input_tokens", 0),
2747
+ "output_tokens": ast_result.get("output_tokens", 0) + sem_result.get("output_tokens", 0),
2748
+ }
2749
+
2750
+ graph_json_path = graphify_out / "graph.json"
2751
+ analysis_path = graphify_out / ".graphify_analysis.json"
2752
+
2753
+ if no_cluster:
2754
+ # --no-cluster: dump the raw merged extraction as graph.json.
2755
+ # No NetworkX, no community detection, no analysis sidecar.
2756
+ graph_json_path.write_text(
2757
+ json.dumps(merged, indent=2), encoding="utf-8"
2758
+ )
2759
+ cost = _estimate_cost(
2760
+ backend, merged["input_tokens"], merged["output_tokens"]
2761
+ )
2762
+ print(
2763
+ f"[graphify extract] wrote {graph_json_path} — "
2764
+ f"{len(merged['nodes'])} nodes, {len(merged['edges'])} edges "
2765
+ f"(no clustering)"
2766
+ )
2767
+ if merged["input_tokens"] or merged["output_tokens"]:
2768
+ print(
2769
+ f"[graphify extract] tokens: "
2770
+ f"{merged['input_tokens']:,} in / "
2771
+ f"{merged['output_tokens']:,} out, "
2772
+ f"est. cost: ${cost:.4f}"
2773
+ )
2774
+ try:
2775
+ _save_manifest(files_by_type, manifest_path=str(manifest_path), kind="both")
2776
+ except Exception as exc:
2777
+ print(f"[graphify extract] warning: could not write manifest: {exc}", file=sys.stderr)
2778
+ if global_merge:
2779
+ from graphify.global_graph import global_add as _global_add
2780
+ _tag = global_repo_tag or target.name
2781
+ try:
2782
+ result = _global_add(graphify_out / "graph.json", _tag)
2783
+ if result["skipped"]:
2784
+ print(f"[graphify global] '{_tag}' unchanged since last add — skipped.")
2785
+ else:
2786
+ print(f"[graphify global] '{_tag}' merged into global graph "
2787
+ f"(+{result['nodes_added']} nodes, -{result['nodes_removed']} pruned).")
2788
+ except Exception as exc:
2789
+ print(f"[graphify global] warning: failed to merge into global graph: {exc}", file=sys.stderr)
2790
+ sys.exit(0)
2791
+
2792
+ # Build graph + cluster + score + write.
2793
+ from graphify.build import (
2794
+ build as _build,
2795
+ build_from_json as _build_from_json,
2796
+ build_merge as _build_merge,
2797
+ )
2798
+ from graphify.cluster import cluster as _cluster, score_all as _score_all
2799
+ from graphify.export import to_json as _to_json
2800
+ from graphify.analyze import god_nodes as _god_nodes, surprising_connections as _surprising
2801
+ dedup_backend = backend if dedup_llm else None
2802
+ if incremental_mode:
2803
+ G = _build_merge(
2804
+ [merged],
2805
+ graph_path=existing_graph_path,
2806
+ prune_sources=deleted_files or None,
2807
+ dedup=True,
2808
+ dedup_llm_backend=dedup_backend,
2809
+ )
2810
+ else:
2811
+ G = _build([merged], dedup=True, dedup_llm_backend=dedup_backend)
2812
+ if G.number_of_nodes() == 0:
2813
+ print(
2814
+ "[graphify extract] graph is empty — extraction produced no nodes. "
2815
+ "Possible causes: all files skipped, binary-only corpus, or LLM "
2816
+ "returned no edges.",
2817
+ file=sys.stderr,
2818
+ )
2819
+ sys.exit(1)
2820
+
2821
+ communities = _cluster(G, resolution=cli_resolution, exclude_hubs_percentile=cli_exclude_hubs)
2822
+ cohesion = _score_all(G, communities)
2823
+ try:
2824
+ gods = _god_nodes(G)
2825
+ except Exception:
2826
+ gods = []
2827
+ try:
2828
+ surprises = _surprising(G, communities)
2829
+ except Exception:
2830
+ surprises = []
2831
+
2832
+ _to_json(G, communities, str(graph_json_path), force=True)
2833
+ if global_merge:
2834
+ from graphify.global_graph import global_add as _global_add
2835
+ _tag = global_repo_tag or target.name
2836
+ try:
2837
+ result = _global_add(graphify_out / "graph.json", _tag)
2838
+ if result["skipped"]:
2839
+ print(f"[graphify global] '{_tag}' unchanged since last add — skipped.")
2840
+ else:
2841
+ print(f"[graphify global] '{_tag}' merged into global graph "
2842
+ f"(+{result['nodes_added']} nodes, -{result['nodes_removed']} pruned).")
2843
+ except Exception as exc:
2844
+ print(f"[graphify global] warning: failed to merge into global graph: {exc}", file=sys.stderr)
2845
+ analysis = {
2846
+ "communities": {str(k): v for k, v in communities.items()},
2847
+ "cohesion": {str(k): v for k, v in cohesion.items()},
2848
+ "gods": gods,
2849
+ "surprises": surprises,
2850
+ "tokens": {
2851
+ "input": merged["input_tokens"],
2852
+ "output": merged["output_tokens"],
2853
+ },
2854
+ }
2855
+ analysis_path.write_text(json.dumps(analysis, indent=2), encoding="utf-8")
2856
+ try:
2857
+ _save_manifest(files_by_type, manifest_path=str(manifest_path), kind="both")
2858
+ except Exception as exc:
2859
+ print(f"[graphify extract] warning: could not write manifest: {exc}", file=sys.stderr)
2860
+
2861
+ cost = _estimate_cost(backend, merged["input_tokens"], merged["output_tokens"])
2862
+ print(
2863
+ f"[graphify extract] wrote {graph_json_path}: "
2864
+ f"{G.number_of_nodes()} nodes, {G.number_of_edges()} edges, "
2865
+ f"{len(communities)} communities"
2866
+ )
2867
+ print(f"[graphify extract] wrote {analysis_path}")
2868
+ if incremental_mode:
2869
+ print(
2870
+ f"[graphify extract] incremental summary: "
2871
+ f"{sem_cache_hits + unchanged_total} files cached/unchanged, "
2872
+ f"{len(code_files) + sem_cache_misses} re-extracted, "
2873
+ f"{len(deleted_files)} deleted"
2874
+ )
2875
+ elif sem_cache_hits:
2876
+ print(f"[graphify extract] semantic cache: {sem_cache_hits} cached, {sem_cache_misses} re-extracted")
2877
+ if merged["input_tokens"] or merged["output_tokens"]:
2878
+ print(
2879
+ f"[graphify extract] tokens: "
2880
+ f"{merged['input_tokens']:,} in / "
2881
+ f"{merged['output_tokens']:,} out, "
2882
+ f"est. cost (~{backend}): ${cost:.4f}"
2883
+ )
2884
+
2885
+ elif Path(cmd).exists() or cmd in (".", "..") or cmd.startswith(("./", "../", "/", "~")):
2886
+ # User ran `graphify <path>` directly — treat as `graphify extract <path>`.
2887
+ # Common when following the PowerShell note in README (`graphify .`) or
2888
+ # copy-pasting skill invocations without the leading slash.
2889
+ sys.argv.insert(2, sys.argv[1])
2890
+ sys.argv[1] = "extract"
2891
+ main()
2892
+ else:
2893
+ print(f"error: unknown command '{cmd}'", file=sys.stderr)
2894
+ print("Run 'graphify --help' for usage.", file=sys.stderr)
2895
+ sys.exit(1)
2896
+
2897
+
2898
+ if __name__ == "__main__":
2899
+ main()