code-context-control 2.28.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cli/__init__.py +1 -0
- cli/_hook_utils.py +99 -0
- cli/c3.py +6152 -0
- cli/commands/__init__.py +1 -0
- cli/commands/common.py +312 -0
- cli/commands/parser.py +286 -0
- cli/docs.html +3178 -0
- cli/edits.html +878 -0
- cli/hook_auto_snapshot.py +142 -0
- cli/hook_c3_signal.py +61 -0
- cli/hook_c3read.py +116 -0
- cli/hook_edit_ledger.py +213 -0
- cli/hook_edit_unlock.py +170 -0
- cli/hook_filter.py +130 -0
- cli/hook_ghost_files.py +238 -0
- cli/hook_pretool_enforce.py +334 -0
- cli/hook_read.py +200 -0
- cli/hook_session_stats.py +62 -0
- cli/hook_terse_advisor.py +190 -0
- cli/hub.html +3764 -0
- cli/hub_server.py +1619 -0
- cli/mcp_proxy.py +428 -0
- cli/mcp_server.py +660 -0
- cli/server.py +2985 -0
- cli/tools/__init__.py +4 -0
- cli/tools/_helpers.py +65 -0
- cli/tools/agent.py +1165 -0
- cli/tools/compress.py +215 -0
- cli/tools/delegate.py +1184 -0
- cli/tools/edit.py +313 -0
- cli/tools/edits.py +118 -0
- cli/tools/filter.py +285 -0
- cli/tools/impact.py +163 -0
- cli/tools/memory.py +469 -0
- cli/tools/read.py +224 -0
- cli/tools/search.py +337 -0
- cli/tools/session.py +95 -0
- cli/tools/shell.py +193 -0
- cli/tools/status.py +306 -0
- cli/tools/validate.py +310 -0
- cli/ui/api.js +36 -0
- cli/ui/app.js +207 -0
- cli/ui/components/chat.js +758 -0
- cli/ui/components/dashboard.js +689 -0
- cli/ui/components/edits.js +220 -0
- cli/ui/components/instructions.js +481 -0
- cli/ui/components/memory.js +626 -0
- cli/ui/components/sessions.js +606 -0
- cli/ui/components/settings.js +1404 -0
- cli/ui/components/sidebar.js +156 -0
- cli/ui/icons.js +51 -0
- cli/ui/shared.js +119 -0
- cli/ui/theme.js +22 -0
- cli/ui.html +168 -0
- cli/ui_legacy.html +6797 -0
- cli/ui_nano.html +503 -0
- code_context_control-2.28.0.dist-info/METADATA +248 -0
- code_context_control-2.28.0.dist-info/RECORD +150 -0
- code_context_control-2.28.0.dist-info/WHEEL +5 -0
- code_context_control-2.28.0.dist-info/entry_points.txt +4 -0
- code_context_control-2.28.0.dist-info/licenses/LICENSE +201 -0
- code_context_control-2.28.0.dist-info/top_level.txt +5 -0
- core/__init__.py +75 -0
- core/config.py +269 -0
- core/ide.py +188 -0
- oracle/__init__.py +1 -0
- oracle/config.py +75 -0
- oracle/oracle.html +3900 -0
- oracle/oracle_server.py +663 -0
- oracle/services/__init__.py +1 -0
- oracle/services/c3_bridge.py +210 -0
- oracle/services/chat_engine.py +1103 -0
- oracle/services/chat_store.py +155 -0
- oracle/services/cross_memory.py +154 -0
- oracle/services/federated_graph.py +463 -0
- oracle/services/health_checker.py +117 -0
- oracle/services/insight_engine.py +307 -0
- oracle/services/memory_reader.py +106 -0
- oracle/services/memory_writer.py +182 -0
- oracle/services/ollama_bridge.py +332 -0
- oracle/services/project_scanner.py +87 -0
- oracle/services/review_agent.py +206 -0
- services/__init__.py +1 -0
- services/activity_log.py +93 -0
- services/agent_base.py +124 -0
- services/agents.py +1529 -0
- services/auto_memory.py +407 -0
- services/bench/__init__.py +6 -0
- services/bench/external/__init__.py +29 -0
- services/bench/external/aider_polyglot.py +405 -0
- services/bench/external/swe_bench.py +485 -0
- services/benchmark_dashboard.py +596 -0
- services/claude_md.py +785 -0
- services/compressor.py +592 -0
- services/context_snapshot.py +356 -0
- services/conversation_store.py +870 -0
- services/doc_index.py +537 -0
- services/e2e_benchmark.py +2884 -0
- services/e2e_evaluator.py +396 -0
- services/e2e_tasks.py +743 -0
- services/edit_ledger.py +459 -0
- services/embedding_index.py +341 -0
- services/error_reporting.py +123 -0
- services/file_memory.py +734 -0
- services/hub_service.py +585 -0
- services/indexer.py +712 -0
- services/memory.py +318 -0
- services/memory_consolidator.py +538 -0
- services/memory_graph.py +382 -0
- services/memory_grounder.py +304 -0
- services/memory_scorer.py +246 -0
- services/metrics.py +86 -0
- services/notifications.py +209 -0
- services/ollama_client.py +201 -0
- services/output_filter.py +488 -0
- services/parser.py +1238 -0
- services/project_manager.py +579 -0
- services/protocol.py +306 -0
- services/proxy_state.py +152 -0
- services/retrieval_broker.py +129 -0
- services/router.py +414 -0
- services/runtime.py +326 -0
- services/session_benchmark.py +1945 -0
- services/session_manager.py +1026 -0
- services/session_preloader.py +251 -0
- services/text_index.py +90 -0
- services/tool_classifier.py +176 -0
- services/transcript_index.py +340 -0
- services/validation_cache.py +155 -0
- services/vector_store.py +299 -0
- services/version_tracker.py +271 -0
- services/watcher.py +192 -0
- tui/__init__.py +0 -0
- tui/backend.py +59 -0
- tui/main.py +145 -0
- tui/screens/__init__.py +1 -0
- tui/screens/benchmark_view.py +109 -0
- tui/screens/claudemd_view.py +46 -0
- tui/screens/compress_view.py +52 -0
- tui/screens/index_view.py +74 -0
- tui/screens/init_view.py +82 -0
- tui/screens/mcp_view.py +73 -0
- tui/screens/optimize_view.py +41 -0
- tui/screens/pipe_view.py +46 -0
- tui/screens/projects_view.py +355 -0
- tui/screens/search_view.py +55 -0
- tui/screens/session_view.py +143 -0
- tui/screens/stats.py +158 -0
- tui/screens/ui_view.py +54 -0
- tui/theme.tcss +335 -0
services/protocol.py
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Compression Protocol
|
|
3
|
+
|
|
4
|
+
A custom encoding/decoding scheme for prompts and context that reduces token usage
|
|
5
|
+
by converting natural language into compressed shorthand.
|
|
6
|
+
|
|
7
|
+
Features:
|
|
8
|
+
- Action shorthand (READ -> R, FIX -> FX, CREATE -> CR, etc.)
|
|
9
|
+
- Path abbreviation
|
|
10
|
+
- Common phrase compression
|
|
11
|
+
- Project-specific dictionary building
|
|
12
|
+
- Reversible encoding
|
|
13
|
+
"""
|
|
14
|
+
import json
|
|
15
|
+
import re
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
from core import measure_savings
|
|
19
|
+
|
|
20
|
+
# Core action dictionary
|
|
21
|
+
ACTION_CODES = {
|
|
22
|
+
# File operations
|
|
23
|
+
"read": "R", "write": "W", "create": "CR", "delete": "DEL",
|
|
24
|
+
"edit": "ED", "modify": "MOD", "update": "UPD", "rename": "RN",
|
|
25
|
+
"move": "MV", "copy": "CP",
|
|
26
|
+
# Code operations
|
|
27
|
+
"fix": "FX", "debug": "DBG", "refactor": "RFT", "optimize": "OPT",
|
|
28
|
+
"test": "TST", "add": "ADD", "remove": "RM", "implement": "IMP",
|
|
29
|
+
"extract": "EXT", "inline": "INL", "wrap": "WRP",
|
|
30
|
+
# Analysis
|
|
31
|
+
"explain": "EXP", "analyze": "ANL", "review": "REV",
|
|
32
|
+
"find": "FND", "search": "SRCH", "list": "LST", "show": "SHW",
|
|
33
|
+
"compare": "CMP", "check": "CHK",
|
|
34
|
+
# Common qualifiers
|
|
35
|
+
"the": "", "this": "", "that": "", "a": "", "an": "",
|
|
36
|
+
"please": "", "can you": "", "could you": "", "would you": "",
|
|
37
|
+
"i want to": "", "i need to": "", "i'd like to": "",
|
|
38
|
+
"help me": "", "go ahead and": "",
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
# Common programming terms
|
|
42
|
+
TERM_CODES = {
|
|
43
|
+
"function": "fn", "variable": "var", "constant": "const",
|
|
44
|
+
"class": "cls", "method": "mth", "property": "prop",
|
|
45
|
+
"interface": "ifc", "type": "typ", "enum": "enm",
|
|
46
|
+
"component": "cmp", "module": "mod", "package": "pkg",
|
|
47
|
+
"import": "imp", "export": "exp", "default": "def",
|
|
48
|
+
"parameter": "param", "argument": "arg", "return": "ret",
|
|
49
|
+
"async": "asc", "await": "awt", "promise": "prom",
|
|
50
|
+
"error": "err", "exception": "exc", "warning": "warn",
|
|
51
|
+
"database": "db", "query": "qry", "schema": "sch",
|
|
52
|
+
"request": "req", "response": "res", "middleware": "mw",
|
|
53
|
+
"authentication": "auth", "authorization": "authz",
|
|
54
|
+
"configuration": "cfg", "environment": "env",
|
|
55
|
+
"typescript": "TS", "javascript": "JS", "python": "PY",
|
|
56
|
+
"react": "RCT", "node": "ND",
|
|
57
|
+
"line": "L", "file": "F", "directory": "D",
|
|
58
|
+
"string": "str", "number": "num", "boolean": "bool",
|
|
59
|
+
"array": "arr", "object": "obj", "null": "nil",
|
|
60
|
+
"undefined": "undef",
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
# Common phrase patterns
|
|
64
|
+
PHRASE_PATTERNS = [
|
|
65
|
+
(r"please read the file (.+?) and", r"R:\1"),
|
|
66
|
+
(r"can you (?:please )?look at (.+)", r"R:\1"),
|
|
67
|
+
(r"fix the (?:bug|error|issue) (?:in|on|at) (.+?)(?:\s+(?:where|on|at)\s+line\s+(\d+))?", r"FX:\1 L\2"),
|
|
68
|
+
(r"create a (?:new )?(.+?) (?:file|component|module) (?:called|named) (.+)", r"CR:\2.\1"),
|
|
69
|
+
(r"add (.+?) to (.+)", r"ADD:\1 IN:\2"),
|
|
70
|
+
(r"remove (.+?) from (.+)", r"RM:\1 FROM:\2"),
|
|
71
|
+
(r"refactor (.+?) to (.+)", r"RFT:\1 TO:\2"),
|
|
72
|
+
(r"move (.+?) to (.+)", r"MV:\1 TO:\2"),
|
|
73
|
+
(r"rename (.+?) to (.+)", r"RN:\1 TO:\2"),
|
|
74
|
+
(r"implement (.+?) in (.+)", r"IMP:\1 IN:\2"),
|
|
75
|
+
(r"there(?:'s| is) (?:a |an )?(?:bug|error|issue|problem) (?:in|with|on) (.+)", r"FX:\1"),
|
|
76
|
+
(r"on line (\d+)", r"L\1"),
|
|
77
|
+
(r"the (.+?) (?:is|are) (?:not working|broken|failing)", r"FX:\1"),
|
|
78
|
+
(r"(?:the |)(.+?) (?:doesn't|does not|isn't|is not) (?:work|working)", r"FX:\1"),
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
# Reverse lookup for decoding
|
|
82
|
+
REVERSE_ACTIONS = {v: k for k, v in ACTION_CODES.items() if v}
|
|
83
|
+
REVERSE_TERMS = {v: k for k, v in TERM_CODES.items()}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class CompressionProtocol:
|
|
87
|
+
"""Encode/decode natural language prompts to compressed shorthand."""
|
|
88
|
+
|
|
89
|
+
def __init__(self, project_path: str = "", custom_dict_path: str = ".c3/dictionary.json"):
|
|
90
|
+
self.project_path = Path(project_path) if project_path else Path(".")
|
|
91
|
+
self.dict_path = self.project_path / custom_dict_path
|
|
92
|
+
self.custom_dict = self._load_custom_dict()
|
|
93
|
+
self.path_aliases = {}
|
|
94
|
+
|
|
95
|
+
def encode(self, text: str) -> dict:
|
|
96
|
+
"""Encode natural language to compressed format."""
|
|
97
|
+
original = text
|
|
98
|
+
compressed = text.lower().strip()
|
|
99
|
+
|
|
100
|
+
# Step 1: Apply phrase patterns
|
|
101
|
+
for pattern, replacement in PHRASE_PATTERNS:
|
|
102
|
+
compressed = re.sub(pattern, replacement, compressed, flags=re.IGNORECASE)
|
|
103
|
+
|
|
104
|
+
# Step 2: Remove filler words
|
|
105
|
+
for word in ["please", "can you", "could you", "would you", "i want to",
|
|
106
|
+
"i need to", "i'd like to", "help me", "go ahead and",
|
|
107
|
+
"the", "this", "that"]:
|
|
108
|
+
compressed = re.sub(rf'\b{re.escape(word)}\b', '', compressed, flags=re.IGNORECASE)
|
|
109
|
+
|
|
110
|
+
# Step 3: Apply action codes
|
|
111
|
+
for word, code in ACTION_CODES.items():
|
|
112
|
+
if code: # Skip empty replacements (already removed filler words)
|
|
113
|
+
compressed = re.sub(rf'\b{re.escape(word)}\b', code, compressed, flags=re.IGNORECASE)
|
|
114
|
+
|
|
115
|
+
# Step 4: Apply term codes
|
|
116
|
+
for word, code in TERM_CODES.items():
|
|
117
|
+
compressed = re.sub(rf'\b{re.escape(word)}\b', code, compressed, flags=re.IGNORECASE)
|
|
118
|
+
|
|
119
|
+
# Step 5: Apply custom dictionary
|
|
120
|
+
for word, code in self.custom_dict.items():
|
|
121
|
+
compressed = re.sub(rf'\b{re.escape(word)}\b', code, compressed, flags=re.IGNORECASE)
|
|
122
|
+
|
|
123
|
+
# Step 6: Compress paths
|
|
124
|
+
compressed = self._compress_paths(compressed)
|
|
125
|
+
|
|
126
|
+
# Step 7: Clean up whitespace
|
|
127
|
+
compressed = re.sub(r'\s+', ' ', compressed).strip()
|
|
128
|
+
|
|
129
|
+
savings = measure_savings(original, compressed)
|
|
130
|
+
savings["original"] = original
|
|
131
|
+
savings["compressed"] = compressed
|
|
132
|
+
|
|
133
|
+
return savings
|
|
134
|
+
|
|
135
|
+
def decode(self, compressed: str) -> str:
|
|
136
|
+
"""Decode compressed format back to readable text."""
|
|
137
|
+
text = compressed
|
|
138
|
+
|
|
139
|
+
# Decode action codes (R: -> Read file)
|
|
140
|
+
action_patterns = {
|
|
141
|
+
r'\bR:': "Read file ",
|
|
142
|
+
r'\bW:': "Write to ",
|
|
143
|
+
r'\bCR:': "Create ",
|
|
144
|
+
r'\bDEL:': "Delete ",
|
|
145
|
+
r'\bED:': "Edit ",
|
|
146
|
+
r'\bFX:': "Fix ",
|
|
147
|
+
r'\bDBG:': "Debug ",
|
|
148
|
+
r'\bRFT:': "Refactor ",
|
|
149
|
+
r'\bOPT:': "Optimize ",
|
|
150
|
+
r'\bTST:': "Test ",
|
|
151
|
+
r'\bADD:': "Add ",
|
|
152
|
+
r'\bRM:': "Remove ",
|
|
153
|
+
r'\bIMP:': "Implement ",
|
|
154
|
+
r'\bEXP:': "Explain ",
|
|
155
|
+
r'\bANL:': "Analyze ",
|
|
156
|
+
r'\bREV:': "Review ",
|
|
157
|
+
r'\bFND:': "Find ",
|
|
158
|
+
r'\bSRCH:': "Search for ",
|
|
159
|
+
r'\bMV:': "Move ",
|
|
160
|
+
r'\bRN:': "Rename ",
|
|
161
|
+
r'\bCMP:': "Compare ",
|
|
162
|
+
r'\bCHK:': "Check ",
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
for pattern, replacement in action_patterns.items():
|
|
166
|
+
text = re.sub(pattern, replacement, text)
|
|
167
|
+
|
|
168
|
+
# Decode term codes
|
|
169
|
+
for code, term in REVERSE_TERMS.items():
|
|
170
|
+
text = re.sub(rf'\b{re.escape(code)}\b', term, text)
|
|
171
|
+
|
|
172
|
+
# Decode line references
|
|
173
|
+
text = re.sub(r'\bL(\d+)', r'on line \1', text)
|
|
174
|
+
|
|
175
|
+
# Decode modifiers
|
|
176
|
+
text = re.sub(r'\bIN:', 'in ', text)
|
|
177
|
+
text = re.sub(r'\bTO:', 'to ', text)
|
|
178
|
+
text = re.sub(r'\bFROM:', 'from ', text)
|
|
179
|
+
|
|
180
|
+
# Clean up
|
|
181
|
+
text = re.sub(r'\s+', ' ', text).strip()
|
|
182
|
+
text = text[0].upper() + text[1:] if text else text
|
|
183
|
+
|
|
184
|
+
return text
|
|
185
|
+
|
|
186
|
+
def _compress_paths(self, text: str) -> str:
|
|
187
|
+
"""Compress file paths using aliases."""
|
|
188
|
+
# Auto-detect common path prefixes
|
|
189
|
+
path_pattern = r'(?:src|lib|app|components|pages|utils|hooks|services|api|config)(?:/\w+)+'
|
|
190
|
+
paths = re.findall(path_pattern, text)
|
|
191
|
+
|
|
192
|
+
for path in paths:
|
|
193
|
+
parts = path.split('/')
|
|
194
|
+
if len(parts) > 2:
|
|
195
|
+
# Keep first and last parts
|
|
196
|
+
compressed_path = f"{parts[0]}/../{parts[-1]}"
|
|
197
|
+
text = text.replace(path, compressed_path)
|
|
198
|
+
|
|
199
|
+
return text
|
|
200
|
+
|
|
201
|
+
def _load_custom_dict(self) -> dict:
|
|
202
|
+
"""Load project-specific custom dictionary."""
|
|
203
|
+
if self.dict_path.exists():
|
|
204
|
+
try:
|
|
205
|
+
with open(self.dict_path, encoding='utf-8') as f:
|
|
206
|
+
return json.load(f)
|
|
207
|
+
except Exception:
|
|
208
|
+
pass
|
|
209
|
+
return {}
|
|
210
|
+
|
|
211
|
+
def save_custom_dict(self):
|
|
212
|
+
"""Save custom dictionary to disk."""
|
|
213
|
+
self.dict_path.parent.mkdir(parents=True, exist_ok=True)
|
|
214
|
+
with open(self.dict_path, 'w', encoding='utf-8') as f:
|
|
215
|
+
json.dump(self.custom_dict, f, indent=2)
|
|
216
|
+
|
|
217
|
+
def add_custom_term(self, term: str, code: str):
|
|
218
|
+
"""Add a project-specific term to the dictionary."""
|
|
219
|
+
self.custom_dict[term.lower()] = code
|
|
220
|
+
self.save_custom_dict()
|
|
221
|
+
|
|
222
|
+
def build_project_dictionary(self) -> dict:
|
|
223
|
+
"""Auto-build a project-specific dictionary from codebase analysis."""
|
|
224
|
+
if not self.project_path.exists():
|
|
225
|
+
return {}
|
|
226
|
+
|
|
227
|
+
# Find commonly used terms in the project
|
|
228
|
+
term_freq = {}
|
|
229
|
+
skip_dirs = {'node_modules', '.git', '__pycache__', '.c3', 'venv'}
|
|
230
|
+
code_exts = {'.py', '.js', '.ts', '.tsx', '.jsx', '.r', '.R'}
|
|
231
|
+
|
|
232
|
+
for fpath in self.project_path.rglob('*'):
|
|
233
|
+
if not fpath.is_file() or fpath.suffix not in code_exts:
|
|
234
|
+
continue
|
|
235
|
+
if any(skip in fpath.parts for skip in skip_dirs):
|
|
236
|
+
continue
|
|
237
|
+
|
|
238
|
+
try:
|
|
239
|
+
content = fpath.read_text(errors='replace')
|
|
240
|
+
except Exception:
|
|
241
|
+
continue
|
|
242
|
+
|
|
243
|
+
# Extract identifiers
|
|
244
|
+
identifiers = re.findall(r'\b[a-zA-Z_]\w{5,}\b', content)
|
|
245
|
+
for ident in identifiers:
|
|
246
|
+
lower = ident.lower()
|
|
247
|
+
if lower not in ACTION_CODES and lower not in TERM_CODES:
|
|
248
|
+
term_freq[lower] = term_freq.get(lower, 0) + 1
|
|
249
|
+
|
|
250
|
+
# Generate codes for frequent terms
|
|
251
|
+
frequent = sorted(term_freq.items(), key=lambda x: x[1], reverse=True)[:30]
|
|
252
|
+
new_entries = {}
|
|
253
|
+
|
|
254
|
+
for term, freq in frequent:
|
|
255
|
+
if freq >= 5: # Only for terms appearing 5+ times
|
|
256
|
+
# Generate abbreviation
|
|
257
|
+
if len(term) > 6:
|
|
258
|
+
code = term[:3].upper()
|
|
259
|
+
# Ensure uniqueness
|
|
260
|
+
suffix = 1
|
|
261
|
+
while code in TERM_CODES.values() or code in new_entries.values():
|
|
262
|
+
code = term[:3].upper() + str(suffix)
|
|
263
|
+
suffix += 1
|
|
264
|
+
new_entries[term] = code
|
|
265
|
+
|
|
266
|
+
# Merge with existing custom dict
|
|
267
|
+
self.custom_dict.update(new_entries)
|
|
268
|
+
self.save_custom_dict()
|
|
269
|
+
|
|
270
|
+
return new_entries
|
|
271
|
+
|
|
272
|
+
def get_protocol_header(self) -> str:
|
|
273
|
+
"""
|
|
274
|
+
Generate a compression protocol header to include in system prompt.
|
|
275
|
+
This tells Claude how to interpret compressed messages.
|
|
276
|
+
"""
|
|
277
|
+
header = """# C3 Compression Protocol
|
|
278
|
+
When you see compressed shorthand, decode using:
|
|
279
|
+
## Actions: R=Read W=Write CR=Create FX=Fix DBG=Debug RFT=Refactor OPT=Optimize TST=Test ADD=Add RM=Remove IMP=Implement
|
|
280
|
+
## Modifiers: L=Line F=File D=Directory IN=in TO=to FROM=from
|
|
281
|
+
## Terms: fn=function cls=class cmp=component mod=module cfg=config auth=authentication db=database
|
|
282
|
+
## Format: ACTION:target [MODIFIER:value] [context]
|
|
283
|
+
## Example: "FX:src/auth.ts L47 TS err missing onClick prop" = "Fix the TypeScript error on line 47 of src/auth.ts where the onClick prop is missing"
|
|
284
|
+
"""
|
|
285
|
+
|
|
286
|
+
# Add custom dictionary if exists
|
|
287
|
+
if self.custom_dict:
|
|
288
|
+
custom_section = "## Project-specific: " + ' '.join(
|
|
289
|
+
f"{v}={k}" for k, v in list(self.custom_dict.items())[:20]
|
|
290
|
+
)
|
|
291
|
+
header += custom_section + "\n"
|
|
292
|
+
|
|
293
|
+
return header
|
|
294
|
+
|
|
295
|
+
def batch_encode(self, texts: list) -> list:
|
|
296
|
+
"""Encode multiple texts at once."""
|
|
297
|
+
return [self.encode(t) for t in texts]
|
|
298
|
+
|
|
299
|
+
def get_stats(self) -> dict:
|
|
300
|
+
"""Get compression protocol statistics."""
|
|
301
|
+
return {
|
|
302
|
+
"built_in_actions": len(ACTION_CODES),
|
|
303
|
+
"built_in_terms": len(TERM_CODES),
|
|
304
|
+
"custom_terms": len(self.custom_dict),
|
|
305
|
+
"total_codes": len(ACTION_CODES) + len(TERM_CODES) + len(self.custom_dict),
|
|
306
|
+
}
|
services/proxy_state.py
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""Sliding window state tracker for the MCP proxy.
|
|
2
|
+
|
|
3
|
+
Maintains rolling conversation context: recent tool calls, files, decisions,
|
|
4
|
+
and detected goal. Generates a compact context line (~50 tokens) that gets
|
|
5
|
+
injected into tool responses so Claude retains state awareness across turns.
|
|
6
|
+
"""
|
|
7
|
+
import re
|
|
8
|
+
from collections import deque
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ProxyState:
|
|
12
|
+
"""Rolling conversation state tracker."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, window_size: int = 10):
|
|
15
|
+
self.tool_calls: deque = deque(maxlen=window_size)
|
|
16
|
+
self.recent_files: deque = deque(maxlen=5)
|
|
17
|
+
self.recent_decisions: deque = deque(maxlen=3)
|
|
18
|
+
self.current_goal: str = ""
|
|
19
|
+
|
|
20
|
+
# ── Recording ──────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
def record_tool_call(self, tool_name: str, args: dict,
|
|
23
|
+
response_text: str = "") -> None:
|
|
24
|
+
"""Record a tool call with a 1-line summary."""
|
|
25
|
+
summary = self._summarize_call(tool_name, args, response_text)
|
|
26
|
+
self.tool_calls.append({"name": tool_name, "summary": summary})
|
|
27
|
+
|
|
28
|
+
# Extract file paths from args
|
|
29
|
+
self._extract_files(args)
|
|
30
|
+
|
|
31
|
+
# Detect decisions from session_log
|
|
32
|
+
if tool_name == "c3_session_log" and args.get("event_type") == "decision":
|
|
33
|
+
decision = args.get("data", "")[:80]
|
|
34
|
+
if decision:
|
|
35
|
+
self.recent_decisions.append(decision)
|
|
36
|
+
|
|
37
|
+
# Update goal from tool args
|
|
38
|
+
self._detect_goal(tool_name, args)
|
|
39
|
+
|
|
40
|
+
def record_user_text(self, text: str) -> None:
|
|
41
|
+
"""Extract goal hints from user/tool text."""
|
|
42
|
+
# Look for intent patterns
|
|
43
|
+
patterns = [
|
|
44
|
+
(r"(?:fix|debug|resolve)\s+(.{5,40})", "fix"),
|
|
45
|
+
(r"(?:add|implement|create)\s+(.{5,40})", "add"),
|
|
46
|
+
(r"(?:refactor|clean|simplify)\s+(.{5,40})", "refactor"),
|
|
47
|
+
(r"(?:find|search|look\s+for)\s+(.{5,40})", "find"),
|
|
48
|
+
(r"(?:understand|explain|how\s+does)\s+(.{5,40})", "understand"),
|
|
49
|
+
]
|
|
50
|
+
for pattern, verb in patterns:
|
|
51
|
+
m = re.search(pattern, text, re.IGNORECASE)
|
|
52
|
+
if m:
|
|
53
|
+
self.current_goal = f"{verb} {m.group(1).strip()}"
|
|
54
|
+
break
|
|
55
|
+
|
|
56
|
+
# ── Output ─────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
def get_context_line(self) -> str:
|
|
59
|
+
"""Generate compact context summary for injection."""
|
|
60
|
+
parts = []
|
|
61
|
+
|
|
62
|
+
# Last tool calls
|
|
63
|
+
if self.tool_calls:
|
|
64
|
+
recent = list(self.tool_calls)[-3:]
|
|
65
|
+
call_strs = [c["summary"] for c in recent]
|
|
66
|
+
parts.append(f"Last: {', '.join(call_strs)}")
|
|
67
|
+
|
|
68
|
+
# Current goal
|
|
69
|
+
if self.current_goal:
|
|
70
|
+
parts.append(f"Goal: {self.current_goal}")
|
|
71
|
+
|
|
72
|
+
# Recent files
|
|
73
|
+
if self.recent_files:
|
|
74
|
+
files = [f.split("/")[-1] for f in self.recent_files]
|
|
75
|
+
parts.append(f"Files: {', '.join(files)}")
|
|
76
|
+
|
|
77
|
+
if not parts:
|
|
78
|
+
return ""
|
|
79
|
+
return f"\n[Context: {'. '.join(parts)}]"
|
|
80
|
+
|
|
81
|
+
def get_recent_tool_names(self) -> list[str]:
|
|
82
|
+
"""Return names of recent tool calls for classifier input."""
|
|
83
|
+
return [c["name"] for c in self.tool_calls]
|
|
84
|
+
|
|
85
|
+
def get_recent_text(self) -> str:
|
|
86
|
+
"""Return recent context text for classifier keyword matching."""
|
|
87
|
+
parts = []
|
|
88
|
+
if self.current_goal:
|
|
89
|
+
parts.append(self.current_goal)
|
|
90
|
+
for c in self.tool_calls:
|
|
91
|
+
parts.append(c["summary"])
|
|
92
|
+
parts.extend(self.recent_decisions)
|
|
93
|
+
return " ".join(parts)
|
|
94
|
+
|
|
95
|
+
# ── Internal ───────────────────────────────────────────
|
|
96
|
+
|
|
97
|
+
def _summarize_call(self, tool_name: str, args: dict,
|
|
98
|
+
response_text: str) -> str:
|
|
99
|
+
"""Create a compact 1-line summary of a tool call."""
|
|
100
|
+
short_name = tool_name.replace("c3_", "")
|
|
101
|
+
|
|
102
|
+
# Summarize based on tool type
|
|
103
|
+
if tool_name == "c3_search":
|
|
104
|
+
query = args.get("query", "")[:30]
|
|
105
|
+
# Count results from response
|
|
106
|
+
count = response_text.count("## ") if response_text else "?"
|
|
107
|
+
return f"{short_name}({query!r}, {count} results)"
|
|
108
|
+
|
|
109
|
+
elif tool_name == "c3_compress":
|
|
110
|
+
fp = args.get("file_path", "").split("/")[-1]
|
|
111
|
+
return f"{short_name}({fp})"
|
|
112
|
+
|
|
113
|
+
elif tool_name in ("c3_remember", "c3_recall", "c3_memory_query"):
|
|
114
|
+
text = args.get("fact", args.get("query", ""))[:30]
|
|
115
|
+
return f"{short_name}({text!r})"
|
|
116
|
+
|
|
117
|
+
elif tool_name == "c3_session_log":
|
|
118
|
+
etype = args.get("event_type", "")
|
|
119
|
+
data = args.get("data", "")[:20]
|
|
120
|
+
return f"{short_name}({etype}: {data})"
|
|
121
|
+
|
|
122
|
+
elif tool_name in ("c3_extract", "c3_filter"):
|
|
123
|
+
fp = args.get("file_path", "").split("/")[-1]
|
|
124
|
+
return f"{short_name}({fp})"
|
|
125
|
+
|
|
126
|
+
else:
|
|
127
|
+
# Generic: show first string arg
|
|
128
|
+
for v in args.values():
|
|
129
|
+
if isinstance(v, str) and len(v) > 2:
|
|
130
|
+
return f"{short_name}({v[:25]})"
|
|
131
|
+
return f"{short_name}()"
|
|
132
|
+
|
|
133
|
+
def _extract_files(self, args: dict) -> None:
|
|
134
|
+
"""Extract file paths from tool args."""
|
|
135
|
+
for key in ("file_path", "path"):
|
|
136
|
+
val = args.get(key)
|
|
137
|
+
if val and isinstance(val, str):
|
|
138
|
+
# Normalize and deduplicate
|
|
139
|
+
if val not in self.recent_files:
|
|
140
|
+
self.recent_files.append(val)
|
|
141
|
+
|
|
142
|
+
def _detect_goal(self, tool_name: str, args: dict) -> None:
|
|
143
|
+
"""Infer user goal from tool usage patterns."""
|
|
144
|
+
if tool_name == "c3_search":
|
|
145
|
+
query = args.get("query", "")
|
|
146
|
+
if query and not self.current_goal:
|
|
147
|
+
self.current_goal = f"investigate {query[:40]}"
|
|
148
|
+
elif tool_name == "c3_session_log":
|
|
149
|
+
if args.get("event_type") == "decision":
|
|
150
|
+
data = args.get("data", "")[:40]
|
|
151
|
+
if data:
|
|
152
|
+
self.current_goal = data
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""Unified retrieval broker across facts, conversations, files, sessions, and snapshots."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from services.text_index import TextIndex
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class MemoryRetrievalBroker:
|
|
12
|
+
"""Normalizes retrieval across the project's memory sources."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, project_path: str, memory_store, conversation_store=None, file_memory=None, snapshots=None):
|
|
15
|
+
self.project_path = Path(project_path)
|
|
16
|
+
self.memory_store = memory_store
|
|
17
|
+
self.conversation_store = conversation_store
|
|
18
|
+
self.file_memory = file_memory
|
|
19
|
+
self.snapshots = snapshots
|
|
20
|
+
self._session_index = TextIndex()
|
|
21
|
+
self._session_meta = {}
|
|
22
|
+
self._session_dirty = True
|
|
23
|
+
|
|
24
|
+
def mark_sessions_dirty(self):
|
|
25
|
+
self._session_dirty = True
|
|
26
|
+
|
|
27
|
+
def search(self, query: str, top_k: int = 5) -> dict:
|
|
28
|
+
self._session_dirty = True
|
|
29
|
+
self._ensure_session_index()
|
|
30
|
+
|
|
31
|
+
fact_results = self.memory_store.recall(query, top_k=top_k)
|
|
32
|
+
conversation_results = self.conversation_store.search(query, limit=top_k) if self.conversation_store else []
|
|
33
|
+
file_results = self.file_memory.search(query, top_k=top_k) if self.file_memory else []
|
|
34
|
+
snapshot_results = self.snapshots.search(query, top_k=top_k) if self.snapshots else []
|
|
35
|
+
|
|
36
|
+
session_hits = []
|
|
37
|
+
for session_id, score in self._session_index.search(query, top_k=top_k):
|
|
38
|
+
meta = self._session_meta.get(session_id)
|
|
39
|
+
if not meta:
|
|
40
|
+
continue
|
|
41
|
+
session_hits.append({**meta, "score": round(score, 4)})
|
|
42
|
+
|
|
43
|
+
merged = []
|
|
44
|
+
for fact in fact_results:
|
|
45
|
+
merged.append({
|
|
46
|
+
"kind": "fact",
|
|
47
|
+
"id": fact["id"],
|
|
48
|
+
"title": fact.get("category", "fact"),
|
|
49
|
+
"text": fact.get("fact", ""),
|
|
50
|
+
"score": float(fact.get("score", 0.0)),
|
|
51
|
+
"payload": fact,
|
|
52
|
+
})
|
|
53
|
+
for convo in conversation_results:
|
|
54
|
+
merged.append({
|
|
55
|
+
"kind": "conversation",
|
|
56
|
+
"id": convo["turn_key"],
|
|
57
|
+
"title": convo.get("session_title", convo.get("session_id", "")),
|
|
58
|
+
"text": convo.get("snippet") or convo.get("text", ""),
|
|
59
|
+
"score": float(convo.get("score", 0.0)),
|
|
60
|
+
"payload": convo,
|
|
61
|
+
})
|
|
62
|
+
for session in session_hits:
|
|
63
|
+
merged.append({
|
|
64
|
+
"kind": "session",
|
|
65
|
+
"id": session["session_id"],
|
|
66
|
+
"title": session.get("summary") or session.get("session_id", ""),
|
|
67
|
+
"text": session.get("summary", ""),
|
|
68
|
+
"score": float(session.get("score", 0.0)),
|
|
69
|
+
"payload": session,
|
|
70
|
+
})
|
|
71
|
+
for file_hit in file_results:
|
|
72
|
+
merged.append({
|
|
73
|
+
"kind": "file",
|
|
74
|
+
"id": file_hit["path"],
|
|
75
|
+
"title": file_hit["path"],
|
|
76
|
+
"text": file_hit.get("summary") or "",
|
|
77
|
+
"score": float(file_hit.get("score", 0.0)),
|
|
78
|
+
"payload": file_hit,
|
|
79
|
+
})
|
|
80
|
+
for snap in snapshot_results:
|
|
81
|
+
merged.append({
|
|
82
|
+
"kind": "snapshot",
|
|
83
|
+
"id": snap["snapshot_id"],
|
|
84
|
+
"title": snap.get("task_description", ""),
|
|
85
|
+
"text": snap.get("task_description", ""),
|
|
86
|
+
"score": float(snap.get("score", 0.0)),
|
|
87
|
+
"payload": snap,
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
merged.sort(key=lambda item: item["score"], reverse=True)
|
|
91
|
+
return {
|
|
92
|
+
"facts": fact_results,
|
|
93
|
+
"conversations": conversation_results[:top_k],
|
|
94
|
+
"sessions": session_hits[:top_k],
|
|
95
|
+
"files": file_results[:top_k],
|
|
96
|
+
"snapshots": snapshot_results[:top_k],
|
|
97
|
+
"results": merged[:top_k * 3],
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
def _ensure_session_index(self):
|
|
101
|
+
if not self._session_dirty:
|
|
102
|
+
return
|
|
103
|
+
session_dir = self.project_path / ".c3" / "sessions"
|
|
104
|
+
docs = {}
|
|
105
|
+
meta = {}
|
|
106
|
+
if session_dir.exists():
|
|
107
|
+
for path in sorted(session_dir.glob("session_*.json"), reverse=True):
|
|
108
|
+
try:
|
|
109
|
+
with open(path, encoding="utf-8") as handle:
|
|
110
|
+
session = json.load(handle)
|
|
111
|
+
except Exception:
|
|
112
|
+
continue
|
|
113
|
+
session_id = session.get("id") or session.get("session_id")
|
|
114
|
+
if not session_id:
|
|
115
|
+
continue
|
|
116
|
+
text_parts = [session.get("description", ""), session.get("summary", "")]
|
|
117
|
+
for decision in session.get("decisions", []):
|
|
118
|
+
text_parts.append(decision.get("decision", ""))
|
|
119
|
+
text_parts.append(decision.get("reasoning", ""))
|
|
120
|
+
text_parts.extend(session.get("context_notes", []))
|
|
121
|
+
docs[session_id] = " ".join(part for part in text_parts if part)
|
|
122
|
+
meta[session_id] = {
|
|
123
|
+
"session_id": session_id,
|
|
124
|
+
"started": session.get("started", ""),
|
|
125
|
+
"summary": session.get("summary", ""),
|
|
126
|
+
}
|
|
127
|
+
self._session_index.rebuild(docs)
|
|
128
|
+
self._session_meta = meta
|
|
129
|
+
self._session_dirty = False
|