Skip to content

Commit d667644

Browse files
feat: add GitHub Copilot CLI support
- config.rs: Config::load() now respects SQUEEZ_DIR env var for profile switching instead of always reading from ~/.claude/squeez/ - init.rs: add run_copilot() / squeez init --copilot subcommand that stores session state in ~/.copilot/squeez/ and injects memory banner into ~/.copilot/copilot-instructions.md (auto-loaded by Copilot CLI) - main.rs: parse 'init --copilot' flag - hooks/copilot-pretooluse.sh: PreToolUse bash compression with SQUEEZ_DIR pointing to ~/.copilot/squeez/ - hooks/copilot-session-start.sh: SessionStart hook that calls 'squeez init --copilot' - hooks/copilot-posttooluse.sh: PostToolUse token tracking with SQUEEZ_DIR pointing to ~/.copilot/squeez/ - install.sh: new Copilot CLI section registers hooks in ~/.copilot/settings.json and seeds copilot-instructions.md - bench/run.sh: auto-detect local release build vs installed binary - bench/fixtures/git_copilot_session.txt: new fixture representing a real Copilot CLI bash session (35% compression, 3ms)
1 parent 8519620 commit d667644

10 files changed

Lines changed: 292 additions & 14 deletions

File tree

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
Cloning into 'openwatch'...
2+
remote: Enumerating objects: 2745, done.
3+
remote: Counting objects: 0% (1/150)
4+
remote: Counting objects: 50% (75/150)
5+
remote: Counting objects: 100% (150/150), done.
6+
remote: Compressing objects: 50% (39/78)
7+
remote: Compressing objects: 100% (78/78), done.
8+
Receiving objects: 10% (275/2745)
9+
Receiving objects: 50% (1373/2745)
10+
Receiving objects: 100% (2745/2745), 1.82 MiB | 8.36 MiB/s, done.
11+
Resolving deltas: 50% (849/1696)
12+
Resolving deltas: 100% (1696/1696), done.
13+
# squeez [git] 950→42 tokens (-95%) 12ms
14+
1dc6071 Fix systemic limitations and improve data reliability
15+
6d7c12b chore: track copilot-instructions.md and fix inconsistencies
16+
68dd123 chore: ignore _worktrees directory
17+
602e402 feat: expand data coverage via MCP Brasil
18+
c2d6044 fix(worker): structured logging and resilient startup observability
19+
branch 'main' set up to track 'origin/main'.
20+
Already up to date.
21+
Updating fc86688..1dc6071
22+
Fast-forward
23+
.claude/settings.json | 10 +-
24+
.gitattributes | 7 +
25+
shared/connectors/__init__.py | 12 +-
26+
shared/connectors/datajud.py | 312 ++++
27+
shared/connectors/ibge.py | 287 ++++
28+
shared/connectors/tcu.py | 198 +++
29+
shared/typologies/base.py | 34 +-
30+
shared/typologies/registry.py | 21 +-
31+
shared/typologies/t01_concentration.py | 8 +-
32+
shared/typologies/t02_low_competition.py | 15 +-
33+
shared/typologies/t03_splitting.py | 8 +-
34+
tests/conftest.py | 45 +-
35+
tests/connectors/test_connectors.py | 87 +-
36+
tests/worker/test_signal_tasks.py | 34 +-
37+
web/src/app/globals.css | 112 +-
38+
web/src/app/page.tsx | 89 +-
39+
web/src/components/Badge.tsx | 45 +-
40+
web/src/lib/design-tokens.ts | 78 +-
41+
worker/tasks/signal_tasks.py | 56 +-
42+
37 files changed, 1847 insertions(+), 423 deletions(-)
43+
CONFLICT (content): Merge conflict in shared/connectors/__init__.py
44+
CONFLICT (content): Merge conflict in tests/conftest.py
45+
Done

bench/report.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
FIXTURE BEFORE AFTER REDUCTION LATENCY STATUS
22
──────────────────────────────────────────────────────────────────────────────
3-
docker_logs.txt 665tk 186tk 73% 5ms
3+
docker_logs.txt 665tk 186tk 73% 4ms
44
env_dump.txt 441tk 287tk 35% 3ms ✅
55
find_deep.txt 424tk 134tk 69% 3ms ✅
6-
git_diff.txt 502tk 317tk 37% 4ms ✅
7-
git_log_200.txt 2667tk 819tk 70% 4ms ✅
6+
git_copilot_session.txt 639tk 421tk 35% 3ms ✅
7+
git_diff.txt 502tk 317tk 37% 3ms ✅
8+
git_log_200.txt 2667tk 819tk 70% 3ms ✅
89
git_status.txt 50tk 16tk 68% 3ms ✅
910
ls_la.txt 1782tk 886tk 51% 4ms ✅
1011
npm_install.txt 524tk 231tk 56% 3ms ✅
1112
ps_aux.txt 40373tk 2352tk 95% 6ms ✅
1213

13-
PASS: 9/9 FAIL: 0/9
14+
PASS: 10/10 FAIL: 0/10

bench/run.sh

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
#!/usr/bin/env bash
22
set -euo pipefail
3-
SQUEEZ="$HOME/.claude/squeez/bin/squeez"
4-
if [ ! -x "$SQUEEZ" ]; then
5-
echo "ERROR: squeez binary not found at $SQUEEZ" >&2
3+
# Use local dev build if available, otherwise fall back to installed binary
4+
if [ -x "$(dirname "$0")/../target/release/squeez" ]; then
5+
SQUEEZ="$(cd "$(dirname "$0")/.." && pwd)/target/release/squeez"
6+
elif [ -x "$HOME/.claude/squeez/bin/squeez" ]; then
7+
SQUEEZ="$HOME/.claude/squeez/bin/squeez"
8+
else
9+
echo "ERROR: squeez binary not found. Run 'cargo build --release' first." >&2
610
exit 1
711
fi
812
FIXTURES="$(dirname "$0")/fixtures"

hooks/copilot-posttooluse.sh

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
#!/usr/bin/env bash
2+
# squeez Copilot CLI PostToolUse hook — tracks token usage per tool call
3+
SQUEEZ="$HOME/.claude/squeez/bin/squeez"
4+
[ ! -x "$SQUEEZ" ] && exit 0
5+
6+
export SQUEEZ_DIR="$HOME/.copilot/squeez"
7+
8+
input=$(cat)
9+
10+
tool=$(printf '%s' "$input" | python3 -c "
11+
import sys, json
12+
try:
13+
d = json.load(sys.stdin)
14+
print(d.get('tool_name', 'unknown'))
15+
except Exception:
16+
print('unknown')
17+
" 2>/dev/null || echo "unknown")
18+
19+
size=$(printf '%s' "$input" | python3 -c "
20+
import sys, json
21+
try:
22+
d = json.load(sys.stdin)
23+
content = d.get('tool_result', {})
24+
if isinstance(content, dict):
25+
content = str(content.get('content', ''))
26+
elif content is None:
27+
content = ''
28+
else:
29+
content = str(content)
30+
print(len(content))
31+
except Exception:
32+
print(0)
33+
" 2>/dev/null || echo 0)
34+
35+
"$SQUEEZ" track "$tool" "$size" 2>/dev/null || true

hooks/copilot-pretooluse.sh

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
#!/usr/bin/env bash
2+
# squeez Copilot CLI PreToolUse hook — compresses Bash tool output
3+
# Registered in ~/.copilot/settings.json (same format as Claude Code)
4+
set -euo pipefail
5+
6+
SQUEEZ="$HOME/.claude/squeez/bin/squeez"
7+
[ ! -x "$SQUEEZ" ] && exit 0
8+
9+
export SQUEEZ_DIR="$HOME/.copilot/squeez"
10+
11+
SQUEEZ_BIN="$SQUEEZ" python3 -c "
12+
import sys, json, os, shlex
13+
14+
data = sys.stdin.read()
15+
if not data.strip():
16+
sys.exit(0)
17+
18+
try:
19+
d = json.loads(data)
20+
except json.JSONDecodeError:
21+
sys.exit(0)
22+
23+
if d.get('tool_name') != 'Bash':
24+
sys.exit(0)
25+
26+
cmd = d.get('tool_input', {}).get('command')
27+
if cmd is None:
28+
sys.exit(0)
29+
30+
squeez = os.environ['SQUEEZ_BIN']
31+
32+
if cmd.startswith(squeez):
33+
sys.exit(0)
34+
35+
if cmd.startswith('--no-squeez '):
36+
d['tool_input']['command'] = cmd[len('--no-squeez '):]
37+
print(json.dumps({'hookSpecificOutput': {'permissionDecision': 'allow', 'updatedInput': d['tool_input']}}))
38+
sys.exit(0)
39+
40+
d['tool_input']['command'] = squeez + ' wrap ' + shlex.quote(cmd)
41+
print(json.dumps({'hookSpecificOutput': {'permissionDecision': 'allow', 'updatedInput': d['tool_input']}}))
42+
"

hooks/copilot-session-start.sh

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
#!/usr/bin/env bash
2+
# squeez Copilot CLI session-start integration
3+
# Initialises the session and injects memory into ~/.copilot/copilot-instructions.md
4+
# Run this once per session: add to shell RC or invoke manually.
5+
SQUEEZ="$HOME/.claude/squeez/bin/squeez"
6+
[ ! -x "$SQUEEZ" ] && exit 0
7+
8+
export SQUEEZ_DIR="$HOME/.copilot/squeez"
9+
mkdir -p "$SQUEEZ_DIR/sessions" "$SQUEEZ_DIR/memory"
10+
chmod 700 "$SQUEEZ_DIR" "$SQUEEZ_DIR/sessions" "$SQUEEZ_DIR/memory" 2>/dev/null || true
11+
12+
"$SQUEEZ" init --copilot

install.sh

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,8 +125,68 @@ with open(tmp, "w") as f:
125125
os.replace(tmp, path)
126126
EOF
127127

128+
echo "Installing Copilot CLI hooks..."
129+
COPILOT_SQUEEZ_DIR="$HOME/.copilot/squeez"
130+
mkdir -p "$COPILOT_SQUEEZ_DIR/bin" "$COPILOT_SQUEEZ_DIR/hooks" \
131+
"$COPILOT_SQUEEZ_DIR/sessions" "$COPILOT_SQUEEZ_DIR/memory"
132+
chmod 700 "$COPILOT_SQUEEZ_DIR" "$COPILOT_SQUEEZ_DIR/sessions" "$COPILOT_SQUEEZ_DIR/memory" 2>/dev/null || true
133+
134+
# Symlink the same binary so SQUEEZ_DIR-aware calls work
135+
ln -sf "$INSTALL_DIR/bin/squeez" "$COPILOT_SQUEEZ_DIR/bin/squeez" 2>/dev/null || \
136+
cp "$INSTALL_DIR/bin/squeez" "$COPILOT_SQUEEZ_DIR/bin/squeez"
137+
138+
curl -fsSL "$REPO_RAW/hooks/copilot-pretooluse.sh" -o "$INSTALL_DIR/hooks/copilot-pretooluse.sh"
139+
curl -fsSL "$REPO_RAW/hooks/copilot-session-start.sh" -o "$INSTALL_DIR/hooks/copilot-session-start.sh"
140+
curl -fsSL "$REPO_RAW/hooks/copilot-posttooluse.sh" -o "$INSTALL_DIR/hooks/copilot-posttooluse.sh"
141+
chmod +x "$INSTALL_DIR/hooks/copilot-pretooluse.sh" \
142+
"$INSTALL_DIR/hooks/copilot-session-start.sh" \
143+
"$INSTALL_DIR/hooks/copilot-posttooluse.sh"
144+
145+
# Seed Copilot instructions (writes ~/.copilot/copilot-instructions.md)
146+
"$INSTALL_DIR/bin/squeez" init --copilot 2>/dev/null || true
147+
148+
# Register hooks in ~/.copilot/settings.json (Copilot CLI hook format mirrors Claude Code)
149+
if [ -d "$HOME/.copilot" ]; then
150+
python3 - <<'COPILOT_EOF'
151+
import json, os, sys
152+
path = os.path.expanduser("~/.copilot/settings.json")
153+
settings = {}
154+
try:
155+
if os.path.exists(path):
156+
with open(path) as f:
157+
settings = json.load(f)
158+
except (json.JSONDecodeError, IOError) as e:
159+
print(f"Warning: could not read ~/.copilot/settings.json: {e}", file=sys.stderr)
160+
161+
if not isinstance(settings.get("PreToolUse"), list):
162+
settings["PreToolUse"] = []
163+
pre = {"matcher": "Bash", "hooks": [{"type": "command", "command": "bash ~/.claude/squeez/hooks/copilot-pretooluse.sh"}]}
164+
if not any("squeez" in str(h) for h in settings["PreToolUse"]):
165+
settings["PreToolUse"].append(pre)
166+
167+
if not isinstance(settings.get("SessionStart"), list):
168+
settings["SessionStart"] = []
169+
start = {"hooks": [{"type": "command", "command": "bash ~/.claude/squeez/hooks/copilot-session-start.sh"}]}
170+
if not any("squeez" in str(h) for h in settings["SessionStart"]):
171+
settings["SessionStart"].append(start)
172+
173+
if not isinstance(settings.get("PostToolUse"), list):
174+
settings["PostToolUse"] = []
175+
post = {"hooks": [{"type": "command", "command": "bash ~/.claude/squeez/hooks/copilot-posttooluse.sh"}]}
176+
if not any("squeez" in str(h) for h in settings["PostToolUse"]):
177+
settings["PostToolUse"].append(post)
178+
179+
tmp = path + ".tmp"
180+
with open(tmp, "w") as f:
181+
json.dump(settings, f, indent=2)
182+
os.replace(tmp, path)
183+
COPILOT_EOF
184+
fi
185+
128186
version=$("$INSTALL_DIR/bin/squeez" --version 2>/dev/null || echo "squeez")
129187
echo "$version installed."
130188
echo ""
131-
echo "Claude Code: Restart Claude Code to activate."
132-
echo "OpenCode: Restart OpenCode to activate the plugin (automatic Bash compression)."
189+
echo "Claude Code: Restart Claude Code to activate."
190+
echo "OpenCode: Restart OpenCode to activate the plugin (automatic Bash compression)."
191+
echo "Copilot CLI: Memory injected into ~/.copilot/copilot-instructions.md."
192+
echo " Restart Copilot CLI to activate hook-based bash compression."

src/commands/init.rs

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,77 @@ pub fn run() -> i32 {
1616
run_with_dirs(&sessions, &mem, &cfg)
1717
}
1818

19+
/// Entry point called from main.rs: `squeez init --copilot`
20+
/// Same as run() but also injects the memory banner into
21+
/// ~/.copilot/copilot-instructions.md so Copilot CLI picks it up
22+
/// at every session start (no hook system required).
23+
pub fn run_copilot() -> i32 {
24+
let home = std::env::var("HOME").unwrap_or_default();
25+
// Honour SQUEEZ_DIR override, default to ~/.copilot/squeez
26+
let base = std::env::var("SQUEEZ_DIR")
27+
.unwrap_or_else(|_| format!("{}/.copilot/squeez", home));
28+
let sessions = std::path::PathBuf::from(&base).join("sessions");
29+
let mem = std::path::PathBuf::from(&base).join("memory");
30+
let _ = std::fs::create_dir_all(&sessions);
31+
let _ = std::fs::create_dir_all(&mem);
32+
33+
// Load config from the copilot squeez dir
34+
let cfg = load_config_from(&base);
35+
36+
let code = run_with_dirs(&sessions, &mem, &cfg);
37+
38+
// Inject memory banner into Copilot CLI instructions file
39+
let summaries = memory::read_last_n(&mem, 3);
40+
inject_copilot_instructions(&home, &cfg, &summaries);
41+
42+
code
43+
}
44+
45+
fn load_config_from(base: &str) -> Config {
46+
let path = format!("{}/config.ini", base);
47+
std::fs::read_to_string(&path)
48+
.map(|s| Config::from_str(&s))
49+
.unwrap_or_default()
50+
}
51+
52+
/// Replaces the squeez block (<!-- squeez:start --> … <!-- squeez:end -->)
53+
/// in ~/.copilot/copilot-instructions.md, creating the file if absent.
54+
fn inject_copilot_instructions(home: &str, cfg: &Config, summaries: &[memory::Summary]) {
55+
let path = format!("{}/.copilot/copilot-instructions.md", home);
56+
let existing = std::fs::read_to_string(&path).unwrap_or_default();
57+
58+
let mut block = String::from("<!-- squeez:start -->\n");
59+
block.push_str("## squeez — session context\n");
60+
let budget_k = cfg.compact_threshold_tokens * 5 / 4 / 1000;
61+
block.push_str(&format!(
62+
"Context budget: ~{}K tokens | Compression: ON | Memory: ON\n",
63+
budget_k
64+
));
65+
for s in summaries {
66+
block.push_str(&format!("- {}\n", s.display_line()));
67+
}
68+
if summaries.is_empty() {
69+
block.push_str("- No prior sessions recorded yet.\n");
70+
}
71+
block.push_str("<!-- squeez:end -->\n");
72+
73+
// Strip previous squeez block if present
74+
let cleaned = if existing.contains("<!-- squeez:start -->") {
75+
let start = existing.find("<!-- squeez:start -->").unwrap_or(0);
76+
let end = existing
77+
.find("<!-- squeez:end -->")
78+
.map(|i| i + "<!-- squeez:end -->".len() + 1) // include newline
79+
.unwrap_or(start);
80+
format!("{}{}", &existing[..start], &existing[end.min(existing.len())..])
81+
} else {
82+
existing
83+
};
84+
85+
// Prepend the fresh block
86+
let contents = format!("{}\n{}", block, cleaned.trim_start());
87+
let _ = std::fs::write(&path, contents);
88+
}
89+
1990
/// Testable version with explicit directories.
2091
pub fn run_with_dirs(sessions_dir: &Path, memory_dir: &Path, config: &Config) -> i32 {
2192
// 1. Finalise previous session → memory (best-effort)

src/config.rs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,10 +78,13 @@ impl Config {
7878
}
7979

8080
pub fn load() -> Self {
81-
let path = format!(
82-
"{}/.claude/squeez/config.ini",
83-
std::env::var("HOME").unwrap_or_default()
84-
);
81+
let base = std::env::var("SQUEEZ_DIR").unwrap_or_else(|_| {
82+
format!(
83+
"{}/.claude/squeez",
84+
std::env::var("HOME").unwrap_or_default()
85+
)
86+
});
87+
let path = format!("{}/config.ini", base);
8588
std::fs::read_to_string(&path)
8689
.map(|s| Self::from_str(&s))
8790
.unwrap_or_default()

src/main.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,12 @@ fn main() {
2525
std::process::exit(exit_code);
2626
}
2727
Some("init") => {
28-
let exit_code = squeez::commands::init::run();
28+
let copilot = args.get(2).map(String::as_str) == Some("--copilot");
29+
let exit_code = if copilot {
30+
squeez::commands::init::run_copilot()
31+
} else {
32+
squeez::commands::init::run()
33+
};
2934
std::process::exit(exit_code);
3035
}
3136
Some("compact") => {

0 commit comments

Comments
 (0)