Skip to content

Commit 2c6f083

Browse files
feat: add CLI (gcf encode/decode/stats) bundled with library
1 parent 228366d commit 2c6f083

3 files changed

Lines changed: 180 additions & 3 deletions

File tree

README.md

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,28 @@ Python implementation of [GCF (Graph Compact Format)](https://github.com/blackwe
1515
pip install gcf-py
1616
```
1717

18-
Zero dependencies. Pure Python. Python 3.9+.
18+
Zero dependencies. Pure Python. Python 3.9+. Includes CLI.
1919

20-
## Quick Start
20+
## CLI
21+
22+
```bash
23+
gcf encode < payload.json # JSON to GCF
24+
gcf decode < payload.gcf # GCF to JSON
25+
gcf stats < payload.json # token comparison with visual bar
26+
```
27+
28+
```
29+
Payload: 50 symbols, 20 edges
30+
31+
JSON ██████████████████████████████ 4,200 tokens
32+
GCF ████████░░░░░░░░░░░░░░░░░░░░░░ 1,150 tokens
33+
34+
Savings: 73% fewer tokens with GCF
35+
```
36+
37+
## Library
38+
39+
### Quick Start
2140

2241
```python
2342
from gcf import encode, Payload, Symbol, Edge

pyproject.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,13 @@ classifiers = [
2727
"Typing :: Typed",
2828
]
2929

30+
[project.scripts]
31+
gcf = "gcf.cli:main"
32+
3033
[project.urls]
3134
Homepage = "https://github.com/blackwell-systems/gcf-python"
35+
Documentation = "https://blackwell-systems.github.io/gcf/"
3236
Specification = "https://github.com/blackwell-systems/gcf"
33-
"Go Implementation" = "https://github.com/blackwell-systems/gcf-go"
3437

3538
[tool.hatch.build.targets.wheel]
3639
packages = ["src/gcf"]

src/gcf/cli.py

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
"""GCF command-line interface: encode, decode, stats."""
2+
3+
import json
4+
import sys
5+
6+
from .decode import decode
7+
from .encode import encode
8+
from .types import Edge, Payload, Symbol
9+
10+
USAGE = """gcf - token-optimized wire format for LLM tool responses
11+
12+
Usage:
13+
gcf encode [file] Encode JSON payload to GCF (stdin if no file)
14+
gcf decode [file] Decode GCF text to JSON (stdin if no file)
15+
gcf stats [file] Compare token counts: JSON vs GCF (stdin if no file)
16+
gcf version Print version
17+
18+
Examples:
19+
gcf encode < payload.json
20+
gcf decode < payload.gcf
21+
gcf stats payload.json
22+
"""
23+
24+
25+
def main() -> None:
26+
args = sys.argv[1:]
27+
if not args or args[0] in ("-h", "--help", "help"):
28+
print(USAGE, end="")
29+
sys.exit(0 if args else 1)
30+
31+
cmd = args[0]
32+
file_args = args[1:]
33+
34+
if cmd == "encode":
35+
data = _read_input(file_args)
36+
_do_encode(data)
37+
elif cmd == "decode":
38+
data = _read_input(file_args)
39+
_do_decode(data)
40+
elif cmd == "stats":
41+
data = _read_input(file_args)
42+
_do_stats(data)
43+
elif cmd == "version":
44+
print("gcf 0.1.0")
45+
else:
46+
print(f"unknown command: {cmd}\n", file=sys.stderr)
47+
print(USAGE, file=sys.stderr, end="")
48+
sys.exit(1)
49+
50+
51+
def _read_input(args: list[str]) -> str:
52+
if args and args[0] != "-":
53+
with open(args[0]) as f:
54+
return f.read()
55+
return sys.stdin.read()
56+
57+
58+
def _payload_from_json(data: str) -> Payload:
59+
obj = json.loads(data)
60+
symbols = [
61+
Symbol(
62+
qualified_name=s["qualifiedName"],
63+
kind=s["kind"],
64+
score=s["score"],
65+
provenance=s["provenance"],
66+
distance=s.get("distance", 0),
67+
)
68+
for s in obj.get("symbols", [])
69+
]
70+
edges = [
71+
Edge(
72+
source=e["source"],
73+
target=e["target"],
74+
edge_type=e["edgeType"],
75+
status=e.get("status", ""),
76+
)
77+
for e in obj.get("edges", [])
78+
]
79+
return Payload(
80+
tool=obj.get("tool", ""),
81+
token_budget=obj.get("tokenBudget", 0),
82+
tokens_used=obj.get("tokensUsed", 0),
83+
pack_root=obj.get("packRoot", ""),
84+
symbols=symbols,
85+
edges=edges,
86+
)
87+
88+
89+
def _payload_to_json(p: Payload) -> str:
90+
obj = {
91+
"tool": p.tool,
92+
"tokensUsed": p.tokens_used,
93+
"tokenBudget": p.token_budget,
94+
"packRoot": p.pack_root,
95+
"symbols": [
96+
{
97+
"qualifiedName": s.qualified_name,
98+
"kind": s.kind,
99+
"score": s.score,
100+
"provenance": s.provenance,
101+
"distance": s.distance,
102+
}
103+
for s in p.symbols
104+
],
105+
"edges": [
106+
{
107+
"source": e.source,
108+
"target": e.target,
109+
"edgeType": e.edge_type,
110+
**({"status": e.status} if e.status else {}),
111+
}
112+
for e in p.edges
113+
],
114+
}
115+
return json.dumps(obj, indent=2)
116+
117+
118+
def _do_encode(data: str) -> None:
119+
try:
120+
p = _payload_from_json(data)
121+
except (json.JSONDecodeError, KeyError, TypeError) as e:
122+
print(f"error: invalid JSON: {e}", file=sys.stderr)
123+
sys.exit(1)
124+
print(encode(p), end="")
125+
126+
127+
def _do_decode(data: str) -> None:
128+
p = decode(data)
129+
print(_payload_to_json(p))
130+
131+
132+
def _do_stats(data: str) -> None:
133+
try:
134+
p = _payload_from_json(data)
135+
except (json.JSONDecodeError, KeyError, TypeError) as e:
136+
print(f"error: invalid JSON: {e}", file=sys.stderr)
137+
sys.exit(1)
138+
139+
gcf_output = encode(p)
140+
json_tokens = len(data.strip()) // 4
141+
gcf_tokens = len(gcf_output.strip()) // 4
142+
143+
savings = 0.0
144+
if json_tokens > 0:
145+
savings = 100.0 * (1.0 - gcf_tokens / json_tokens)
146+
147+
bar_width = 30
148+
json_bar = "█" * bar_width
149+
gcf_filled = (gcf_tokens * bar_width) // json_tokens if json_tokens > 0 else 0
150+
gcf_bar = "█" * gcf_filled + "░" * (bar_width - gcf_filled)
151+
152+
print(f"Payload: {len(p.symbols)} symbols, {len(p.edges)} edges\n")
153+
print(f" JSON {json_bar} {json_tokens} tokens")
154+
print(f" GCF {gcf_bar} {gcf_tokens} tokens")
155+
print(f"\n Savings: {savings:.0f}% fewer tokens with GCF")

0 commit comments

Comments
 (0)