diff --git a/README.md b/README.md index a83a8d9a..33a34dbd 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,18 @@ pip install graphifyy && graphify install > The PyPI package is temporarily named `graphifyy` while the `graphify` name is being reclaimed. The CLI and skill command are still `graphify`. -Then open Claude Code in any directory and type: +### Platform support + +| Platform | Install command | +|----------|----------------| +| Claude Code | `graphify install` | +| Codex | `graphify install --platform codex` | +| OpenCode | `graphify install --platform opencode` | +| OpenClaw | `graphify install --platform claw` | + +Codex users also need `multi_agent = true` under `[features]` in `~/.codex/config.toml` for parallel extraction. OpenClaw uses sequential extraction (parallel agent support is still early on that platform). + +Then open your AI coding assistant and type: ``` /graphify . @@ -131,7 +142,7 @@ Works with any mix of file types: **Hyperedges** - group relationships connecting 3+ nodes that pairwise edges can't express. All classes implementing a shared protocol, all functions in an auth flow, all concepts from a paper section forming one idea. -**Token benchmark** - printed automatically after every run. On a mixed corpus (Karpathy repos + papers + images): **71.5x** fewer tokens per query vs reading raw files. +**Token benchmark** - printed automatically after every run. On a mixed corpus (Karpathy repos + papers + images): **71.5x** fewer tokens per query vs reading raw files. The first run extracts and builds the graph (this costs tokens). Every subsequent query reads the compact graph instead of raw files — that's where the savings compound. The SHA256 cache means re-runs only re-process changed files. **Auto-sync** (`--watch`) - run in a background terminal and the graph updates itself as your codebase changes. Code file saves trigger an instant rebuild (AST only, no LLM). Doc/image changes notify you to run `--update` for the LLM re-pass. diff --git a/docs/superpowers/plans/2026-04-06-v3-platform-compatibility.md b/docs/superpowers/plans/2026-04-06-v3-platform-compatibility.md new file mode 100644 index 00000000..2dc86fd5 --- /dev/null +++ b/docs/superpowers/plans/2026-04-06-v3-platform-compatibility.md @@ -0,0 +1,581 @@ +# v3 Platform Compatibility Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add Codex, OpenCode, and OpenClaw platform support via platform-specific skill files and a `graphify install --platform X` flag. + +**Architecture:** The only section that differs between platforms is Step B2 (semantic extraction subagent dispatch) in skill.md. Three new skill files are created — one per platform — each identical to skill.md except for that one section. The `install()` function in `__main__.py` gains a `--platform` flag that copies the right skill file to the right config directory. + +**Tech Stack:** Python 3.10+, pathlib, shutil, argparse (no new deps) + +--- + +## File Map + +| File | Action | Purpose | +|------|--------|---------| +| `graphify/skill.md` | Read-only | Source of truth — unchanged | +| `graphify/skill-codex.md` | Create | Codex variant (spawn_agent + wait) | +| `graphify/skill-opencode.md` | Create | OpenCode variant (@mention dispatch) | +| `graphify/skill-claw.md` | Create | OpenClaw variant (sequential extraction) | +| `graphify/__main__.py` | Modify | Add --platform flag to install() and main() | +| `pyproject.toml` | Modify | Add 3 new skill files to package-data | +| `tests/test_install.py` | Create | Platform routing tests | +| `README.md` | Modify | Platform table + token efficiency clarification | + +--- + +## Task 1: Create the v3 branch + +**Files:** none (git only) + +- [ ] **Step 1: Create and switch to v3 branch** + +```bash +cd /home/safi/graphify +git checkout -b v3 +``` + +Expected: `Switched to a new branch 'v3'` + +- [ ] **Step 2: Verify branch** + +```bash +git branch --show-current +``` + +Expected: `v3` + +--- + +## Task 2: Create `skill-codex.md` + +skill-codex.md is identical to skill.md with one change: Step B2 replaces `Agent` tool calls with `spawn_agent` + `wait` + `close_agent` calls. + +**Files:** +- Create: `graphify/skill-codex.md` + +- [ ] **Step 1: Copy skill.md as the base** + +```bash +cp graphify/skill.md graphify/skill-codex.md +``` + +- [ ] **Step 2: Open `graphify/skill-codex.md` and replace the Step B2 section** + +Find this block (starts at "**Step B2 - Dispatch ALL subagents in a single message**", ends before "**Step B3**"): + +Replace the entire Step B2 section with: + +```markdown +**Step B2 - Dispatch ALL subagents in a single message (Codex)** + +> **Codex platform:** This step uses `spawn_agent` + `wait` + `close_agent` instead of the Agent tool. +> Requires `multi_agent = true` in `~/.codex/config.toml`. If you get an error about multi-agent support, ask the user to add that config line and restart Codex. + +Call `spawn_agent` once per chunk — all in the same response so they run in parallel: + +``` +spawn_agent(agent_type="worker", message="Your task is to perform the following. Follow the instructions below exactly.\n\n\nYou are a graphify extraction subagent. Read the files listed and extract a knowledge graph fragment.\nOutput ONLY valid JSON matching the schema below - no explanation, no markdown fences, no preamble.\n\nFiles (chunk CHUNK_NUM of TOTAL_CHUNKS):\nFILE_LIST\n\n[copy the extraction rules and JSON schema verbatim from the existing Step B2 content — it's already in the file from the cp step]\n\n\nExecute this now. Output ONLY the structured JSON response.") +``` + +Collect all handles. Then for each handle: +``` +result = wait(handle) +close_agent(handle) +``` + +Parse each result as JSON. Accumulate nodes/edges/hyperedges across all results into `.graphify_semantic_new.json`. + +If `spawn_agent` is not available, tell the user: "Codex multi-agent support is not enabled. Add `multi_agent = true` under `[features]` in `~/.codex/config.toml` and restart Codex." +``` + +- [ ] **Step 3: Verify the file looks correct** + +```bash +grep -n "spawn_agent\|Step B2\|Step B3" graphify/skill-codex.md | head -20 +``` + +Expected: lines showing spawn_agent in B2 and Step B3 after it. + +- [ ] **Step 4: Commit** + +```bash +git add graphify/skill-codex.md +git commit -m "add skill-codex.md for Codex platform (spawn_agent parallel extraction)" +``` + +--- + +## Task 3: Create `skill-opencode.md` + +**Files:** +- Create: `graphify/skill-opencode.md` + +- [ ] **Step 1: Copy skill.md as the base** + +```bash +cp graphify/skill.md graphify/skill-opencode.md +``` + +- [ ] **Step 2: Open `graphify/skill-opencode.md` and replace the Step B2 section** + +Replace the entire Step B2 section with: + +```markdown +**Step B2 - Dispatch ALL subagents in a single message (OpenCode)** + +> **OpenCode platform:** This step uses OpenCode's `@mention` dispatch instead of the Agent tool. + +Dispatch all chunks in a single response. Each `@mention` runs in parallel: + +``` +@agent Chunk CHUNK_NUM of TOTAL_CHUNKS: You are a graphify extraction subagent. Read the files listed and extract a knowledge graph fragment. Output ONLY valid JSON matching the schema below. + +Files: +FILE_LIST + +[copy the extraction rules and JSON schema verbatim from the existing Step B2 content — already in the file from the cp step] +``` + +One `@mention` block per chunk. All in the same message — this is what makes them parallel. + +Wait for all agents to return. Parse each response as JSON. Accumulate nodes/edges/hyperedges across all results into `.graphify_semantic_new.json`. +``` + +- [ ] **Step 3: Verify the file looks correct** + +```bash +grep -n "@mention\|Step B2\|Step B3" graphify/skill-opencode.md | head -20 +``` + +Expected: lines showing @mention in B2 and Step B3 after it. + +- [ ] **Step 4: Commit** + +```bash +git add graphify/skill-opencode.md +git commit -m "add skill-opencode.md for OpenCode platform (@mention parallel extraction)" +``` + +--- + +## Task 4: Create `skill-claw.md` + +OpenClaw's agent support is MVP/incomplete so extraction is sequential — the orchestrating LLM reads each file and extracts directly. + +**Files:** +- Create: `graphify/skill-claw.md` + +- [ ] **Step 1: Copy skill.md as the base** + +```bash +cp graphify/skill.md graphify/skill-claw.md +``` + +- [ ] **Step 2: Open `graphify/skill-claw.md` and replace the Step B2 section** + +Replace the entire Step B2 section with: + +```markdown +**Step B2 - Sequential extraction (OpenClaw)** + +> **OpenClaw platform:** OpenClaw's multi-agent support is still early. Extraction runs sequentially — you read each file yourself and extract directly. This is slower than parallel platforms but reliable. + +Load files from `.graphify_uncached.txt`. For each file, one at a time: + +1. Read the file contents +2. Extract nodes, edges, and hyperedges following the same rules and schema as the parallel variant (see schema below) +3. Accumulate results into a running JSON object + +Apply all the same extraction rules: +- EXTRACTED / INFERRED / AMBIGUOUS confidence with confidence_score on every edge +- rationale_for nodes for design decisions and WHY comments +- semantically_similar_to edges for cross-file conceptual links (non-obvious only) +- hyperedges for groups of 3+ nodes (max 3 per file) +- DEEP_MODE: more aggressive INFERRED edges if --mode deep was given + +Schema (same as parallel variant): +{"nodes":[{"id":"filestem_entityname","label":"Human Readable Name","file_type":"code|document|paper|image","source_file":"relative/path","source_location":null,"source_url":null,"captured_at":null,"author":null,"contributor":null}],"edges":[{"source":"node_id","target":"node_id","relation":"calls|implements|references|cites|conceptually_related_to|shares_data_with|semantically_similar_to|rationale_for","confidence":"EXTRACTED|INFERRED|AMBIGUOUS","confidence_score":1.0,"source_file":"relative/path","source_location":null,"weight":1.0}],"hyperedges":[{"id":"snake_case_id","label":"Human Readable Label","nodes":["node_id1","node_id2","node_id3"],"relation":"participate_in|implement|form","confidence":"EXTRACTED|INFERRED","confidence_score":0.75,"source_file":"relative/path"}],"input_tokens":0,"output_tokens":0} + +After processing all files, write the accumulated result to `.graphify_semantic_new.json`. +``` + +- [ ] **Step 3: Also remove the timing estimate block from Step B** + +In skill-claw.md, find and remove this paragraph (it only applies to parallel dispatch): + +``` +Before dispatching subagents, print a timing estimate: +- Load `total_words` and file counts from `.graphify_detect.json` +- Estimate agents needed: `ceil(uncached_non_code_files / 22)` (chunk size is 20-25) +- Estimate time: ~45s per agent batch (they run in parallel, so total ≈ 45s × ceil(agents/parallel_limit)) +- Print: "Semantic extraction: ~N files → X agents, estimated ~Ys" +``` + +Replace with: + +``` +Print: "Semantic extraction: N files (sequential — OpenClaw platform)" +``` + +- [ ] **Step 4: Verify** + +```bash +grep -n "sequential\|Step B2\|Step B3\|spawn_agent\|@mention" graphify/skill-claw.md | head -20 +``` + +Expected: "sequential" appears in B2, no spawn_agent or @mention. + +- [ ] **Step 5: Commit** + +```bash +git add graphify/skill-claw.md +git commit -m "add skill-claw.md for OpenClaw platform (sequential extraction)" +``` + +--- + +## Task 5: Update `pyproject.toml` package-data + +**Files:** +- Modify: `pyproject.toml` + +- [ ] **Step 1: Update package-data to include the three new skill files** + +In `pyproject.toml`, find: + +```toml +[tool.setuptools.package-data] +graphify = ["skill.md"] +``` + +Replace with: + +```toml +[tool.setuptools.package-data] +graphify = ["skill.md", "skill-codex.md", "skill-opencode.md", "skill-claw.md"] +``` + +- [ ] **Step 2: Verify** + +```bash +grep -A2 "package-data" pyproject.toml +``` + +Expected: all four skill files listed. + +- [ ] **Step 3: Commit** + +```bash +git add pyproject.toml +git commit -m "include platform skill files in package-data" +``` + +--- + +## Task 6: Add `--platform` flag to install command + +**Files:** +- Modify: `graphify/__main__.py` + +- [ ] **Step 1: Write the failing test first** + +Create `tests/test_install.py`: + +```python +"""Tests for graphify install --platform routing.""" +import shutil +from pathlib import Path +import pytest +from unittest.mock import patch + + +PLATFORMS = { + "claude": ("skill.md", ".claude/skills/graphify/SKILL.md"), + "codex": ("skill-codex.md", ".agents/skills/graphify/SKILL.md"), + "opencode": ("skill-opencode.md", ".config/opencode/skills/graphify/SKILL.md"), + "claw": ("skill-claw.md", ".claw/skills/graphify/SKILL.md"), +} + + +def test_install_default_uses_claude_skill(tmp_path): + """install() with no platform copies skill.md to ~/.claude/skills/graphify/SKILL.md""" + from graphify.__main__ import install + with patch("graphify.__main__.Path.home", return_value=tmp_path): + install(platform="claude") + dst = tmp_path / ".claude" / "skills" / "graphify" / "SKILL.md" + assert dst.exists() + + +def test_install_codex_copies_correct_file(tmp_path): + from graphify.__main__ import install + with patch("graphify.__main__.Path.home", return_value=tmp_path): + install(platform="codex") + dst = tmp_path / ".agents" / "skills" / "graphify" / "SKILL.md" + assert dst.exists() + + +def test_install_opencode_copies_correct_file(tmp_path): + from graphify.__main__ import install + with patch("graphify.__main__.Path.home", return_value=tmp_path): + install(platform="opencode") + dst = tmp_path / ".config" / "opencode" / "skills" / "graphify" / "SKILL.md" + assert dst.exists() + + +def test_install_claw_copies_correct_file(tmp_path): + from graphify.__main__ import install + with patch("graphify.__main__.Path.home", return_value=tmp_path): + install(platform="claw") + dst = tmp_path / ".claw" / "skills" / "graphify" / "SKILL.md" + assert dst.exists() + + +def test_install_unknown_platform_exits(tmp_path): + from graphify.__main__ import install + with patch("graphify.__main__.Path.home", return_value=tmp_path): + with pytest.raises(SystemExit): + install(platform="unknown") + + +def test_all_skill_files_exist_in_package(): + """Verify all platform skill files are present in the installed package.""" + import graphify + pkg_dir = Path(graphify.__file__).parent + for src_name, _ in PLATFORMS.values(): + skill_path = pkg_dir / src_name + assert skill_path.exists(), f"Missing skill file: {src_name}" +``` + +- [ ] **Step 2: Run the test to verify it fails** + +```bash +python -m pytest tests/test_install.py -v --tb=short 2>&1 | head -40 +``` + +Expected: FAIL — `install()` doesn't accept a `platform` argument yet. + +- [ ] **Step 3: Update `install()` in `graphify/__main__.py`** + +Replace the current `install()` function and add `_PLATFORM_CONFIG`: + +```python +_PLATFORM_CONFIG = { + "claude": { + "skill_file": "skill.md", + "skill_dst": Path(".claude") / "skills" / "graphify" / "SKILL.md", + "claude_md": True, # only Claude Code gets CLAUDE.md registration + }, + "codex": { + "skill_file": "skill-codex.md", + "skill_dst": Path(".agents") / "skills" / "graphify" / "SKILL.md", + "claude_md": False, + }, + "opencode": { + "skill_file": "skill-opencode.md", + "skill_dst": Path(".config") / "opencode" / "skills" / "graphify" / "SKILL.md", + "claude_md": False, + }, + "claw": { + "skill_file": "skill-claw.md", + "skill_dst": Path(".claw") / "skills" / "graphify" / "SKILL.md", + "claude_md": False, + }, +} + + +def install(platform: str = "claude") -> None: + if platform not in _PLATFORM_CONFIG: + print(f"error: unknown platform '{platform}'. Choose from: {', '.join(_PLATFORM_CONFIG)}", file=sys.stderr) + sys.exit(1) + + cfg = _PLATFORM_CONFIG[platform] + skill_src = Path(__file__).parent / cfg["skill_file"] + if not skill_src.exists(): + print(f"error: {cfg['skill_file']} not found in package - reinstall graphify", file=sys.stderr) + sys.exit(1) + + skill_dst = Path.home() / cfg["skill_dst"] + skill_dst.parent.mkdir(parents=True, exist_ok=True) + shutil.copy(skill_src, skill_dst) + print(f" skill installed → {skill_dst}") + + if cfg["claude_md"]: + # Register in ~/.claude/CLAUDE.md (Claude Code only) + claude_md = Path.home() / ".claude" / "CLAUDE.md" + if claude_md.exists(): + content = claude_md.read_text() + if "graphify" in content: + print(f" CLAUDE.md → already registered (no change)") + else: + claude_md.write_text(content.rstrip() + _SKILL_REGISTRATION) + print(f" CLAUDE.md → skill registered in {claude_md}") + else: + claude_md.parent.mkdir(parents=True, exist_ok=True) + claude_md.write_text(_SKILL_REGISTRATION.lstrip()) + print(f" CLAUDE.md → created at {claude_md}") + + print() + print("Done. Open your AI coding assistant and type:") + print() + print(" /graphify .") + print() +``` + +- [ ] **Step 4: Update `main()` to pass `--platform` to `install()`** + +In `main()`, find the `if cmd == "install":` block: + +```python + if cmd == "install": + install() +``` + +Replace with: + +```python + if cmd == "install": + platform = "claude" + args = sys.argv[2:] + i = 0 + while i < len(args): + if args[i].startswith("--platform="): + platform = args[i].split("=", 1)[1] + i += 1 + elif args[i] == "--platform" and i + 1 < len(args): + platform = args[i + 1] + i += 2 + else: + i += 1 + install(platform=platform) +``` + +- [ ] **Step 5: Update the help text in `main()`** + +Find: +```python + print(" install copy skill to ~/.claude/skills/ and register in CLAUDE.md") +``` + +Replace with: +```python + print(" install [--platform P] copy skill to platform config dir (claude|codex|opencode|claw)") +``` + +- [ ] **Step 6: Run tests to verify they pass** + +```bash +python -m pytest tests/test_install.py -v --tb=short +``` + +Expected: all 6 tests PASS. + +- [ ] **Step 7: Run the full test suite to check for regressions** + +```bash +python -m pytest tests/ -q --tb=short 2>&1 | tail -20 +``` + +Expected: existing tests still pass. + +- [ ] **Step 8: Commit** + +```bash +git add graphify/__main__.py tests/test_install.py +git commit -m "add --platform flag to graphify install (codex, opencode, claw)" +``` + +--- + +## Task 7: Update README + +**Files:** +- Modify: `README.md` + +- [ ] **Step 1: Add platform support table under the Install section** + +After the `pip install graphifyy && graphify install` code block, add: + +```markdown +### Platform support + +| Platform | Install command | +|----------|----------------| +| Claude Code | `graphify install` | +| Codex | `graphify install --platform codex` | +| OpenCode | `graphify install --platform opencode` | +| OpenClaw | `graphify install --platform claw` | + +Codex users also need `multi_agent = true` under `[features]` in `~/.codex/config.toml` for parallel extraction. OpenClaw uses sequential extraction (parallel agent support is still early on that platform). +``` + +- [ ] **Step 2: Clarify token efficiency — find the benchmark section** + +Find the line: +``` +**Token benchmark** - printed automatically after every run. On a mixed corpus (Karpathy repos + papers + images): **71.5x** fewer tokens per query vs reading raw files. +``` + +Replace with: +``` +**Token benchmark** - printed automatically after every run. On a mixed corpus (Karpathy repos + papers + images): **71.5x** fewer tokens per query vs reading raw files. The first run extracts and builds the graph (this costs tokens). Every subsequent query reads the compact graph instead of raw files — that's where the savings compound. The SHA256 cache means re-runs only re-process changed files. +``` + +- [ ] **Step 3: Verify README renders correctly** + +```bash +grep -n "Platform support\|multi_agent\|first run extracts" README.md +``` + +Expected: all three lines found. + +- [ ] **Step 4: Commit** + +```bash +git add README.md +git commit -m "add platform support table and clarify token efficiency in README" +``` + +--- + +## Task 8: Final verification + +- [ ] **Step 1: Run the full test suite** + +```bash +python -m pytest tests/ -q --tb=short 2>&1 | tail -20 +``` + +Expected: all tests pass, no regressions. + +- [ ] **Step 2: Verify all four skill files are present in the package** + +```bash +ls graphify/skill*.md +``` + +Expected: +``` +graphify/skill.md +graphify/skill-codex.md +graphify/skill-opencode.md +graphify/skill-claw.md +``` + +- [ ] **Step 3: Smoke test each install path** + +```bash +python -m graphify.__main__ install --platform codex 2>&1 | head -5 +python -m graphify.__main__ install --platform opencode 2>&1 | head -5 +python -m graphify.__main__ install --platform claw 2>&1 | head -5 +python -m graphify.__main__ install --platform unknown 2>&1 +``` + +Expected: first three print "skill installed →", last prints "error: unknown platform". + +- [ ] **Step 4: Push v3 branch** + +```bash +git push -u origin v3 +``` diff --git a/docs/superpowers/specs/2026-04-06-v3-platform-compatibility-design.md b/docs/superpowers/specs/2026-04-06-v3-platform-compatibility-design.md new file mode 100644 index 00000000..d04b86bb --- /dev/null +++ b/docs/superpowers/specs/2026-04-06-v3-platform-compatibility-design.md @@ -0,0 +1,92 @@ +# v3 Platform Compatibility Design + +**Date:** 2026-04-06 +**Status:** Approved + +## Problem + +graphify's `skill.md` uses the Claude Code `Agent` tool for parallel semantic extraction. Users on Codex, OpenCode, and OpenClaw cannot use the skill. v3 adds platform-specific skill files so graphify works natively on all four platforms. + +## Scope + +- Four platform-specific skill files (one already exists) +- `graphify install --platform X` routing +- README clarifications (token efficiency, platform table) +- No always-on project hooks for non-Claude-Code platforms in v3 (deferred to v3.1) + +## What Changes Per Platform + +The semantic extraction step (Step 3B) is the **only** section that differs. AST extraction, merging, clustering, labelling, export, and benchmarking are identical across all platforms and live in the shared Python CLI. + +| Platform | Extraction approach | Rationale | +|----------|-------------------|-----------| +| Claude Code | Parallel `Agent` tool calls | Current behavior, unchanged | +| Codex | Parallel `spawn_agent` + `wait` + `close_agent` | Codex multi-agent API (`multi_agent = true` required) | +| OpenCode | Parallel `@mention` dispatches | OpenCode's native subagent system | +| OpenClaw | Sequential loop — orchestrator extracts each file itself | OpenClaw agent support is MVP/incomplete; sequential is reliable | + +## File Structure + +``` +graphify/ +├── skill.md # Claude Code (unchanged) +├── skill-codex.md # Codex — parallel via spawn_agent +├── skill-opencode.md # OpenCode — parallel via @mention +├── skill-claw.md # OpenClaw — sequential extraction +``` + +All four files ship in the PyPI package via `pyproject.toml` `package-data`. + +## Install Command + +`graphify install` gains a `--platform` flag: + +``` +graphify install # Claude Code → ~/.claude/skills/graphify/SKILL.md +graphify install --platform codex # Codex → ~/.agents/skills/graphify/SKILL.md +graphify install --platform opencode # OpenCode → ~/.config/opencode/skills/graphify/SKILL.md +graphify install --platform claw # OpenClaw → ~/.claw/skills/graphify/SKILL.md +``` + +Behaviour: +- Creates target directory if it doesn't exist (same as current Claude Code install) +- If the platform's root config directory doesn't exist, prints a warning and exits cleanly: `"Codex config directory not found — is Codex installed?"` +- `--platform` is optional; default is `claude` (current behaviour preserved) + +## Skill File Content + +Each file follows the same structure as `skill.md`. The extraction step (Step 3B) is rewritten for the platform: + +**Codex (`skill-codex.md`):** +- For each uncached file, call `spawn_agent(agent_type="worker", message=)` +- Collect all agent handles, call `wait()` on each, then `close_agent()` +- Requires user to have `multi_agent = true` in `~/.codex/config.toml`; skill notes this requirement + +**OpenCode (`skill-opencode.md`):** +- For each uncached file, dispatch via `@mention` with the extraction prompt +- Collect results as agents complete + +**OpenClaw (`skill-claw.md`):** +- Loop over uncached files sequentially +- Orchestrating LLM reads each file and extracts concepts/relationships/edges directly +- Slower than parallel platforms but reliable given OpenClaw's MVP agent status +- A note in the skill explains why: "OpenClaw's multi-agent support is still early; sequential extraction ensures reliability" + +## README Changes + +1. Add "Platform support" table under the Install section +2. Clarify token efficiency: *"First run extracts and builds the graph — subsequent queries read the compact graph instead of raw files. The 71.5x reduction applies per query, and the cache means re-runs only re-process changed files."* +3. Note sequential extraction on OpenClaw with brief explanation + +## Not In Scope (v3) + +- `graphify codex install` / `graphify opencode install` (always-on project hooks for non-CC platforms) — deferred to v3.1 +- Gemini CLI support — not enough information yet +- Copilot CLI support — not enough information yet + +## Testing + +- Unit tests in `tests/test_install.py`: verify `--platform X` routes to correct source file and target path +- Package data test: assert all four skill files are present in the installed package +- No execution tests for platform-specific extraction (requires live platform) +- Evals before release: run each platform skill on a real corpus, verify graph output is equivalent diff --git a/graphify/__main__.py b/graphify/__main__.py index fbd4ec93..cb81d39b 100644 --- a/graphify/__main__.py +++ b/graphify/__main__.py @@ -29,39 +29,66 @@ _SKILL_REGISTRATION = ( ) -def _bundled_skill() -> Path: - """Path to the skill.md bundled with this package.""" - return Path(__file__).parent / "skill.md" +_PLATFORM_CONFIG: dict[str, dict] = { + "claude": { + "skill_file": "skill.md", + "skill_dst": Path(".claude") / "skills" / "graphify" / "SKILL.md", + "claude_md": True, + }, + "codex": { + "skill_file": "skill-codex.md", + "skill_dst": Path(".agents") / "skills" / "graphify" / "SKILL.md", + "claude_md": False, + }, + "opencode": { + "skill_file": "skill-opencode.md", + "skill_dst": Path(".config") / "opencode" / "skills" / "graphify" / "SKILL.md", + "claude_md": False, + }, + "claw": { + "skill_file": "skill-claw.md", + "skill_dst": Path(".claw") / "skills" / "graphify" / "SKILL.md", + "claude_md": False, + }, +} -def install() -> None: - skill_src = _bundled_skill() - if not skill_src.exists(): - print("error: skill.md not found in package - reinstall graphify", file=sys.stderr) +def install(platform: str = "claude") -> None: + if platform not in _PLATFORM_CONFIG: + print( + f"error: unknown platform '{platform}'. Choose from: {', '.join(_PLATFORM_CONFIG)}", + file=sys.stderr, + ) sys.exit(1) - # Copy skill to ~/.claude/skills/graphify/SKILL.md - skill_dst = Path.home() / ".claude" / "skills" / "graphify" / "SKILL.md" + cfg = _PLATFORM_CONFIG[platform] + skill_src = Path(__file__).parent / cfg["skill_file"] + if not skill_src.exists(): + print(f"error: {cfg['skill_file']} not found in package - reinstall graphify", file=sys.stderr) + sys.exit(1) + + skill_dst = Path.home() / cfg["skill_dst"] skill_dst.parent.mkdir(parents=True, exist_ok=True) shutil.copy(skill_src, skill_dst) print(f" skill installed → {skill_dst}") - # Register in ~/.claude/CLAUDE.md - claude_md = Path.home() / ".claude" / "CLAUDE.md" - if claude_md.exists(): - content = claude_md.read_text() - if "graphify" in content: - print(f" CLAUDE.md → already registered (no change)") + if cfg["claude_md"]: + # Register in ~/.claude/CLAUDE.md (Claude Code only) + claude_md = Path.home() / ".claude" / "CLAUDE.md" + if claude_md.exists(): + content = claude_md.read_text() + if "graphify" in content: + print(f" CLAUDE.md → already registered (no change)") + else: + claude_md.write_text(content.rstrip() + _SKILL_REGISTRATION) + print(f" CLAUDE.md → skill registered in {claude_md}") else: - claude_md.write_text(content.rstrip() + _SKILL_REGISTRATION) - print(f" CLAUDE.md → skill registered in {claude_md}") - else: - claude_md.parent.mkdir(parents=True, exist_ok=True) - claude_md.write_text(_SKILL_REGISTRATION.lstrip()) - print(f" CLAUDE.md → created at {claude_md}") + claude_md.parent.mkdir(parents=True, exist_ok=True) + claude_md.write_text(_SKILL_REGISTRATION.lstrip()) + print(f" CLAUDE.md → created at {claude_md}") print() - print("Done. Open Claude Code in any directory and type:") + print("Done. Open your AI coding assistant and type:") print() print(" /graphify .") print() @@ -184,7 +211,7 @@ def main() -> None: print("Usage: graphify ") print() print("Commands:") - print(" install copy skill to ~/.claude/skills/ and register in CLAUDE.md") + print(" install [--platform P] copy skill to platform config dir (claude|codex|opencode|claw)") print(" benchmark [graph.json] measure token reduction vs naive full-corpus approach") print(" hook install install post-commit git hook (auto-rebuilds graph on commit)") print(" hook uninstall remove post-commit git hook") @@ -196,7 +223,19 @@ def main() -> None: cmd = sys.argv[1] if cmd == "install": - install() + platform = "claude" + args = sys.argv[2:] + i = 0 + while i < len(args): + if args[i].startswith("--platform="): + platform = args[i].split("=", 1)[1] + i += 1 + elif args[i] == "--platform" and i + 1 < len(args): + platform = args[i + 1] + i += 2 + else: + i += 1 + install(platform=platform) elif cmd == "claude": subcmd = sys.argv[2] if len(sys.argv) > 2 else "" if subcmd == "install": diff --git a/pyproject.toml b/pyproject.toml index b7759a9f..b24133c5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,4 +49,4 @@ where = ["."] include = ["graphify*"] [tool.setuptools.package-data] -graphify = ["skill.md"] +graphify = ["skill.md", "skill-codex.md", "skill-opencode.md", "skill-claw.md"] diff --git a/tests/test_install.py b/tests/test_install.py new file mode 100644 index 00000000..5cee5904 --- /dev/null +++ b/tests/test_install.py @@ -0,0 +1,85 @@ +"""Tests for graphify install --platform routing.""" +from pathlib import Path +from unittest.mock import patch +import pytest + + +PLATFORMS = { + "claude": (".claude/skills/graphify/SKILL.md",), + "codex": (".agents/skills/graphify/SKILL.md",), + "opencode": (".config/opencode/skills/graphify/SKILL.md",), + "claw": (".claw/skills/graphify/SKILL.md",), +} + + +def _install(tmp_path, platform): + from graphify.__main__ import install + with patch("graphify.__main__.Path.home", return_value=tmp_path): + install(platform=platform) + + +def test_install_default_claude(tmp_path): + _install(tmp_path, "claude") + assert (tmp_path / ".claude" / "skills" / "graphify" / "SKILL.md").exists() + + +def test_install_codex(tmp_path): + _install(tmp_path, "codex") + assert (tmp_path / ".agents" / "skills" / "graphify" / "SKILL.md").exists() + + +def test_install_opencode(tmp_path): + _install(tmp_path, "opencode") + assert (tmp_path / ".config" / "opencode" / "skills" / "graphify" / "SKILL.md").exists() + + +def test_install_claw(tmp_path): + _install(tmp_path, "claw") + assert (tmp_path / ".claw" / "skills" / "graphify" / "SKILL.md").exists() + + +def test_install_unknown_platform_exits(tmp_path): + with pytest.raises(SystemExit): + _install(tmp_path, "unknown") + + +def test_codex_skill_contains_spawn_agent(): + """Codex skill file must reference spawn_agent.""" + import graphify + skill = (Path(graphify.__file__).parent / "skill-codex.md").read_text() + assert "spawn_agent" in skill + + +def test_opencode_skill_contains_mention(): + """OpenCode skill file must reference @mention.""" + import graphify + skill = (Path(graphify.__file__).parent / "skill-opencode.md").read_text() + assert "@mention" in skill + + +def test_claw_skill_is_sequential(): + """OpenClaw skill file must describe sequential extraction.""" + import graphify + skill = (Path(graphify.__file__).parent / "skill-claw.md").read_text() + assert "sequential" in skill.lower() + assert "spawn_agent" not in skill + assert "@mention" not in skill + + +def test_all_skill_files_exist_in_package(): + """All four platform skill files must be present in the installed package.""" + import graphify + pkg = Path(graphify.__file__).parent + for name in ("skill.md", "skill-codex.md", "skill-opencode.md", "skill-claw.md"): + assert (pkg / name).exists(), f"Missing: {name}" + + +def test_claude_install_registers_claude_md(tmp_path): + """Claude platform install writes CLAUDE.md; others do not.""" + _install(tmp_path, "claude") + assert (tmp_path / ".claude" / "CLAUDE.md").exists() + + +def test_codex_install_does_not_write_claude_md(tmp_path): + _install(tmp_path, "codex") + assert not (tmp_path / ".claude" / "CLAUDE.md").exists()