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.
Files changed (52) hide show
  1. agentcache/__init__.py +29 -0
  2. agentcache/app.py +312 -0
  3. agentcache/cli.py +346 -0
  4. agentcache/connect.py +724 -0
  5. agentcache/core/__init__.py +19 -0
  6. agentcache/core/audit_log.py +104 -0
  7. agentcache/core/config.py +51 -0
  8. agentcache/core/context_builder.py +209 -0
  9. agentcache/core/graph.py +120 -0
  10. agentcache/core/image_store.py +111 -0
  11. agentcache/core/infer.py +142 -0
  12. agentcache/core/kv_scopes.py +86 -0
  13. agentcache/core/lessons.py +242 -0
  14. agentcache/core/llm.py +596 -0
  15. agentcache/core/memory_store.py +207 -0
  16. agentcache/core/observation_store.py +576 -0
  17. agentcache/core/privacy.py +34 -0
  18. agentcache/core/project_profile.py +625 -0
  19. agentcache/core/search_service.py +444 -0
  20. agentcache/core/session_store.py +382 -0
  21. agentcache/core/slots.py +504 -0
  22. agentcache/db.py +359 -0
  23. agentcache/import_data.py +86 -0
  24. agentcache/legacy.py +94 -0
  25. agentcache/mcp_stdio.py +141 -0
  26. agentcache/py.typed +0 -0
  27. agentcache/replay_import.py +680 -0
  28. agentcache/routes/__init__.py +29 -0
  29. agentcache/routes/_deps.py +46 -0
  30. agentcache/routes/auth.py +68 -0
  31. agentcache/routes/graph.py +81 -0
  32. agentcache/routes/health.py +149 -0
  33. agentcache/routes/mcp.py +614 -0
  34. agentcache/routes/memories.py +116 -0
  35. agentcache/routes/migration.py +32 -0
  36. agentcache/routes/observations.py +253 -0
  37. agentcache/routes/search.py +80 -0
  38. agentcache/search.py +935 -0
  39. agentcache/storage/__init__.py +29 -0
  40. agentcache/storage/images.py +102 -0
  41. agentcache/storage/paths.py +116 -0
  42. agentcache/storage/scopes.py +9 -0
  43. agentcache/viewer/favicon.svg +1 -0
  44. agentcache/viewer/index.html +4235 -0
  45. agentcache/viewer_helpers.py +61 -0
  46. agentcache/workers.py +192 -0
  47. agentcache_core-0.9.9.dist-info/METADATA +194 -0
  48. agentcache_core-0.9.9.dist-info/RECORD +52 -0
  49. agentcache_core-0.9.9.dist-info/WHEEL +5 -0
  50. agentcache_core-0.9.9.dist-info/entry_points.txt +2 -0
  51. agentcache_core-0.9.9.dist-info/licenses/LICENSE +190 -0
  52. 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()