Yash030 commited on
Commit
447d72f
·
1 Parent(s): c8258aa

Expose missing MCP tools for Pinned Slots, Actions, and crystallization (Crystals)

Browse files
AGENTS.md CHANGED
@@ -91,6 +91,45 @@ Auth: all endpoints check `AGENTMEMORY_SECRET` via timing-safe `hmac.compare_dig
91
 
92
  WebSocket at `/stream/mem-live/viewer` broadcasts raw + compressed observations to connected viewers.
93
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94
  ## Consistency rules
95
 
96
  **When adding a REST endpoint:**
 
91
 
92
  WebSocket at `/stream/mem-live/viewer` broadcasts raw + compressed observations to connected viewers.
93
 
94
+ ## MCP Tools
95
+
96
+ The server exposes 30 MCP tools via `GET /agentmemory/mcp/tools` (schema) and `POST /agentmemory/mcp/tools` (execution).
97
+
98
+ | Tool | Description | Status |
99
+ |------|-------------|--------|
100
+ | `memory_recall` | Search past session observations | Working |
101
+ | `memory_save` | Save long-term memory (concepts/files as string or array) | Working |
102
+ | `memory_sessions` | List recent sessions | Working |
103
+ | `memory_sessions_list` | Retrieve all memory sessions | Working |
104
+ | `memory_smart_search` | Hybrid semantic+keyword search | Working |
105
+ | `memory_timeline` | Chronological observations | Working |
106
+ | `memory_observations` | Get observations for session | Working |
107
+ | `memory_profile` | User/project profile | Working |
108
+ | `memory_lessons` | List saved lessons | Working |
109
+ | `memory_lesson_save` | Save lesson from session | Working |
110
+ | `memory_lesson_recall` | Search lessons by query | Working |
111
+ | `memory_lesson_search` | Search lessons (keywords) | Working |
112
+ | `memory_consolidate` | Summarize sessions, extract memory | Working |
113
+ | `memory_reflect` | Reflect on session, update context | Working |
114
+ | `memory_diagnose` | Health check subsystems | Working |
115
+ | `memory_forget` | Delete memory/session/observations | Working |
116
+ | `memory_export` | Export all data as JSON | Working |
117
+ | `agent_observe` | Log agent execution observation | Working |
118
+ | `agent_remember` | Save agent memory to long-term | Working |
119
+ | `memory_antigravity_sync` | Sync Antigravity transcripts | Working |
120
+ | `memory_slot_list` | List all pinned memory slots | Working |
121
+ | `memory_slot_get` | Retrieve a specific pinned slot | Working |
122
+ | `memory_slot_create` | Create/overwrite pinned slot | Working |
123
+ | `memory_slot_append` | Append text content to slot | Working |
124
+ | `memory_slot_replace` | Replace slot content | Working |
125
+ | `memory_slot_delete` | Delete pinned memory slot | Working |
126
+ | `memory_action_create` | Create a new work item / action | Working |
127
+ | `memory_action_update` | Update fields of existing action | Working |
128
+ | `memory_frontier` | Get active/pending actions | Working |
129
+ | `memory_crystallize` | Summarize session observations | Working |
130
+
131
+ **MCP stdio wrapper:** `src/mcp_stdio.py` reads `AGENTMEMORY_URL` and `AGENTMEMORY_SECRET` from environment variables dynamically.
132
+
133
  ## Consistency rules
134
 
135
  **When adding a REST endpoint:**
README.md CHANGED
@@ -106,7 +106,7 @@ curl -X POST http://localhost:3111/agentmemory/search \
106
  |---------|--------|-------|
107
  | REST API — sessions, memories, observations | ✅ | Full surface |
108
  | WebSocket live stream | ✅ | `/stream/mem-live/viewer` |
109
- | MCP tools endpoint | ✅ | 16 tools |
110
  | Built-in HTML viewer | ✅ | Real-time dashboard at `/viewer` |
111
  | BM25 keyword search | ✅ | Always on, no API key needed |
112
  | Hybrid BM25 + vector search | ✅ | Requires `GEMINI_API_KEY` |
@@ -171,7 +171,7 @@ Verify with: curl http://localhost:3111/agentmemory/livez
171
  Open the viewer at: http://localhost:3111/viewer
172
  ```
173
 
174
- ### Available MCP Tools (16)
175
 
176
  | Tool | Description |
177
  |------|-------------|
@@ -179,18 +179,32 @@ Open the viewer at: http://localhost:3111/viewer
179
  | `memory_recall` | Search past observations by keyword |
180
  | `memory_smart_search` | Hybrid BM25 + vector semantic search |
181
  | `memory_sessions` | List recent sessions |
182
- | `memory_sessions_list` | Paginated sessions list |
183
  | `memory_timeline` | Chronological observations for a session |
184
  | `memory_observations` | Observations for a session |
185
  | `memory_profile` | Per-project concept + file profile |
186
  | `memory_lessons` | List active lessons with confidence scores |
187
  | `memory_lesson_save` | Save a lesson (duplicate saves strengthen it) |
188
- | `memory_lesson_search` | Search lessons semantically |
 
189
  | `memory_consolidate` | Run 4-tier memory consolidation |
190
- | `memory_reflect` | Auto-populate slots from session observations |
191
  | `memory_diagnose` | Health check across all subsystems |
192
- | `memory_forget` | Delete an observation or session |
193
  | `memory_export` | Export all memory data as JSON |
 
 
 
 
 
 
 
 
 
 
 
 
 
194
 
195
  ---
196
 
@@ -453,7 +467,7 @@ Query
453
  | Storage | Dolt SQL (git-versioned MySQL) | SQLite WAL (single file) |
454
  | Engine dependency | iii-engine (separate binary) | None — just Flask |
455
  | Embeddings | 6 providers + local `@xenova/transformers` | Gemini 768-dim |
456
- | MCP tools | 53 | 16 |
457
  | REST endpoints | 128 | ~50 |
458
  | Deploy | npm, Docker, fly.io, Railway, Render | Docker, HuggingFace Spaces |
459
  | Cold boot | ~7s (iii engine warm-up) | <2s |
 
106
  |---------|--------|-------|
107
  | REST API — sessions, memories, observations | ✅ | Full surface |
108
  | WebSocket live stream | ✅ | `/stream/mem-live/viewer` |
109
+ | MCP tools endpoint | ✅ | 30 tools |
110
  | Built-in HTML viewer | ✅ | Real-time dashboard at `/viewer` |
111
  | BM25 keyword search | ✅ | Always on, no API key needed |
112
  | Hybrid BM25 + vector search | ✅ | Requires `GEMINI_API_KEY` |
 
171
  Open the viewer at: http://localhost:3111/viewer
172
  ```
173
 
174
+ ### Available MCP Tools (30)
175
 
176
  | Tool | Description |
177
  |------|-------------|
 
179
  | `memory_recall` | Search past observations by keyword |
180
  | `memory_smart_search` | Hybrid BM25 + vector semantic search |
181
  | `memory_sessions` | List recent sessions |
182
+ | `memory_sessions_list` | Retrieve all memory sessions |
183
  | `memory_timeline` | Chronological observations for a session |
184
  | `memory_observations` | Observations for a session |
185
  | `memory_profile` | Per-project concept + file profile |
186
  | `memory_lessons` | List active lessons with confidence scores |
187
  | `memory_lesson_save` | Save a lesson (duplicate saves strengthen it) |
188
+ | `memory_lesson_recall` | Search lessons by query |
189
+ | `memory_lesson_search` | Search lessons by keywords |
190
  | `memory_consolidate` | Run 4-tier memory consolidation |
191
+ | `memory_reflect` | Reflect on session, update context |
192
  | `memory_diagnose` | Health check across all subsystems |
193
+ | `memory_forget` | Delete memory, session, or observations |
194
  | `memory_export` | Export all memory data as JSON |
195
+ | `agent_observe` | Log agent execution observation |
196
+ | `agent_remember` | Save agent memory to long-term storage |
197
+ | `memory_antigravity_sync` | Sync Antigravity transcripts to memory |
198
+ | `memory_slot_list` | List all pinned memory slots |
199
+ | `memory_slot_get` | Retrieve a specific pinned memory slot |
200
+ | `memory_slot_create` | Create/overwrite a pinned memory slot |
201
+ | `memory_slot_append` | Append text content to a pinned memory slot |
202
+ | `memory_slot_replace` | Replace pinned memory slot content |
203
+ | `memory_slot_delete` | Delete a pinned memory slot |
204
+ | `memory_action_create` | Create a new work item / action |
205
+ | `memory_action_update` | Update fields of an existing action |
206
+ | `memory_frontier` | Get active and pending actions sorted by priority |
207
+ | `memory_crystallize` | Crystallize/summarize observations in a session |
208
 
209
  ---
210
 
 
467
  | Storage | Dolt SQL (git-versioned MySQL) | SQLite WAL (single file) |
468
  | Engine dependency | iii-engine (separate binary) | None — just Flask |
469
  | Embeddings | 6 providers + local `@xenova/transformers` | Gemini 768-dim |
470
+ | MCP tools | 53 | 30 |
471
  | REST endpoints | 128 | ~50 |
472
  | Deploy | npm, Docker, fly.io, Railway, Render | Docker, HuggingFace Spaces |
473
  | Cold boot | ~7s (iii engine warm-up) | <2s |
plugin/.claude-plugin/plugin.json CHANGED
@@ -1,7 +1,7 @@
1
  {
2
  "name": "agentmemory",
3
  "version": "0.9.27",
4
- "description": "Persistent memory for AI coding agents -- captures tool usage, compresses via LLM, injects context into future sessions. 12 hooks, 16 MCP tools, 16 skills, real-time viewer.",
5
  "author": {
6
  "name": "Rohit Ghumare",
7
  "url": "https://github.com/rohitg00"
 
1
  {
2
  "name": "agentmemory",
3
  "version": "0.9.27",
4
+ "description": "Persistent memory for AI coding agents -- captures tool usage, compresses via LLM, injects context into future sessions. 12 hooks, 30 MCP tools, 16 skills, real-time viewer.",
5
  "author": {
6
  "name": "Rohit Ghumare",
7
  "url": "https://github.com/rohitg00"
plugin/plugin.json CHANGED
@@ -1,7 +1,7 @@
1
  {
2
  "name": "agentmemory",
3
  "version": "0.9.27",
4
- "description": "Persistent memory for AI coding agents -- captures tool usage, compresses via LLM, injects context into future sessions. 12 hooks, 16 MCP tools, 16 skills, real-time viewer.",
5
  "author": {
6
  "name": "Rohit Ghumare",
7
  "url": "https://github.com/rohitg00"
 
1
  {
2
  "name": "agentmemory",
3
  "version": "0.9.27",
4
+ "description": "Persistent memory for AI coding agents -- captures tool usage, compresses via LLM, injects context into future sessions. 12 hooks, 30 MCP tools, 16 skills, real-time viewer.",
5
  "author": {
6
  "name": "Rohit Ghumare",
7
  "url": "https://github.com/rohitg00"
plugin/scripts/prompt_submit.py CHANGED
@@ -7,25 +7,26 @@ from datetime import datetime, timezone
7
  from hook_utils import resolve_project, is_sdk_child, api_call_bg
8
 
9
  def main():
10
- try:
11
- input_data = sys.stdin.read()
12
- except Exception:
13
- return
14
-
15
- # Log to file for debugging
16
  log_path = os.path.expanduser("~/.agentmemory/hook_log.txt")
17
  try:
18
  with open(log_path, "a", encoding="utf-8") as f:
19
  f.write(f"[{datetime.now(timezone.utc).isoformat()}] prompt_submit hook called\n")
20
- f.write(f"stdin content: {input_data[:500]}\n")
21
  except Exception as e:
22
  pass
23
 
24
  try:
 
25
  if not input_data:
26
  return
27
  data = json.loads(input_data)
28
- except Exception:
 
 
 
 
 
29
  return
30
 
31
  if is_sdk_child(data):
 
7
  from hook_utils import resolve_project, is_sdk_child, api_call_bg
8
 
9
  def main():
10
+ # Log to file for debugging - write early before any potential errors
 
 
 
 
 
11
  log_path = os.path.expanduser("~/.agentmemory/hook_log.txt")
12
  try:
13
  with open(log_path, "a", encoding="utf-8") as f:
14
  f.write(f"[{datetime.now(timezone.utc).isoformat()}] prompt_submit hook called\n")
15
+ f.write(f"sys.argv: {sys.argv}\n")
16
  except Exception as e:
17
  pass
18
 
19
  try:
20
+ input_data = sys.stdin.read()
21
  if not input_data:
22
  return
23
  data = json.loads(input_data)
24
+ except Exception as e:
25
+ try:
26
+ with open(log_path, "a", encoding="utf-8") as f:
27
+ f.write(f"Error parsing stdin: {e}\n")
28
+ except:
29
+ pass
30
  return
31
 
32
  if is_sdk_child(data):
src/app.py CHANGED
@@ -1998,6 +1998,131 @@ def mcp_tools_list():
1998
  },
1999
  "required": ["mode"]
2000
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2001
  }
2002
  ]
2003
  return jsonify({"tools": tools}), 200
@@ -2184,6 +2309,102 @@ def mcp_tools_call():
2184
  res = perform_antigravity_sync(mode, current_convo, current_folder)
2185
  text_out = json.dumps(res)
2186
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2187
  else:
2188
  return jsonify({"error": f"unknown tool: {name}"}), 400
2189
 
 
1998
  },
1999
  "required": ["mode"]
2000
  }
2001
+ },
2002
+ {
2003
+ "name": "memory_slot_list",
2004
+ "description": "List all pinned memory slots.",
2005
+ "inputSchema": {"type": "object", "properties": {}}
2006
+ },
2007
+ {
2008
+ "name": "memory_slot_get",
2009
+ "description": "Retrieve the content of a specific pinned memory slot.",
2010
+ "inputSchema": {
2011
+ "type": "object",
2012
+ "properties": {
2013
+ "label": {"type": "string", "description": "The label of the pinned slot to fetch"}
2014
+ },
2015
+ "required": ["label"]
2016
+ }
2017
+ },
2018
+ {
2019
+ "name": "memory_slot_create",
2020
+ "description": "Create a new pinned memory slot or overwrite an existing one.",
2021
+ "inputSchema": {
2022
+ "type": "object",
2023
+ "properties": {
2024
+ "label": {"type": "string", "description": "The label of the pinned slot"},
2025
+ "content": {"type": "string", "description": "Initial content for the slot (optional)"},
2026
+ "scope": {"type": "string", "description": "Scope: global or session (optional, default 'global')"},
2027
+ "sizeLimit": {"type": "number", "description": "Character limit (optional)"},
2028
+ "pinned": {"type": "boolean", "description": "Whether pinned to context (optional, default true)"}
2029
+ },
2030
+ "required": ["label"]
2031
+ }
2032
+ },
2033
+ {
2034
+ "name": "memory_slot_append",
2035
+ "description": "Append text content to a pinned memory slot.",
2036
+ "inputSchema": {
2037
+ "type": "object",
2038
+ "properties": {
2039
+ "label": {"type": "string", "description": "The label of the pinned slot"},
2040
+ "text": {"type": "string", "description": "Text to append"}
2041
+ },
2042
+ "required": ["label", "text"]
2043
+ }
2044
+ },
2045
+ {
2046
+ "name": "memory_slot_replace",
2047
+ "description": "Replace the content of a pinned memory slot.",
2048
+ "inputSchema": {
2049
+ "type": "object",
2050
+ "properties": {
2051
+ "label": {"type": "string", "description": "The label of the pinned slot"},
2052
+ "content": {"type": "string", "description": "New content"}
2053
+ },
2054
+ "required": ["label", "content"]
2055
+ }
2056
+ },
2057
+ {
2058
+ "name": "memory_slot_delete",
2059
+ "description": "Delete a pinned memory slot.",
2060
+ "inputSchema": {
2061
+ "type": "object",
2062
+ "properties": {
2063
+ "label": {"type": "string", "description": "The label of the pinned slot to delete"}
2064
+ },
2065
+ "required": ["label"]
2066
+ }
2067
+ },
2068
+ {
2069
+ "name": "memory_action_create",
2070
+ "description": "Create a new work item / action.",
2071
+ "inputSchema": {
2072
+ "type": "object",
2073
+ "properties": {
2074
+ "title": {"type": "string", "description": "Title of the action"},
2075
+ "description": {"type": "string", "description": "Detailed description of the action (optional)"},
2076
+ "priority": {"type": "number", "description": "Priority score, higher is more urgent (optional, default 0)"},
2077
+ "status": {"type": "string", "description": "Status: pending, active, completed (optional, default 'pending')"},
2078
+ "tags": {
2079
+ "oneOf": [
2080
+ {"type": "string", "description": "Comma-separated tags (optional)"},
2081
+ {"type": "array", "items": {"type": "string"}, "description": "List of tags (optional)"}
2082
+ ]
2083
+ },
2084
+ "sessionId": {"type": "string", "description": "Link to a specific session ID (optional)"}
2085
+ },
2086
+ "required": ["title"]
2087
+ }
2088
+ },
2089
+ {
2090
+ "name": "memory_action_update",
2091
+ "description": "Update fields of an existing action.",
2092
+ "inputSchema": {
2093
+ "type": "object",
2094
+ "properties": {
2095
+ "actionId": {"type": "string", "description": "ID of the action to update"},
2096
+ "title": {"type": "string", "description": "Updated title (optional)"},
2097
+ "description": {"type": "string", "description": "Updated description (optional)"},
2098
+ "priority": {"type": "number", "description": "Updated priority (optional)"},
2099
+ "status": {"type": "string", "description": "Updated status: pending, active, completed, discarded (optional)"},
2100
+ "tags": {
2101
+ "oneOf": [
2102
+ {"type": "string", "description": "Comma-separated tags (optional)"},
2103
+ {"type": "array", "items": {"type": "string"}, "description": "List of tags (optional)"}
2104
+ ]
2105
+ },
2106
+ "sessionId": {"type": "string", "description": "Updated session ID (optional)"}
2107
+ },
2108
+ "required": ["actionId"]
2109
+ }
2110
+ },
2111
+ {
2112
+ "name": "memory_frontier",
2113
+ "description": "Get pending and active actions sorted by priority.",
2114
+ "inputSchema": {"type": "object", "properties": {}}
2115
+ },
2116
+ {
2117
+ "name": "memory_crystallize",
2118
+ "description": "Crystallize/summarize all observations in a session.",
2119
+ "inputSchema": {
2120
+ "type": "object",
2121
+ "properties": {
2122
+ "sessionId": {"type": "string", "description": "Session ID to crystallize"}
2123
+ },
2124
+ "required": ["sessionId"]
2125
+ }
2126
  }
2127
  ]
2128
  return jsonify({"tools": tools}), 200
 
2309
  res = perform_antigravity_sync(mode, current_convo, current_folder)
2310
  text_out = json.dumps(res)
2311
 
2312
+ elif name == "memory_slot_list":
2313
+ res = functions.slot_list(kv)
2314
+ text_out = json.dumps(res, indent=2)
2315
+
2316
+ elif name == "memory_slot_get":
2317
+ label = args.get("label")
2318
+ if not label:
2319
+ return jsonify({"error": "label is required"}), 400
2320
+ res = functions.slot_get(kv, label)
2321
+ text_out = json.dumps(res, indent=2)
2322
+
2323
+ elif name == "memory_slot_create":
2324
+ label = args.get("label")
2325
+ if not label:
2326
+ return jsonify({"error": "label is required"}), 400
2327
+ res = functions.slot_create(kv, {
2328
+ "label": label,
2329
+ "content": args.get("content"),
2330
+ "scope": args.get("scope") or "global",
2331
+ "sizeLimit": args.get("sizeLimit"),
2332
+ "pinned": args.get("pinned", True)
2333
+ })
2334
+ text_out = json.dumps(res, indent=2)
2335
+
2336
+ elif name == "memory_slot_append":
2337
+ label = args.get("label")
2338
+ text = args.get("text")
2339
+ if not label or not text:
2340
+ return jsonify({"error": "label and text are required"}), 400
2341
+ res = functions.slot_append(kv, label, text)
2342
+ text_out = json.dumps(res, indent=2)
2343
+
2344
+ elif name == "memory_slot_replace":
2345
+ label = args.get("label")
2346
+ content = args.get("content")
2347
+ if not label or content is None:
2348
+ return jsonify({"error": "label and content are required"}), 400
2349
+ res = functions.slot_replace(kv, label, content)
2350
+ text_out = json.dumps(res, indent=2)
2351
+
2352
+ elif name == "memory_slot_delete":
2353
+ label = args.get("label")
2354
+ if not label:
2355
+ return jsonify({"error": "label is required"}), 400
2356
+ res = functions.slot_delete(kv, label)
2357
+ text_out = json.dumps(res, indent=2)
2358
+
2359
+ elif name == "memory_action_create":
2360
+ action_id = functions.generate_id("act")
2361
+ from datetime import datetime, timezone
2362
+ now = datetime.now(timezone.utc).isoformat()
2363
+ tags = parse_mcp_list_arg(args.get("tags"))
2364
+ action = {
2365
+ "id": action_id,
2366
+ "title": args.get("title") or "",
2367
+ "description": args.get("description"),
2368
+ "priority": args.get("priority", 0),
2369
+ "status": args.get("status", "pending"),
2370
+ "tags": tags,
2371
+ "sessionId": args.get("sessionId"),
2372
+ "createdAt": now,
2373
+ "updatedAt": now,
2374
+ }
2375
+ kv.set(KV.actions, action_id, action)
2376
+ text_out = json.dumps({"action": action, "success": True}, indent=2)
2377
+
2378
+ elif name == "memory_action_update":
2379
+ action_id = args.get("actionId")
2380
+ if not action_id:
2381
+ return jsonify({"error": "actionId is required"}), 400
2382
+ existing = kv.get(KV.actions, action_id)
2383
+ if not existing:
2384
+ return jsonify({"error": "action not found"}), 404
2385
+ from datetime import datetime, timezone
2386
+ allowed_fields = {"title", "description", "priority", "status", "tags", "sessionId"}
2387
+ updates = {k: v for k, v in args.items() if k in allowed_fields}
2388
+ if "tags" in args:
2389
+ updates["tags"] = parse_mcp_list_arg(args.get("tags"))
2390
+ existing.update(updates)
2391
+ existing["updatedAt"] = datetime.now(timezone.utc).isoformat()
2392
+ kv.set(KV.actions, action_id, existing)
2393
+ text_out = json.dumps({"action": existing, "success": True}, indent=2)
2394
+
2395
+ elif name == "memory_frontier":
2396
+ items = kv.list(KV.actions)
2397
+ frontier = [a for a in items if a.get("status") in ("pending", "active")]
2398
+ frontier.sort(key=lambda a: (-(a.get("priority") or 0), a.get("createdAt", "")))
2399
+ text_out = json.dumps({"frontier": frontier[:50], "total": len(frontier)}, indent=2)
2400
+
2401
+ elif name == "memory_crystallize":
2402
+ session_id = args.get("sessionId")
2403
+ if not session_id:
2404
+ return jsonify({"error": "sessionId is required"}), 400
2405
+ res = functions.summarize(kv, {"sessionId": session_id})
2406
+ text_out = json.dumps(res, indent=2)
2407
+
2408
  else:
2409
  return jsonify({"error": f"unknown tool: {name}"}), 400
2410
 
src/mcp_stdio.py CHANGED
@@ -27,6 +27,244 @@ def send(msg):
27
  sys.stdout.write(line + "\n")
28
  sys.stdout.flush()
29
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
  def handle(req):
31
  method = req.get("method", "")
32
  req_id = req.get("id")
@@ -61,14 +299,17 @@ def handle(req):
61
  name = params.get("name")
62
  args = params.get("arguments") or {}
63
  try:
64
- r = requests.post(f"{BASE}/mcp/tools",
65
- headers=headers(),
66
- json={"name": name, "arguments": args},
67
- timeout=30)
68
- result = r.json()
69
- # MCP expects content array
70
- if "content" not in result:
71
- result = {"content": [{"type": "text", "text": json.dumps(result)}]}
 
 
 
72
  send({"jsonrpc": "2.0", "id": req_id, "result": result})
73
  except Exception as e:
74
  send({"jsonrpc": "2.0", "id": req_id,
 
27
  sys.stdout.write(line + "\n")
28
  sys.stdout.flush()
29
 
30
+ def perform_antigravity_sync_local(args):
31
+ import os
32
+ import json
33
+ import glob
34
+ import re
35
+ import requests
36
+
37
+ mode = args.get("mode") or "current_session"
38
+ current_conversation_id = args.get("currentConversationId")
39
+ current_folder = args.get("currentFolder")
40
+
41
+ brain_dir = os.path.join(os.path.expanduser("~"), ".gemini", "antigravity", "brain")
42
+ if not os.path.exists(brain_dir):
43
+ return {
44
+ "content": [{"type": "text", "text": json.dumps({
45
+ "success": False,
46
+ "syncedSessions": [],
47
+ "observationsAdded": 0,
48
+ "error": f"Brain directory not found at {brain_dir}"
49
+ })}]
50
+ }
51
+
52
+ pattern = os.path.join(brain_dir, "*", ".system_generated", "logs", "transcript.jsonl")
53
+ files = glob.glob(pattern)
54
+ if not files:
55
+ return {
56
+ "content": [{"type": "text", "text": json.dumps({
57
+ "success": True,
58
+ "syncedSessions": [],
59
+ "observationsAdded": 0
60
+ })}]
61
+ }
62
+
63
+ conversations = []
64
+ for fpath in files:
65
+ try:
66
+ mtime = os.path.getmtime(fpath)
67
+ convo_id = os.path.basename(os.path.dirname(os.path.dirname(os.path.dirname(fpath))))
68
+ conversations.append({
69
+ "id": convo_id,
70
+ "transcriptPath": fpath,
71
+ "mtime": mtime
72
+ })
73
+ except Exception:
74
+ pass
75
+
76
+ if not conversations:
77
+ return {
78
+ "content": [{"type": "text", "text": json.dumps({
79
+ "success": True,
80
+ "syncedSessions": [],
81
+ "observationsAdded": 0
82
+ })}]
83
+ }
84
+
85
+ conversations.sort(key=lambda x: x["mtime"], reverse=True)
86
+
87
+ targets = []
88
+ if mode == "current_session":
89
+ if current_conversation_id:
90
+ match = next((c for c in conversations if c["id"] == current_conversation_id), None)
91
+ if match:
92
+ targets = [match]
93
+ else:
94
+ targets = [conversations[0]]
95
+ elif mode == "current_folder":
96
+ target_folder = current_folder if current_folder else ""
97
+ if not target_folder:
98
+ target_folder = os.getcwd()
99
+
100
+ target_folder_norm = target_folder.replace("\\", "/").lower().strip()
101
+
102
+ for convo in conversations:
103
+ try:
104
+ with open(convo["transcriptPath"], "r", encoding="utf-8") as tf:
105
+ text = tf.read().lower()
106
+ text_norm = text.replace("\\/", "/").replace("\\\\", "/")
107
+ if target_folder_norm in text_norm:
108
+ targets.append(convo)
109
+ except Exception:
110
+ pass
111
+ elif mode == "all":
112
+ targets = conversations
113
+ else:
114
+ return {
115
+ "content": [{"type": "text", "text": json.dumps({
116
+ "success": False,
117
+ "syncedSessions": [],
118
+ "observationsAdded": 0,
119
+ "error": f"Invalid mode: {mode}"
120
+ })}]
121
+ }
122
+
123
+ if not targets:
124
+ return {
125
+ "content": [{"type": "text", "text": json.dumps({
126
+ "success": True,
127
+ "syncedSessions": [],
128
+ "observationsAdded": 0
129
+ })}]
130
+ }
131
+
132
+ synced_sessions = []
133
+ observations_added = 0
134
+
135
+ headers_dict = headers()
136
+
137
+ for convo in targets:
138
+ convo_id = convo["id"]
139
+ tpath = convo["transcriptPath"]
140
+ session_id = f"antigravity_{convo_id[:18].replace('-', '_')}"
141
+
142
+ project_path = None
143
+ try:
144
+ with open(tpath, "r", encoding="utf-8") as tf:
145
+ first_line = tf.readline()
146
+ if first_line:
147
+ step = json.loads(first_line)
148
+ match = re.search(r"\[([^\]]+)\]\s*->\s*\[([^\]]+)\]", step.get("content", ""))
149
+ if match:
150
+ project_path = match.group(2)
151
+ except Exception:
152
+ pass
153
+
154
+ if not project_path:
155
+ project_path = os.getcwd()
156
+
157
+ turns = []
158
+ current_prompt = None
159
+ current_timestamp = None
160
+
161
+ try:
162
+ with open(tpath, "r", encoding="utf-8") as tf:
163
+ for line in tf:
164
+ if not line.strip():
165
+ continue
166
+ try:
167
+ step = json.loads(line)
168
+ step_type = step.get("type")
169
+ if step_type == "USER_INPUT":
170
+ p_text = step.get("content", "")
171
+ if "<USER_REQUEST>" in p_text:
172
+ parts = p_text.split("<USER_REQUEST>")
173
+ if len(parts) > 1:
174
+ p_text = parts[1]
175
+ if "</USER_REQUEST>" in p_text:
176
+ p_text = p_text.split("</USER_REQUEST>")[0]
177
+ current_prompt = p_text.strip()
178
+ current_timestamp = step.get("created_at")
179
+ elif step_type == "PLANNER_RESPONSE" and current_prompt:
180
+ ts = current_timestamp or step.get("created_at")
181
+ if not ts:
182
+ import datetime
183
+ ts = datetime.datetime.utcnow().isoformat() + "Z"
184
+ turns.append({
185
+ "prompt": current_prompt,
186
+ "response": step.get("content", ""),
187
+ "timestamp": ts
188
+ })
189
+ current_prompt = None
190
+ current_timestamp = None
191
+ except Exception:
192
+ pass
193
+ except Exception:
194
+ continue
195
+
196
+ if not turns:
197
+ continue
198
+
199
+ existing_inputs = set()
200
+
201
+ try:
202
+ r = requests.get(f"{BASE}/observations?sessionId={session_id}", headers=headers_dict, timeout=10)
203
+ if r.status_code == 200:
204
+ obs_list = r.json().get("observations", [])
205
+ for obs in obs_list:
206
+ tool_input = obs.get("toolInput") or (obs.get("raw") or {}).get("tool_input")
207
+ if tool_input:
208
+ existing_inputs.add(tool_input.strip())
209
+ except Exception:
210
+ pass
211
+
212
+ try:
213
+ sess_check = requests.get(f"{BASE}/sessions", headers=headers_dict, timeout=5)
214
+ session_exists = False
215
+ if sess_check.status_code == 200:
216
+ sessions_list = sess_check.json().get("sessions", [])
217
+ session_exists = any(s.get("id") == session_id for s in sessions_list)
218
+
219
+ if not session_exists:
220
+ session_payload = {
221
+ "sessionId": session_id,
222
+ "project": project_path,
223
+ "cwd": project_path,
224
+ "title": f"Antigravity Pair Programming ({convo_id[:8]})",
225
+ "agentId": "antigravity"
226
+ }
227
+ requests.post(f"{BASE}/session/start", headers=headers_dict, json=session_payload, timeout=10)
228
+ except Exception:
229
+ pass
230
+
231
+ convo_synced = False
232
+ for turn in turns:
233
+ prompt = turn["prompt"]
234
+ if prompt.strip() in existing_inputs:
235
+ continue
236
+
237
+ payload = {
238
+ "sessionId": session_id,
239
+ "project": project_path,
240
+ "cwd": project_path,
241
+ "hookType": "post_tool_use",
242
+ "timestamp": turn["timestamp"],
243
+ "agentId": "antigravity",
244
+ "data": {
245
+ "tool_name": "conversation",
246
+ "tool_input": prompt,
247
+ "tool_output": turn["response"],
248
+ }
249
+ }
250
+ try:
251
+ requests.post(f"{BASE}/observe", headers=headers_dict, json=payload, timeout=10)
252
+ observations_added += 1
253
+ convo_synced = True
254
+ except Exception:
255
+ pass
256
+
257
+ if convo_synced:
258
+ synced_sessions.append(convo_id)
259
+
260
+ return {
261
+ "content": [{"type": "text", "text": json.dumps({
262
+ "success": True,
263
+ "syncedSessions": synced_sessions,
264
+ "observationsAdded": observations_added
265
+ })}]
266
+ }
267
+
268
  def handle(req):
269
  method = req.get("method", "")
270
  req_id = req.get("id")
 
299
  name = params.get("name")
300
  args = params.get("arguments") or {}
301
  try:
302
+ if name == "memory_antigravity_sync":
303
+ result = perform_antigravity_sync_local(args)
304
+ else:
305
+ r = requests.post(f"{BASE}/mcp/tools",
306
+ headers=headers(),
307
+ json={"name": name, "arguments": args},
308
+ timeout=30)
309
+ result = r.json()
310
+ # MCP expects content array
311
+ if "content" not in result:
312
+ result = {"content": [{"type": "text", "text": json.dumps(result)}]}
313
  send({"jsonrpc": "2.0", "id": req_id, "result": result})
314
  except Exception as e:
315
  send({"jsonrpc": "2.0", "id": req_id,