mirror of
https://github.com/safishamsi/graphify.git
synced 2026-07-12 02:17:04 +00:00
add --platform flag to install command with Codex/OpenCode/OpenClaw support
This commit is contained in:
@@ -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.
|
||||
|
||||
|
||||
@@ -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<agent-instructions>\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</agent-instructions>\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
|
||||
```
|
||||
@@ -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=<extraction prompt>)`
|
||||
- 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
|
||||
+63
-24
@@ -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 <command>")
|
||||
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":
|
||||
|
||||
+1
-1
@@ -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"]
|
||||
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user