agentcache-core 0.9.9__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.
- agentcache/__init__.py +29 -0
- agentcache/app.py +312 -0
- agentcache/cli.py +346 -0
- agentcache/connect.py +724 -0
- agentcache/core/__init__.py +19 -0
- agentcache/core/audit_log.py +104 -0
- agentcache/core/config.py +51 -0
- agentcache/core/context_builder.py +209 -0
- agentcache/core/graph.py +120 -0
- agentcache/core/image_store.py +111 -0
- agentcache/core/infer.py +142 -0
- agentcache/core/kv_scopes.py +86 -0
- agentcache/core/lessons.py +242 -0
- agentcache/core/llm.py +596 -0
- agentcache/core/memory_store.py +207 -0
- agentcache/core/observation_store.py +576 -0
- agentcache/core/privacy.py +34 -0
- agentcache/core/project_profile.py +625 -0
- agentcache/core/search_service.py +444 -0
- agentcache/core/session_store.py +382 -0
- agentcache/core/slots.py +504 -0
- agentcache/db.py +359 -0
- agentcache/import_data.py +86 -0
- agentcache/legacy.py +94 -0
- agentcache/mcp_stdio.py +141 -0
- agentcache/py.typed +0 -0
- agentcache/replay_import.py +680 -0
- agentcache/routes/__init__.py +29 -0
- agentcache/routes/_deps.py +46 -0
- agentcache/routes/auth.py +68 -0
- agentcache/routes/graph.py +81 -0
- agentcache/routes/health.py +149 -0
- agentcache/routes/mcp.py +614 -0
- agentcache/routes/memories.py +116 -0
- agentcache/routes/migration.py +32 -0
- agentcache/routes/observations.py +253 -0
- agentcache/routes/search.py +80 -0
- agentcache/search.py +935 -0
- agentcache/storage/__init__.py +29 -0
- agentcache/storage/images.py +102 -0
- agentcache/storage/paths.py +116 -0
- agentcache/storage/scopes.py +9 -0
- agentcache/viewer/favicon.svg +1 -0
- agentcache/viewer/index.html +4235 -0
- agentcache/viewer_helpers.py +61 -0
- agentcache/workers.py +192 -0
- agentcache_core-0.9.9.dist-info/METADATA +194 -0
- agentcache_core-0.9.9.dist-info/RECORD +52 -0
- agentcache_core-0.9.9.dist-info/WHEEL +5 -0
- agentcache_core-0.9.9.dist-info/entry_points.txt +2 -0
- agentcache_core-0.9.9.dist-info/licenses/LICENSE +190 -0
- agentcache_core-0.9.9.dist-info/top_level.txt +1 -0
agentcache/connect.py
ADDED
|
@@ -0,0 +1,724 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import argparse
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import shutil
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
# Helper functions for connect module
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_home_dir():
|
|
12
|
+
return os.path.expanduser("~")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_appdata_dir():
|
|
16
|
+
return os.environ.get("APPDATA") or os.environ.get("LOCALAPPDATA") or get_home_dir()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def read_json_safe(path):
|
|
20
|
+
if not os.path.exists(path):
|
|
21
|
+
return {}
|
|
22
|
+
try:
|
|
23
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
24
|
+
return json.load(f)
|
|
25
|
+
except Exception:
|
|
26
|
+
return {}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def write_json_atomic(path, data):
|
|
30
|
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
31
|
+
temp_path = f"{path}.tmp"
|
|
32
|
+
with open(temp_path, "w", encoding="utf-8") as f:
|
|
33
|
+
json.dump(data, f, indent=2)
|
|
34
|
+
f.write("\n")
|
|
35
|
+
# Atomic rename
|
|
36
|
+
shutil.move(temp_path, path)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def backup_file(path, prefix, ext="json"):
|
|
40
|
+
if not os.path.exists(path):
|
|
41
|
+
return None
|
|
42
|
+
backup_path = f"{path}.{prefix}.backup-{int(os.path.getmtime(path))}.{ext}"
|
|
43
|
+
shutil.copy2(path, backup_path)
|
|
44
|
+
return backup_path
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def get_plugin_root():
|
|
48
|
+
# connect.py resides in src/agentcache/, plugin/ is in the parent of src/
|
|
49
|
+
src_dir = os.path.dirname(os.path.abspath(__file__))
|
|
50
|
+
project_root = os.path.dirname(os.path.dirname(src_dir))
|
|
51
|
+
plugin_path = os.path.join(project_root, "plugin")
|
|
52
|
+
if os.path.exists(os.path.join(plugin_path, "scripts")):
|
|
53
|
+
return plugin_path
|
|
54
|
+
raise RuntimeError("Could not find plugin root directory.")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def get_mcp_stdio_path():
|
|
58
|
+
src_dir = os.path.dirname(os.path.abspath(__file__))
|
|
59
|
+
mcp_path = os.path.join(src_dir, "mcp_stdio.py")
|
|
60
|
+
if os.path.exists(mcp_path):
|
|
61
|
+
return mcp_path
|
|
62
|
+
raise RuntimeError("Could not find mcp_stdio.py.")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def build_merged_hooks(existing_hooks, plugin_root, manifest_filename="hooks.json"):
|
|
66
|
+
manifest_path = os.path.join(plugin_root, "hooks", manifest_filename)
|
|
67
|
+
with open(manifest_path, "r", encoding="utf-8") as f:
|
|
68
|
+
ours = json.load(f)
|
|
69
|
+
|
|
70
|
+
# Normalize paths for comparison
|
|
71
|
+
normalized_scripts_dir = os.path.join(plugin_root, "scripts").replace("\\", "/")
|
|
72
|
+
|
|
73
|
+
# Clean existing agentmemory hooks
|
|
74
|
+
cleaned_hooks = {}
|
|
75
|
+
if existing_hooks and "hooks" in existing_hooks:
|
|
76
|
+
for event, entries in existing_hooks["hooks"].items():
|
|
77
|
+
kept = []
|
|
78
|
+
for entry in entries:
|
|
79
|
+
is_ours = False
|
|
80
|
+
for handler in entry.get("hooks", []):
|
|
81
|
+
cmd = handler.get("command", "").replace("\\", "/")
|
|
82
|
+
if normalized_scripts_dir in cmd:
|
|
83
|
+
is_ours = True
|
|
84
|
+
break
|
|
85
|
+
if not is_ours:
|
|
86
|
+
kept.append(entry)
|
|
87
|
+
if kept:
|
|
88
|
+
cleaned_hooks[event] = kept
|
|
89
|
+
|
|
90
|
+
# Add ours
|
|
91
|
+
for event, entries in ours.get("hooks", {}).items():
|
|
92
|
+
resolved_entries = []
|
|
93
|
+
for entry in entries:
|
|
94
|
+
next_entry = {}
|
|
95
|
+
if "matcher" in entry:
|
|
96
|
+
next_entry["matcher"] = entry["matcher"]
|
|
97
|
+
|
|
98
|
+
resolved_handlers = []
|
|
99
|
+
for handler in entry.get("hooks", []):
|
|
100
|
+
cmd = handler.get("command", "")
|
|
101
|
+
resolved_cmd = cmd.replace(
|
|
102
|
+
"${CLAUDE_PLUGIN_ROOT}", plugin_root.replace("\\", "/")
|
|
103
|
+
)
|
|
104
|
+
# Also replace python with sys.executable to use the correct Python instance
|
|
105
|
+
if resolved_cmd.startswith("python "):
|
|
106
|
+
python_exe_posix = sys.executable.replace("\\", "/")
|
|
107
|
+
resolved_cmd = f'"{python_exe_posix}" ' + resolved_cmd[7:]
|
|
108
|
+
resolved_handlers.append(
|
|
109
|
+
{"type": handler.get("type"), "command": resolved_cmd}
|
|
110
|
+
)
|
|
111
|
+
next_entry["hooks"] = resolved_handlers
|
|
112
|
+
resolved_entries.append(next_entry)
|
|
113
|
+
|
|
114
|
+
cleaned_hooks[event] = cleaned_hooks.get(event, []) + resolved_entries
|
|
115
|
+
|
|
116
|
+
return {"hooks": cleaned_hooks}
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# ----------------- Adapters -----------------
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class ClaudeCodeAdapter:
|
|
123
|
+
name = "claude-code"
|
|
124
|
+
display_name = "Claude Code"
|
|
125
|
+
|
|
126
|
+
def detect(self):
|
|
127
|
+
claude_dir = os.path.join(get_home_dir(), ".claude")
|
|
128
|
+
return os.path.exists(claude_dir)
|
|
129
|
+
|
|
130
|
+
def install(self, args):
|
|
131
|
+
claude_json = os.path.join(get_home_dir(), ".claude.json")
|
|
132
|
+
mcp_stdio_path = get_mcp_stdio_path()
|
|
133
|
+
|
|
134
|
+
existing = read_json_safe(claude_json)
|
|
135
|
+
next_cfg = existing.copy()
|
|
136
|
+
servers = next_cfg.get("mcpServers", {})
|
|
137
|
+
|
|
138
|
+
already_has = "agentcache" in servers
|
|
139
|
+
if already_has and not args.force:
|
|
140
|
+
print(f"[OK] Claude Code already wired in {claude_json}")
|
|
141
|
+
else:
|
|
142
|
+
if args.dry_run:
|
|
143
|
+
print(f"[dry-run] Would write mcpServers.agentcache in {claude_json}")
|
|
144
|
+
else:
|
|
145
|
+
backup = backup_file(claude_json, "claude-code")
|
|
146
|
+
if backup:
|
|
147
|
+
print(f"Backed up configuration to {backup}")
|
|
148
|
+
|
|
149
|
+
env = {
|
|
150
|
+
"AGENTCACHE_URL": os.environ.get("AGENTCACHE_URL")
|
|
151
|
+
or os.environ.get("AGENTMEMORY_URL")
|
|
152
|
+
or "http://localhost:3111"
|
|
153
|
+
}
|
|
154
|
+
secret = os.environ.get("AGENTCACHE_SECRET") or os.environ.get(
|
|
155
|
+
"AGENTMEMORY_SECRET"
|
|
156
|
+
)
|
|
157
|
+
if secret:
|
|
158
|
+
env["AGENTCACHE_SECRET"] = secret
|
|
159
|
+
servers["agentcache"] = {
|
|
160
|
+
"command": sys.executable,
|
|
161
|
+
"args": [mcp_stdio_path],
|
|
162
|
+
"env": env,
|
|
163
|
+
}
|
|
164
|
+
next_cfg["mcpServers"] = servers
|
|
165
|
+
write_json_atomic(claude_json, next_cfg)
|
|
166
|
+
print(f"[OK] Wired Claude Code MCP to {claude_json}")
|
|
167
|
+
|
|
168
|
+
if args.with_hooks:
|
|
169
|
+
claude_settings = os.path.join(get_home_dir(), ".claude", "settings.json")
|
|
170
|
+
try:
|
|
171
|
+
plugin_root = get_plugin_root()
|
|
172
|
+
existing_settings = read_json_safe(claude_settings)
|
|
173
|
+
merged = build_merged_hooks(
|
|
174
|
+
existing_settings, plugin_root, "hooks.json"
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
if args.dry_run:
|
|
178
|
+
print(f"[dry-run] Would merge hooks into {claude_settings}")
|
|
179
|
+
else:
|
|
180
|
+
backup = backup_file(claude_settings, "claude-settings")
|
|
181
|
+
if backup:
|
|
182
|
+
print(f"Backed up settings to {backup}")
|
|
183
|
+
existing_settings["hooks"] = merged["hooks"]
|
|
184
|
+
write_json_atomic(claude_settings, existing_settings)
|
|
185
|
+
print(f"[OK] Wired Claude Code hooks to {claude_settings}")
|
|
186
|
+
except Exception as e:
|
|
187
|
+
print(f"[FAIL] Failed to configure Claude Code hooks: {e}")
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
class CodexAdapter:
|
|
191
|
+
name = "codex"
|
|
192
|
+
display_name = "Codex CLI"
|
|
193
|
+
|
|
194
|
+
def detect(self):
|
|
195
|
+
codex_dir = os.path.join(get_home_dir(), ".codex")
|
|
196
|
+
return os.path.exists(codex_dir)
|
|
197
|
+
|
|
198
|
+
def install(self, args):
|
|
199
|
+
codex_toml = os.path.join(get_home_dir(), ".codex", "config.toml")
|
|
200
|
+
mcp_stdio_path = get_mcp_stdio_path()
|
|
201
|
+
|
|
202
|
+
url = (
|
|
203
|
+
os.environ.get("AGENTCACHE_URL")
|
|
204
|
+
or os.environ.get("AGENTMEMORY_URL")
|
|
205
|
+
or "http://localhost:3111"
|
|
206
|
+
)
|
|
207
|
+
secret = os.environ.get("AGENTCACHE_SECRET") or os.environ.get(
|
|
208
|
+
"AGENTMEMORY_SECRET"
|
|
209
|
+
)
|
|
210
|
+
python_exe_posix = sys.executable.replace("\\", "/")
|
|
211
|
+
mcp_stdio_posix = mcp_stdio_path.replace("\\", "/")
|
|
212
|
+
toml_block = f"""
|
|
213
|
+
[mcp_servers.agentcache]
|
|
214
|
+
command = "{python_exe_posix}"
|
|
215
|
+
args = ["{mcp_stdio_posix}"]
|
|
216
|
+
[mcp_servers.agentcache.env]
|
|
217
|
+
AGENTCACHE_URL = "{url}"
|
|
218
|
+
"""
|
|
219
|
+
if secret:
|
|
220
|
+
toml_block += f'AGENTCACHE_SECRET = "{secret}"\n'
|
|
221
|
+
|
|
222
|
+
exists = os.path.exists(codex_toml)
|
|
223
|
+
current = ""
|
|
224
|
+
if exists:
|
|
225
|
+
with open(codex_toml, "r", encoding="utf-8") as f:
|
|
226
|
+
current = f.read()
|
|
227
|
+
|
|
228
|
+
wired = "[mcp_servers.agentcache]" in current
|
|
229
|
+
if wired and not args.force:
|
|
230
|
+
print(f"[OK] Codex CLI already wired in {codex_toml}")
|
|
231
|
+
else:
|
|
232
|
+
if args.dry_run:
|
|
233
|
+
print(
|
|
234
|
+
f"[dry-run] Would write [mcp_servers.agentcache] block to {codex_toml}"
|
|
235
|
+
)
|
|
236
|
+
else:
|
|
237
|
+
backup = backup_file(codex_toml, "codex", "toml")
|
|
238
|
+
if backup:
|
|
239
|
+
print(f"Backed up config to {backup}")
|
|
240
|
+
|
|
241
|
+
# Strip existing block if forcing
|
|
242
|
+
cleaned = current
|
|
243
|
+
if wired:
|
|
244
|
+
lines = current.splitlines()
|
|
245
|
+
out = []
|
|
246
|
+
skipping = False
|
|
247
|
+
for line in lines:
|
|
248
|
+
trimmed = line.strip()
|
|
249
|
+
if (
|
|
250
|
+
trimmed == "[mcp_servers.agentcache]"
|
|
251
|
+
or trimmed == "[mcp_servers.agentcache.env]"
|
|
252
|
+
):
|
|
253
|
+
skipping = True
|
|
254
|
+
continue
|
|
255
|
+
if (
|
|
256
|
+
skipping
|
|
257
|
+
and trimmed.startswith("[")
|
|
258
|
+
and trimmed != "[mcp_servers.agentcache.env]"
|
|
259
|
+
):
|
|
260
|
+
skipping = False
|
|
261
|
+
if not skipping:
|
|
262
|
+
out.append(line)
|
|
263
|
+
cleaned = "\n".join(out).strip()
|
|
264
|
+
|
|
265
|
+
next_toml = (
|
|
266
|
+
cleaned + ("\n\n" if cleaned else "") + toml_block.strip() + "\n"
|
|
267
|
+
)
|
|
268
|
+
os.makedirs(os.path.dirname(codex_toml), exist_ok=True)
|
|
269
|
+
with open(codex_toml, "w", encoding="utf-8") as f:
|
|
270
|
+
f.write(next_toml)
|
|
271
|
+
print(f"[OK] Wired Codex CLI TOML configuration to {codex_toml}")
|
|
272
|
+
|
|
273
|
+
if args.with_hooks:
|
|
274
|
+
codex_hooks = os.path.join(get_home_dir(), ".codex", "hooks.json")
|
|
275
|
+
try:
|
|
276
|
+
plugin_root = get_plugin_root()
|
|
277
|
+
existing_hooks = read_json_safe(codex_hooks)
|
|
278
|
+
merged = build_merged_hooks(
|
|
279
|
+
existing_hooks, plugin_root, "hooks.codex.json"
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
if args.dry_run:
|
|
283
|
+
print(f"[dry-run] Would merge hooks into {codex_hooks}")
|
|
284
|
+
else:
|
|
285
|
+
backup = backup_file(codex_hooks, "codex-hooks")
|
|
286
|
+
if backup:
|
|
287
|
+
print(f"Backed up hooks to {backup}")
|
|
288
|
+
write_json_atomic(codex_hooks, merged)
|
|
289
|
+
print(f"[OK] Wired Codex hooks workaround to {codex_hooks}")
|
|
290
|
+
except Exception as e:
|
|
291
|
+
print(f"[FAIL] Failed to configure Codex hooks: {e}")
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
class HermesAdapter:
|
|
295
|
+
name = "hermes"
|
|
296
|
+
display_name = "Hermes Agent"
|
|
297
|
+
|
|
298
|
+
def detect(self):
|
|
299
|
+
hermes_dir = os.path.join(get_home_dir(), ".hermes")
|
|
300
|
+
return os.path.exists(hermes_dir)
|
|
301
|
+
|
|
302
|
+
def install(self, args):
|
|
303
|
+
dest_dir = os.path.join(get_home_dir(), ".hermes", "plugins", "agentcache")
|
|
304
|
+
src_dir = os.path.dirname(os.path.abspath(__file__))
|
|
305
|
+
project_root = os.path.dirname(src_dir)
|
|
306
|
+
hermes_src = os.path.join(project_root, "integrations", "hermes")
|
|
307
|
+
|
|
308
|
+
if not os.path.exists(hermes_src):
|
|
309
|
+
print(
|
|
310
|
+
f"[FAIL] Failed: Source integrations/hermes not found at {hermes_src}"
|
|
311
|
+
)
|
|
312
|
+
return
|
|
313
|
+
|
|
314
|
+
if args.dry_run:
|
|
315
|
+
print(f"[dry-run] Would copy {hermes_src} to {dest_dir}")
|
|
316
|
+
else:
|
|
317
|
+
if os.path.exists(dest_dir):
|
|
318
|
+
if not args.force:
|
|
319
|
+
print(
|
|
320
|
+
f"[OK] Hermes plugin directory already exists at {dest_dir}. Use --force to overwrite."
|
|
321
|
+
)
|
|
322
|
+
return
|
|
323
|
+
shutil.rmtree(dest_dir)
|
|
324
|
+
|
|
325
|
+
shutil.copytree(hermes_src, dest_dir)
|
|
326
|
+
print(f"[OK] Copied Hermes cache provider plugin to {dest_dir}")
|
|
327
|
+
print("To finish configuration, add to ~/.hermes/config.yaml:")
|
|
328
|
+
print(" mcp_servers:")
|
|
329
|
+
print(" agentcache:")
|
|
330
|
+
print(" command: python")
|
|
331
|
+
print(f' args: ["{get_mcp_stdio_path()}"]')
|
|
332
|
+
print(" cache:")
|
|
333
|
+
print(" provider: agentcache")
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
class AntigravityAdapter:
|
|
337
|
+
name = "antigravity"
|
|
338
|
+
display_name = "Antigravity"
|
|
339
|
+
|
|
340
|
+
def get_user_dir(self):
|
|
341
|
+
if sys.platform == "darwin":
|
|
342
|
+
return os.path.join(
|
|
343
|
+
get_home_dir(), "Library", "Application Support", "Antigravity", "User"
|
|
344
|
+
)
|
|
345
|
+
elif sys.platform == "win32":
|
|
346
|
+
appdata = get_appdata_dir()
|
|
347
|
+
return os.path.join(appdata, "Antigravity", "User")
|
|
348
|
+
else:
|
|
349
|
+
return os.path.join(get_home_dir(), ".config", "Antigravity", "User")
|
|
350
|
+
|
|
351
|
+
def get_gemini_mcp_dir(self):
|
|
352
|
+
return os.path.join(
|
|
353
|
+
get_home_dir(), ".gemini", "antigravity", "mcp", "agentcache"
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
def detect(self):
|
|
357
|
+
gemini_parent = os.path.dirname(os.path.dirname(self.get_gemini_mcp_dir()))
|
|
358
|
+
return os.path.exists(gemini_parent) or os.path.exists(self.get_user_dir())
|
|
359
|
+
|
|
360
|
+
def install_gemini_schemas(self, args):
|
|
361
|
+
gemini_mcp_dir = self.get_gemini_mcp_dir()
|
|
362
|
+
if args.dry_run:
|
|
363
|
+
print(
|
|
364
|
+
f"[dry-run] Would create directory {gemini_mcp_dir} and write tool schema JSON files."
|
|
365
|
+
)
|
|
366
|
+
return
|
|
367
|
+
|
|
368
|
+
os.makedirs(gemini_mcp_dir, exist_ok=True)
|
|
369
|
+
try:
|
|
370
|
+
# Dynamic import to avoid circular dependency
|
|
371
|
+
from .routes.mcp import get_mcp_tools_schemas
|
|
372
|
+
|
|
373
|
+
tools = get_mcp_tools_schemas()
|
|
374
|
+
except Exception as e:
|
|
375
|
+
print(f"[FAIL] Could not load tool schemas: {e}")
|
|
376
|
+
return
|
|
377
|
+
|
|
378
|
+
for tool in tools:
|
|
379
|
+
tool_name = tool["name"]
|
|
380
|
+
schema = {
|
|
381
|
+
"name": tool_name,
|
|
382
|
+
"description": tool.get("description", ""),
|
|
383
|
+
"parameters": tool.get("inputSchema", {}),
|
|
384
|
+
}
|
|
385
|
+
tool_file_path = os.path.join(gemini_mcp_dir, f"{tool_name}.json")
|
|
386
|
+
|
|
387
|
+
if os.path.exists(tool_file_path) and not args.force:
|
|
388
|
+
continue
|
|
389
|
+
|
|
390
|
+
with open(tool_file_path, "w", encoding="utf-8") as f:
|
|
391
|
+
json.dump(schema, f, indent=2)
|
|
392
|
+
f.write("\n")
|
|
393
|
+
print(f"[OK] Installed {len(tools)} tool schemas to {gemini_mcp_dir}")
|
|
394
|
+
|
|
395
|
+
def install(self, args):
|
|
396
|
+
# 1. Install tool schemas under ~/.gemini/antigravity/mcp/agentcache
|
|
397
|
+
gemini_parent = os.path.dirname(os.path.dirname(self.get_gemini_mcp_dir()))
|
|
398
|
+
if os.path.exists(gemini_parent) or args.force:
|
|
399
|
+
self.install_gemini_schemas(args)
|
|
400
|
+
|
|
401
|
+
# 2. Wire the VS Code/User AppData client config if present
|
|
402
|
+
user_dir = self.get_user_dir()
|
|
403
|
+
if os.path.exists(user_dir) or args.force:
|
|
404
|
+
mcp_config_path = os.path.join(user_dir, "mcp_config.json")
|
|
405
|
+
mcp_stdio_path = get_mcp_stdio_path()
|
|
406
|
+
|
|
407
|
+
existing = read_json_safe(mcp_config_path)
|
|
408
|
+
next_cfg = existing.copy()
|
|
409
|
+
servers = next_cfg.get("mcpServers", {})
|
|
410
|
+
|
|
411
|
+
already_has = "agentcache" in servers
|
|
412
|
+
if already_has and not args.force:
|
|
413
|
+
print(
|
|
414
|
+
f"[OK] Antigravity VS Code client already wired in {mcp_config_path}"
|
|
415
|
+
)
|
|
416
|
+
else:
|
|
417
|
+
if args.dry_run:
|
|
418
|
+
print(
|
|
419
|
+
f"[dry-run] Would write mcpServers.agentcache in {mcp_config_path}"
|
|
420
|
+
)
|
|
421
|
+
else:
|
|
422
|
+
backup = backup_file(mcp_config_path, "antigravity")
|
|
423
|
+
if backup:
|
|
424
|
+
print(f"Backed up config to {backup}")
|
|
425
|
+
|
|
426
|
+
env = {
|
|
427
|
+
"AGENTCACHE_URL": os.environ.get("AGENTCACHE_URL")
|
|
428
|
+
or os.environ.get("AGENTMEMORY_URL")
|
|
429
|
+
or "http://localhost:3111"
|
|
430
|
+
}
|
|
431
|
+
secret = os.environ.get("AGENTCACHE_SECRET") or os.environ.get(
|
|
432
|
+
"AGENTMEMORY_SECRET"
|
|
433
|
+
)
|
|
434
|
+
if secret:
|
|
435
|
+
env["AGENTCACHE_SECRET"] = secret
|
|
436
|
+
servers["agentcache"] = {
|
|
437
|
+
"command": sys.executable,
|
|
438
|
+
"args": [mcp_stdio_path],
|
|
439
|
+
"env": env,
|
|
440
|
+
}
|
|
441
|
+
next_cfg["mcpServers"] = servers
|
|
442
|
+
write_json_atomic(mcp_config_path, next_cfg)
|
|
443
|
+
print(
|
|
444
|
+
f"[OK] Wired Antigravity VS Code client MCP config in {mcp_config_path}"
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
class KiroAdapter:
|
|
449
|
+
name = "kiro"
|
|
450
|
+
display_name = "Kiro"
|
|
451
|
+
|
|
452
|
+
def detect(self):
|
|
453
|
+
kiro_dir = os.path.join(get_home_dir(), ".kiro")
|
|
454
|
+
return os.path.exists(kiro_dir)
|
|
455
|
+
|
|
456
|
+
def install(self, args):
|
|
457
|
+
mcp_config_path = os.path.join(get_home_dir(), ".kiro", "settings", "mcp.json")
|
|
458
|
+
mcp_stdio_path = get_mcp_stdio_path()
|
|
459
|
+
|
|
460
|
+
existing = read_json_safe(mcp_config_path)
|
|
461
|
+
next_cfg = existing.copy()
|
|
462
|
+
servers = next_cfg.get("mcpServers", {})
|
|
463
|
+
|
|
464
|
+
already_has = "agentcache" in servers
|
|
465
|
+
if already_has and not args.force:
|
|
466
|
+
print(f"[OK] Kiro already wired in {mcp_config_path}")
|
|
467
|
+
else:
|
|
468
|
+
if args.dry_run:
|
|
469
|
+
print(
|
|
470
|
+
f"[dry-run] Would write mcpServers.agentcache in {mcp_config_path}"
|
|
471
|
+
)
|
|
472
|
+
else:
|
|
473
|
+
backup = backup_file(mcp_config_path, "kiro")
|
|
474
|
+
if backup:
|
|
475
|
+
print(f"Backed up config to {backup}")
|
|
476
|
+
|
|
477
|
+
env = {
|
|
478
|
+
"AGENTCACHE_URL": os.environ.get("AGENTCACHE_URL")
|
|
479
|
+
or os.environ.get("AGENTMEMORY_URL")
|
|
480
|
+
or "http://localhost:3111"
|
|
481
|
+
}
|
|
482
|
+
secret = os.environ.get("AGENTCACHE_SECRET") or os.environ.get(
|
|
483
|
+
"AGENTMEMORY_SECRET"
|
|
484
|
+
)
|
|
485
|
+
if secret:
|
|
486
|
+
env["AGENTCACHE_SECRET"] = secret
|
|
487
|
+
servers["agentcache"] = {
|
|
488
|
+
"command": sys.executable,
|
|
489
|
+
"args": [mcp_stdio_path],
|
|
490
|
+
"env": env,
|
|
491
|
+
}
|
|
492
|
+
next_cfg["mcpServers"] = servers
|
|
493
|
+
write_json_atomic(mcp_config_path, next_cfg)
|
|
494
|
+
print(f"[OK] Wired Kiro MCP config in {mcp_config_path}")
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
class VSCodeAdapter:
|
|
498
|
+
name = "vscode"
|
|
499
|
+
display_name = "VS Code"
|
|
500
|
+
|
|
501
|
+
def get_user_config_path(self):
|
|
502
|
+
if sys.platform == "darwin":
|
|
503
|
+
return os.path.join(
|
|
504
|
+
get_home_dir(),
|
|
505
|
+
"Library",
|
|
506
|
+
"Application Support",
|
|
507
|
+
"Code",
|
|
508
|
+
"User",
|
|
509
|
+
"mcp.json",
|
|
510
|
+
)
|
|
511
|
+
elif sys.platform == "win32":
|
|
512
|
+
appdata = get_appdata_dir()
|
|
513
|
+
return os.path.join(appdata, "Code", "User", "mcp.json")
|
|
514
|
+
else:
|
|
515
|
+
return os.path.join(get_home_dir(), ".config", "Code", "User", "mcp.json")
|
|
516
|
+
|
|
517
|
+
def detect(self):
|
|
518
|
+
return os.path.exists(os.path.dirname(self.get_user_config_path()))
|
|
519
|
+
|
|
520
|
+
def install(self, args):
|
|
521
|
+
mcp_config_path = self.get_user_config_path()
|
|
522
|
+
mcp_stdio_path = get_mcp_stdio_path()
|
|
523
|
+
|
|
524
|
+
existing = read_json_safe(mcp_config_path)
|
|
525
|
+
next_cfg = existing.copy()
|
|
526
|
+
servers = next_cfg.get("servers", {})
|
|
527
|
+
|
|
528
|
+
already_has = "agentcache" in servers
|
|
529
|
+
if already_has and not args.force:
|
|
530
|
+
print(f"[OK] VS Code already wired in {mcp_config_path}")
|
|
531
|
+
else:
|
|
532
|
+
if args.dry_run:
|
|
533
|
+
print(f"[dry-run] Would write servers.agentcache in {mcp_config_path}")
|
|
534
|
+
else:
|
|
535
|
+
backup = backup_file(mcp_config_path, "vscode")
|
|
536
|
+
if backup:
|
|
537
|
+
print(f"Backed up config to {backup}")
|
|
538
|
+
|
|
539
|
+
env = {
|
|
540
|
+
"AGENTCACHE_URL": os.environ.get("AGENTCACHE_URL")
|
|
541
|
+
or os.environ.get("AGENTMEMORY_URL")
|
|
542
|
+
or "http://localhost:3111"
|
|
543
|
+
}
|
|
544
|
+
secret = os.environ.get("AGENTCACHE_SECRET") or os.environ.get(
|
|
545
|
+
"AGENTMEMORY_SECRET"
|
|
546
|
+
)
|
|
547
|
+
if secret:
|
|
548
|
+
env["AGENTCACHE_SECRET"] = secret
|
|
549
|
+
servers["agentcache"] = {
|
|
550
|
+
"command": sys.executable,
|
|
551
|
+
"args": [mcp_stdio_path],
|
|
552
|
+
"env": env,
|
|
553
|
+
}
|
|
554
|
+
next_cfg["servers"] = servers
|
|
555
|
+
write_json_atomic(mcp_config_path, next_cfg)
|
|
556
|
+
print(f"[OK] Wired VS Code MCP config in {mcp_config_path}")
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
class RulesGeneratorAdapter:
|
|
560
|
+
name = "cursor"
|
|
561
|
+
display_name = "Workspace Rules (Cursor/Cline/Windsurf)"
|
|
562
|
+
|
|
563
|
+
def detect(self):
|
|
564
|
+
# Always available for rules generation in current directory
|
|
565
|
+
return True
|
|
566
|
+
|
|
567
|
+
def install(self, args):
|
|
568
|
+
rule_content = """# Agent Cache Rules
|
|
569
|
+
|
|
570
|
+
This workspace is integrated with long-term semantic memory via `agentcache-python`.
|
|
571
|
+
You must act as your own cache manager by calling the cache MCP tools at critical boundaries.
|
|
572
|
+
|
|
573
|
+
## Rules & Workflow
|
|
574
|
+
|
|
575
|
+
1. **Initial Search (Prefetch Context)**:
|
|
576
|
+
At the start of every session or new task, immediately call `cache_smart_search` with terms related to the current objective. This retrieves past architecture patterns, preferences, bug fixes, or lessons.
|
|
577
|
+
- Example: `cache_smart_search(query="jwt token rotation logic")`
|
|
578
|
+
|
|
579
|
+
2. **Lessons & Insights Capture**:
|
|
580
|
+
When you successfully debug a complex error, discover an undocumented requirement, or establish a convention, persist it:
|
|
581
|
+
- Call `cache_lesson_save` to record lessons that improve your coding capabilities. Duplicate saves strengthen confidence scores.
|
|
582
|
+
- Call `cache_save` to save long-term structural facts. Always extract 2-5 specific lowercased tags (e.g. `auth-flow`, `refresh-token`) as concepts.
|
|
583
|
+
|
|
584
|
+
3. **Checklist Before Ending**:
|
|
585
|
+
Before stating a task is complete:
|
|
586
|
+
- Reflect on whether any lessons learned should be saved.
|
|
587
|
+
- Call `cache_reflect` to automatically distribute observations into slots if needed.
|
|
588
|
+
"""
|
|
589
|
+
cwd = os.getcwd()
|
|
590
|
+
|
|
591
|
+
# Write to .cursorrules
|
|
592
|
+
cursorrules = os.path.join(cwd, ".cursorrules")
|
|
593
|
+
clineskills = os.path.join(cwd, ".clineskills")
|
|
594
|
+
windsurfrules = os.path.join(cwd, ".windsurfrules")
|
|
595
|
+
|
|
596
|
+
if args.dry_run:
|
|
597
|
+
print(f"[dry-run] Would write rules templates to {cwd}")
|
|
598
|
+
else:
|
|
599
|
+
with open(cursorrules, "w", encoding="utf-8") as f:
|
|
600
|
+
f.write(rule_content)
|
|
601
|
+
with open(clineskills, "w", encoding="utf-8") as f:
|
|
602
|
+
f.write(rule_content)
|
|
603
|
+
with open(windsurfrules, "w", encoding="utf-8") as f:
|
|
604
|
+
f.write(rule_content)
|
|
605
|
+
print("[OK] Generated rule templates in current workspace:")
|
|
606
|
+
print(f" - {cursorrules}")
|
|
607
|
+
print(f" - {clineskills}")
|
|
608
|
+
print(f" - {windsurfrules}")
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
ADAPTERS = [
|
|
612
|
+
ClaudeCodeAdapter(),
|
|
613
|
+
CodexAdapter(),
|
|
614
|
+
HermesAdapter(),
|
|
615
|
+
AntigravityAdapter(),
|
|
616
|
+
KiroAdapter(),
|
|
617
|
+
VSCodeAdapter(),
|
|
618
|
+
RulesGeneratorAdapter(),
|
|
619
|
+
]
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
def map_agent_alias(name: str) -> str:
|
|
623
|
+
if not name:
|
|
624
|
+
return name
|
|
625
|
+
name = name.lower().strip()
|
|
626
|
+
if name in (
|
|
627
|
+
"antigravity",
|
|
628
|
+
"/anti-gravity",
|
|
629
|
+
"anti-gravity",
|
|
630
|
+
"/antigravity",
|
|
631
|
+
"antigravity",
|
|
632
|
+
):
|
|
633
|
+
return "antigravity"
|
|
634
|
+
if name in ("claude", "claude-code", "claudecode", "claude code"):
|
|
635
|
+
return "claude-code"
|
|
636
|
+
if name in ("kiro", "keyro"):
|
|
637
|
+
return "kiro"
|
|
638
|
+
if name in ("vscode", "vs-code", "visual-studio-code", "vs code"):
|
|
639
|
+
return "vscode"
|
|
640
|
+
return name
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
def run_connect(args):
|
|
644
|
+
# Normalize/Map target agent alias if provided
|
|
645
|
+
agent_name = getattr(args, "agent", None)
|
|
646
|
+
if agent_name:
|
|
647
|
+
agent_name = map_agent_alias(agent_name)
|
|
648
|
+
setattr(args, "agent", agent_name)
|
|
649
|
+
|
|
650
|
+
valid_names = [a.name for a in ADAPTERS]
|
|
651
|
+
if agent_name and agent_name not in valid_names:
|
|
652
|
+
print(
|
|
653
|
+
f"[FAIL] Unknown agent: {agent_name}. Supported agents: {', '.join(valid_names)}"
|
|
654
|
+
)
|
|
655
|
+
sys.exit(1)
|
|
656
|
+
|
|
657
|
+
targets = []
|
|
658
|
+
if getattr(args, "all", False):
|
|
659
|
+
targets = [a for a in ADAPTERS if a.detect() and a.name != "cursor"]
|
|
660
|
+
else:
|
|
661
|
+
matched = [a for a in ADAPTERS if a.name == agent_name]
|
|
662
|
+
if matched:
|
|
663
|
+
targets = matched
|
|
664
|
+
|
|
665
|
+
if not targets:
|
|
666
|
+
print("No agents detected or matched target.")
|
|
667
|
+
sys.exit(1)
|
|
668
|
+
|
|
669
|
+
for target in targets:
|
|
670
|
+
if not target.detect() and not getattr(args, "force", False):
|
|
671
|
+
print(
|
|
672
|
+
f"[FAIL] {target.display_name} not detected on this system. (Use --force to install anyway)"
|
|
673
|
+
)
|
|
674
|
+
continue
|
|
675
|
+
|
|
676
|
+
print(f"Wiring {target.display_name}...")
|
|
677
|
+
try:
|
|
678
|
+
target.install(args)
|
|
679
|
+
except Exception as e:
|
|
680
|
+
print(f"[FAIL] Failed to install {target.display_name}: {e}")
|
|
681
|
+
|
|
682
|
+
|
|
683
|
+
def main():
|
|
684
|
+
parser = argparse.ArgumentParser(
|
|
685
|
+
description="Wired agentcache MCP and Hooks into client agents."
|
|
686
|
+
)
|
|
687
|
+
parser.add_argument(
|
|
688
|
+
"agent",
|
|
689
|
+
nargs="?",
|
|
690
|
+
help="Specify target agent (antigravity, claude-code, kiro, etc.).",
|
|
691
|
+
)
|
|
692
|
+
parser.add_argument(
|
|
693
|
+
"--with-hooks",
|
|
694
|
+
action="store_true",
|
|
695
|
+
help="Install global workspace hook execution blocks (Claude/Codex).",
|
|
696
|
+
)
|
|
697
|
+
parser.add_argument(
|
|
698
|
+
"--dry-run",
|
|
699
|
+
action="store_true",
|
|
700
|
+
help="Log proposed configuration modifications without writing.",
|
|
701
|
+
)
|
|
702
|
+
parser.add_argument(
|
|
703
|
+
"--force",
|
|
704
|
+
action="store_true",
|
|
705
|
+
help="Overwrite existing configuration settings.",
|
|
706
|
+
)
|
|
707
|
+
parser.add_argument(
|
|
708
|
+
"--all", action="store_true", help="Attempt connection to all detected agents."
|
|
709
|
+
)
|
|
710
|
+
|
|
711
|
+
args = parser.parse_args()
|
|
712
|
+
|
|
713
|
+
if not args.agent and not args.all:
|
|
714
|
+
parser.print_help()
|
|
715
|
+
print("\nAvailable agents:")
|
|
716
|
+
for a in ADAPTERS:
|
|
717
|
+
print(f" - {a.name:15} ({a.display_name})")
|
|
718
|
+
sys.exit(0)
|
|
719
|
+
|
|
720
|
+
run_connect(args)
|
|
721
|
+
|
|
722
|
+
|
|
723
|
+
if __name__ == "__main__":
|
|
724
|
+
main()
|