antimatter-ag2 1.0.0__tar.gz
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.
- antimatter_ag2-1.0.0/.gitignore +31 -0
- antimatter_ag2-1.0.0/LICENSE +21 -0
- antimatter_ag2-1.0.0/PKG-INFO +67 -0
- antimatter_ag2-1.0.0/README.md +20 -0
- antimatter_ag2-1.0.0/antimatter_ag2/__init__.py +0 -0
- antimatter_ag2-1.0.0/antimatter_ag2/agent_bridge.py +456 -0
- antimatter_ag2-1.0.0/antimatter_ag2/assets/plugin.json +5 -0
- antimatter_ag2-1.0.0/antimatter_ag2/assets/skills/antimatter-ag2/SKILL.md +28 -0
- antimatter_ag2-1.0.0/antimatter_ag2/cli.py +131 -0
- antimatter_ag2-1.0.0/antimatter_ag2/server.py +135 -0
- antimatter_ag2-1.0.0/pyproject.toml +55 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
node_modules/
|
|
2
|
+
dist/
|
|
3
|
+
build/
|
|
4
|
+
site/
|
|
5
|
+
.DS_Store
|
|
6
|
+
*.log
|
|
7
|
+
.env
|
|
8
|
+
reports/
|
|
9
|
+
.vscode/
|
|
10
|
+
.agents/
|
|
11
|
+
outputs/
|
|
12
|
+
.astro/
|
|
13
|
+
|
|
14
|
+
# Python
|
|
15
|
+
__pycache__/
|
|
16
|
+
*.py[cod]
|
|
17
|
+
*$py.class
|
|
18
|
+
*.so
|
|
19
|
+
.Python
|
|
20
|
+
env/
|
|
21
|
+
venv/
|
|
22
|
+
.venv/
|
|
23
|
+
pip-log.txt
|
|
24
|
+
pip-delete-this-directory.txt
|
|
25
|
+
.pytest_cache/
|
|
26
|
+
*.egg-info/
|
|
27
|
+
.hatch/
|
|
28
|
+
|
|
29
|
+
# VSIX / Extension Manifests
|
|
30
|
+
*.vsix
|
|
31
|
+
extension.vsixmanifest
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Saif Mukhtar
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: antimatter-ag2
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Antimatter AG2 Adapter — companion daemon for Google AntiGravity IDE
|
|
5
|
+
Project-URL: Homepage, https://saifmukhtar.dev
|
|
6
|
+
Project-URL: Repository, https://github.com/saifmukhtar/antimatter
|
|
7
|
+
Project-URL: Issues, https://github.com/saifmukhtar/antimatter/issues
|
|
8
|
+
Project-URL: Documentation, https://saifmukhtar.dev/docs/antimatter
|
|
9
|
+
Project-URL: Changelog, https://github.com/saifmukhtar/antimatter/releases
|
|
10
|
+
Author-email: Saif Mukhtar <saifmukhtar@saifmukhtar.dev>
|
|
11
|
+
License: MIT License
|
|
12
|
+
|
|
13
|
+
Copyright (c) 2026 Saif Mukhtar
|
|
14
|
+
|
|
15
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
16
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
17
|
+
in the Software without restriction, including without limitation the rights
|
|
18
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
19
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
20
|
+
furnished to do so, subject to the following conditions:
|
|
21
|
+
|
|
22
|
+
The above copyright notice and this permission notice shall be included in all
|
|
23
|
+
copies or substantial portions of the Software.
|
|
24
|
+
|
|
25
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
26
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
27
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
28
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
29
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
30
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
31
|
+
SOFTWARE.
|
|
32
|
+
License-File: LICENSE
|
|
33
|
+
Keywords: ai,android,antigravity,bridge,ide,mobile,websocket
|
|
34
|
+
Classifier: Development Status :: 4 - Beta
|
|
35
|
+
Classifier: Environment :: Console
|
|
36
|
+
Classifier: Intended Audience :: Developers
|
|
37
|
+
Classifier: Operating System :: OS Independent
|
|
38
|
+
Classifier: Topic :: Communications
|
|
39
|
+
Classifier: Topic :: Software Development
|
|
40
|
+
Requires-Python: >=3.11
|
|
41
|
+
Requires-Dist: google-antigravity==0.1.2
|
|
42
|
+
Requires-Dist: websockets==15.0.1
|
|
43
|
+
Provides-Extra: test
|
|
44
|
+
Requires-Dist: pytest-asyncio>=0.23.0; extra == 'test'
|
|
45
|
+
Requires-Dist: pytest>=7.0.0; extra == 'test'
|
|
46
|
+
Description-Content-Type: text/markdown
|
|
47
|
+
|
|
48
|
+
# Antimatter: Antigravity 2.0 Adapter (AG2)
|
|
49
|
+
|
|
50
|
+
This directory contains the Antimatter IPC adapter for the Antigravity 2.0 SDK CLI.
|
|
51
|
+
|
|
52
|
+
## Architecture
|
|
53
|
+
|
|
54
|
+
This adapter follows the **Independent Adapter Model**. It does NOT contain any complex networking, Cloudflare tunnels, or cryptographic pairing logic. Instead, it acts purely as a "dumb" IPC client that connects to the central Antimatter Gateway.
|
|
55
|
+
|
|
56
|
+
1. **Connection**: The daemon connects locally to the Gateway via WebSocket at `ws://127.0.0.1:8765`.
|
|
57
|
+
2. **Registration**: Upon connection, it sends `{"type": "REGISTER_ADAPTER", "name": "ag2"}`.
|
|
58
|
+
3. **Execution**: When you send a message from the Antimatter Android app targeting AG2, the Gateway securely routes that payload to this adapter. This adapter interfaces with the Antigravity 2.0 SDK by monitoring the `.system_generated/logs/transcript.jsonl` files and submitting local Python subprocess commands.
|
|
59
|
+
|
|
60
|
+
## Building
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
uv build
|
|
64
|
+
uv tool install .
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
For full system documentation, please see the `docs/` folder in the repository root.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Antimatter: Antigravity 2.0 Adapter (AG2)
|
|
2
|
+
|
|
3
|
+
This directory contains the Antimatter IPC adapter for the Antigravity 2.0 SDK CLI.
|
|
4
|
+
|
|
5
|
+
## Architecture
|
|
6
|
+
|
|
7
|
+
This adapter follows the **Independent Adapter Model**. It does NOT contain any complex networking, Cloudflare tunnels, or cryptographic pairing logic. Instead, it acts purely as a "dumb" IPC client that connects to the central Antimatter Gateway.
|
|
8
|
+
|
|
9
|
+
1. **Connection**: The daemon connects locally to the Gateway via WebSocket at `ws://127.0.0.1:8765`.
|
|
10
|
+
2. **Registration**: Upon connection, it sends `{"type": "REGISTER_ADAPTER", "name": "ag2"}`.
|
|
11
|
+
3. **Execution**: When you send a message from the Antimatter Android app targeting AG2, the Gateway securely routes that payload to this adapter. This adapter interfaces with the Antigravity 2.0 SDK by monitoring the `.system_generated/logs/transcript.jsonl` files and submitting local Python subprocess commands.
|
|
12
|
+
|
|
13
|
+
## Building
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
uv build
|
|
17
|
+
uv tool install .
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
For full system documentation, please see the `docs/` folder in the repository root.
|
|
File without changes
|
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import glob
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
|
|
10
|
+
class AgentBridge:
|
|
11
|
+
def __init__(self, websocket, app_data_dir: str, current_convo_id: str):
|
|
12
|
+
self.websocket = websocket
|
|
13
|
+
self.agent = None
|
|
14
|
+
self.app_data_dir = app_data_dir
|
|
15
|
+
self.conversation_id = current_convo_id
|
|
16
|
+
self.step_index = 0
|
|
17
|
+
# BUG-005: Initialize last_position here so poll_transcript never raises AttributeError
|
|
18
|
+
# even if send_transcript was not called first
|
|
19
|
+
self.last_position = 0
|
|
20
|
+
# BUG-006: Stop event for graceful shutdown of poll_transcript
|
|
21
|
+
self._stop_event = asyncio.Event()
|
|
22
|
+
self._watch_task = None
|
|
23
|
+
|
|
24
|
+
async def _send_step(self, case: str, value: str = None, tool: str = None):
|
|
25
|
+
step_payload = {"case": case}
|
|
26
|
+
if value is not None:
|
|
27
|
+
step_payload["value"] = value
|
|
28
|
+
if tool is not None:
|
|
29
|
+
step_payload["tool"] = tool
|
|
30
|
+
|
|
31
|
+
await self.websocket.send(json.dumps({
|
|
32
|
+
"type": "STEP",
|
|
33
|
+
"index": self.step_index,
|
|
34
|
+
"step": step_payload
|
|
35
|
+
}))
|
|
36
|
+
self.step_index += 1
|
|
37
|
+
|
|
38
|
+
async def _send_generating(self):
|
|
39
|
+
await self.websocket.send(json.dumps({
|
|
40
|
+
"type": "GENERATING",
|
|
41
|
+
"conversationId": self.conversation_id
|
|
42
|
+
}))
|
|
43
|
+
|
|
44
|
+
async def _send_response_complete(self):
|
|
45
|
+
await self.websocket.send(json.dumps({
|
|
46
|
+
"type": "RESPONSE_COMPLETE",
|
|
47
|
+
"conversationId": self.conversation_id
|
|
48
|
+
}))
|
|
49
|
+
|
|
50
|
+
async def send_history(self):
|
|
51
|
+
brain_dir = os.path.join(self.app_data_dir, "brain")
|
|
52
|
+
conversations = []
|
|
53
|
+
if os.path.exists(brain_dir):
|
|
54
|
+
import json, re
|
|
55
|
+
for d in os.listdir(brain_dir):
|
|
56
|
+
d_path = os.path.join(brain_dir, d)
|
|
57
|
+
logs_dir = os.path.join(d_path, ".system_generated", "logs")
|
|
58
|
+
if os.path.isdir(logs_dir):
|
|
59
|
+
transcript_path = os.path.join(logs_dir, "transcript.jsonl")
|
|
60
|
+
if os.path.exists(transcript_path):
|
|
61
|
+
timestamp = int(os.path.getmtime(transcript_path) * 1000)
|
|
62
|
+
title = "New Conversation"
|
|
63
|
+
try:
|
|
64
|
+
with open(transcript_path, 'r', encoding='utf-8') as f:
|
|
65
|
+
for line in f:
|
|
66
|
+
try:
|
|
67
|
+
data = json.loads(line)
|
|
68
|
+
if data.get("type") == "USER_INPUT":
|
|
69
|
+
content = data.get("content", "")
|
|
70
|
+
match = re.search(r'<USER_REQUEST>\s*(.*?)\s*</USER_REQUEST>', content, re.DOTALL)
|
|
71
|
+
if match:
|
|
72
|
+
title = match.group(1).strip()
|
|
73
|
+
else:
|
|
74
|
+
title = re.sub(r'<[^>]+>', '', content).strip()
|
|
75
|
+
# Truncate title
|
|
76
|
+
if len(title) > 50:
|
|
77
|
+
title = title[:47] + "..."
|
|
78
|
+
break
|
|
79
|
+
except Exception:
|
|
80
|
+
pass
|
|
81
|
+
except Exception:
|
|
82
|
+
pass
|
|
83
|
+
|
|
84
|
+
conversations.append({
|
|
85
|
+
"id": d,
|
|
86
|
+
"timestamp": timestamp,
|
|
87
|
+
"title": title
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
# Sort descending by timestamp
|
|
91
|
+
conversations.sort(key=lambda x: x["timestamp"], reverse=True)
|
|
92
|
+
|
|
93
|
+
if not conversations:
|
|
94
|
+
conversations = [{
|
|
95
|
+
"id": self.conversation_id,
|
|
96
|
+
"timestamp": 0,
|
|
97
|
+
"title": "Current Session"
|
|
98
|
+
}]
|
|
99
|
+
|
|
100
|
+
await self.websocket.send(json.dumps({
|
|
101
|
+
"type": "HISTORY_LIST",
|
|
102
|
+
"conversations": conversations
|
|
103
|
+
}))
|
|
104
|
+
|
|
105
|
+
def _is_boilerplate(self, text: str) -> bool:
|
|
106
|
+
t = text.lower()
|
|
107
|
+
if t.startswith("#") and ("tool" in t or "instruction" in t):
|
|
108
|
+
return True
|
|
109
|
+
keywords = [
|
|
110
|
+
"critical instruction", "tool specificity", "tool usage",
|
|
111
|
+
"tool ecosystem", "tool repertoire", "instruction to avoid cat",
|
|
112
|
+
"eliminating the use of ls", "default to grep_search",
|
|
113
|
+
"actively avoiding using ls", "avoiding cat within bash",
|
|
114
|
+
"before making tool calls", "list related tools",
|
|
115
|
+
"tool list: `", "i must prioritize using the most specific tool"
|
|
116
|
+
]
|
|
117
|
+
return any(k in t for k in keywords)
|
|
118
|
+
|
|
119
|
+
def _parse_line(self, line: str):
|
|
120
|
+
import json
|
|
121
|
+
import re
|
|
122
|
+
try:
|
|
123
|
+
data = json.loads(line)
|
|
124
|
+
msg_type = data.get("type")
|
|
125
|
+
content = data.get("content", "")
|
|
126
|
+
|
|
127
|
+
steps_to_return = []
|
|
128
|
+
|
|
129
|
+
def add_step(case_type, value):
|
|
130
|
+
steps_to_return.append({
|
|
131
|
+
"index": self.step_index,
|
|
132
|
+
"step": {"case": case_type, "value": value}
|
|
133
|
+
})
|
|
134
|
+
self.step_index += 1
|
|
135
|
+
|
|
136
|
+
if msg_type == "USER_INPUT":
|
|
137
|
+
match = re.search(r'<USER_REQUEST>\s*(.*?)\s*</USER_REQUEST>', content, re.DOTALL)
|
|
138
|
+
if match:
|
|
139
|
+
content = match.group(1).strip()
|
|
140
|
+
else:
|
|
141
|
+
content = re.sub(r'<[^>]+>', '', content).strip()
|
|
142
|
+
add_step("userInput", content)
|
|
143
|
+
elif msg_type == "SYSTEM_MESSAGE":
|
|
144
|
+
match = re.search(r'content=(.*?)(?:\n</SYSTEM_MESSAGE>|$)', content, re.DOTALL)
|
|
145
|
+
if match:
|
|
146
|
+
parsed_content = match.group(1).strip()
|
|
147
|
+
if not parsed_content.startswith("Task id") and not parsed_content.startswith("Tool is running"):
|
|
148
|
+
add_step("SYSTEM_MESSAGE", parsed_content)
|
|
149
|
+
elif msg_type == "PLANNER_RESPONSE":
|
|
150
|
+
thinking = data.get("thinking", "")
|
|
151
|
+
if thinking:
|
|
152
|
+
clean_thinking = [
|
|
153
|
+
paragraph for paragraph in thinking.split("\n\n")
|
|
154
|
+
if not self._is_boilerplate(paragraph)
|
|
155
|
+
]
|
|
156
|
+
|
|
157
|
+
final_thinking = "\n\n".join(clean_thinking).strip()
|
|
158
|
+
if final_thinking:
|
|
159
|
+
stripped_of_punctuation = re.sub(r'[#\s*\-=_]+', '', final_thinking).strip()
|
|
160
|
+
if stripped_of_punctuation:
|
|
161
|
+
add_step("plannerResponse", final_thinking)
|
|
162
|
+
if content:
|
|
163
|
+
add_step("text", content)
|
|
164
|
+
elif msg_type == "SYSTEM_ALERT":
|
|
165
|
+
add_step("ephemeralMessage", content)
|
|
166
|
+
elif msg_type == "MODEL_TEXT":
|
|
167
|
+
add_step("text", content)
|
|
168
|
+
elif msg_type in ["VIEW_FILE", "LIST_DIRECTORY", "CODE_ACTION", "GREP_SEARCH", "READ_URL_CONTENT", "SEARCH_WEB"]:
|
|
169
|
+
pass
|
|
170
|
+
elif msg_type == "ERROR_MESSAGE":
|
|
171
|
+
add_step("errorMessage", content)
|
|
172
|
+
|
|
173
|
+
return steps_to_return
|
|
174
|
+
except Exception:
|
|
175
|
+
return []
|
|
176
|
+
|
|
177
|
+
async def send_transcript(self, convo_id: str):
|
|
178
|
+
transcript_path = os.path.join(self.app_data_dir, "brain", convo_id, ".system_generated", "logs", "transcript.jsonl")
|
|
179
|
+
steps = []
|
|
180
|
+
|
|
181
|
+
self.step_index = 0
|
|
182
|
+
self.last_position = 0
|
|
183
|
+
|
|
184
|
+
if os.path.exists(transcript_path):
|
|
185
|
+
try:
|
|
186
|
+
with open(transcript_path, 'r', encoding='utf-8') as f:
|
|
187
|
+
for line in f:
|
|
188
|
+
parsed_steps = self._parse_line(line)
|
|
189
|
+
if parsed_steps:
|
|
190
|
+
steps.extend(parsed_steps)
|
|
191
|
+
self.last_position = f.tell()
|
|
192
|
+
|
|
193
|
+
except Exception as e:
|
|
194
|
+
print(f"[Gateway] ERROR in reading transcript: {e}", flush=True)
|
|
195
|
+
|
|
196
|
+
await self.websocket.send(json.dumps({
|
|
197
|
+
"type": "SESSION_STATE",
|
|
198
|
+
"conversationId": convo_id,
|
|
199
|
+
"stepCount": len(steps)
|
|
200
|
+
}))
|
|
201
|
+
|
|
202
|
+
await self.websocket.send(json.dumps({
|
|
203
|
+
"type": "STEP_BATCH",
|
|
204
|
+
"conversationId": convo_id,
|
|
205
|
+
"steps": steps
|
|
206
|
+
}))
|
|
207
|
+
|
|
208
|
+
async def poll_transcript(self):
|
|
209
|
+
"""Poll the transcript file for new lines, with graceful shutdown and debouncing."""
|
|
210
|
+
import asyncio
|
|
211
|
+
current_opened_convo_id = None
|
|
212
|
+
f = None
|
|
213
|
+
|
|
214
|
+
try:
|
|
215
|
+
while not self._stop_event.is_set():
|
|
216
|
+
if current_opened_convo_id != self.conversation_id:
|
|
217
|
+
if f:
|
|
218
|
+
f.close()
|
|
219
|
+
transcript_path = os.path.join(
|
|
220
|
+
self.app_data_dir, "brain", self.conversation_id,
|
|
221
|
+
".system_generated", "logs", "transcript.jsonl"
|
|
222
|
+
)
|
|
223
|
+
# Wait for file to exist
|
|
224
|
+
while not os.path.exists(transcript_path):
|
|
225
|
+
if self._stop_event.is_set() or current_opened_convo_id != self.conversation_id:
|
|
226
|
+
break
|
|
227
|
+
await asyncio.sleep(1)
|
|
228
|
+
|
|
229
|
+
if self._stop_event.is_set():
|
|
230
|
+
break
|
|
231
|
+
|
|
232
|
+
if os.path.exists(transcript_path):
|
|
233
|
+
f = open(transcript_path, 'r', encoding='utf-8')
|
|
234
|
+
f.seek(self.last_position)
|
|
235
|
+
current_opened_convo_id = self.conversation_id
|
|
236
|
+
|
|
237
|
+
if f:
|
|
238
|
+
line = f.readline()
|
|
239
|
+
if not line:
|
|
240
|
+
await asyncio.sleep(0.1)
|
|
241
|
+
continue
|
|
242
|
+
|
|
243
|
+
parsed_steps = self._parse_line(line)
|
|
244
|
+
if parsed_steps:
|
|
245
|
+
for s in parsed_steps:
|
|
246
|
+
if s["step"]["case"] in ["userInput", "toolCall"]:
|
|
247
|
+
await self._send_generating()
|
|
248
|
+
elif s["step"]["case"] in ["text", "errorMessage", "ephemeralMessage"]:
|
|
249
|
+
await self._send_response_complete()
|
|
250
|
+
|
|
251
|
+
await self.websocket.send(json.dumps({
|
|
252
|
+
"type": "STEP_BATCH",
|
|
253
|
+
"conversationId": self.conversation_id,
|
|
254
|
+
"steps": parsed_steps
|
|
255
|
+
}))
|
|
256
|
+
else:
|
|
257
|
+
await asyncio.sleep(0.5)
|
|
258
|
+
finally:
|
|
259
|
+
if f:
|
|
260
|
+
f.close()
|
|
261
|
+
|
|
262
|
+
async def watch_brain_dir(self):
|
|
263
|
+
brain_dir = os.path.join(self.app_data_dir, "brain")
|
|
264
|
+
while not self._stop_event.is_set():
|
|
265
|
+
try:
|
|
266
|
+
if os.path.exists(brain_dir):
|
|
267
|
+
subdirs = [os.path.join(brain_dir, d) for d in os.listdir(brain_dir) if os.path.isdir(os.path.join(brain_dir, d))]
|
|
268
|
+
if subdirs:
|
|
269
|
+
newest_dir = max(subdirs, key=os.path.getmtime)
|
|
270
|
+
newest_convo_id = os.path.basename(newest_dir)
|
|
271
|
+
if newest_convo_id != "scratch":
|
|
272
|
+
# Only broadcast history so the sidebar updates, don't force a UI switch
|
|
273
|
+
await self.send_history()
|
|
274
|
+
except Exception:
|
|
275
|
+
pass
|
|
276
|
+
await asyncio.sleep(2)
|
|
277
|
+
|
|
278
|
+
async def send_artifacts(self, convo_id: str):
|
|
279
|
+
artifacts_dir = os.path.join(self.app_data_dir, "brain", convo_id)
|
|
280
|
+
artifacts = []
|
|
281
|
+
if os.path.exists(artifacts_dir):
|
|
282
|
+
for file in os.listdir(artifacts_dir):
|
|
283
|
+
if file.endswith(".md") and file not in ["task.md", "walkthrough.md", "implementation_plan.md"]:
|
|
284
|
+
artifacts.append({
|
|
285
|
+
"name": file,
|
|
286
|
+
"path": os.path.join(artifacts_dir, file),
|
|
287
|
+
"isDir": False
|
|
288
|
+
})
|
|
289
|
+
# Add standard artifacts
|
|
290
|
+
for std in ["implementation_plan.md", "task.md", "walkthrough.md"]:
|
|
291
|
+
if os.path.exists(os.path.join(artifacts_dir, std)):
|
|
292
|
+
artifacts.append({
|
|
293
|
+
"name": std,
|
|
294
|
+
"path": os.path.join(artifacts_dir, std),
|
|
295
|
+
"isDir": False
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
await self.websocket.send(json.dumps({
|
|
299
|
+
"type": "ARTIFACTS_LIST",
|
|
300
|
+
"artifacts": artifacts
|
|
301
|
+
}))
|
|
302
|
+
|
|
303
|
+
async def read_artifact(self, path: str):
|
|
304
|
+
import pathlib
|
|
305
|
+
p = pathlib.Path(path).resolve()
|
|
306
|
+
brain_dir = pathlib.Path(self.app_data_dir) / "brain"
|
|
307
|
+
try:
|
|
308
|
+
p.relative_to(brain_dir.resolve())
|
|
309
|
+
except ValueError:
|
|
310
|
+
return # Path traversal rejected
|
|
311
|
+
|
|
312
|
+
if p.exists():
|
|
313
|
+
try:
|
|
314
|
+
with open(p, 'r', encoding='utf-8') as f:
|
|
315
|
+
content = f.read()
|
|
316
|
+
await self.websocket.send(json.dumps({
|
|
317
|
+
"type": "ARTIFACT_CONTENT",
|
|
318
|
+
"path": str(p),
|
|
319
|
+
"content": content
|
|
320
|
+
}))
|
|
321
|
+
except Exception:
|
|
322
|
+
pass
|
|
323
|
+
|
|
324
|
+
async def send_workspace(self, root_path=None):
|
|
325
|
+
if root_path is None:
|
|
326
|
+
root_path = os.getcwd()
|
|
327
|
+
|
|
328
|
+
IGNORED = {
|
|
329
|
+
'node_modules', '.git', 'dist', 'build', 'out', '.gradle',
|
|
330
|
+
'__pycache__', '.venv', 'venv', '.idea', '.DS_Store', '.kotlin',
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
# Performance: Build the file tree in a thread pool so the synchronous
|
|
334
|
+
# os.listdir/os.path.isdir calls don't block the asyncio event loop.
|
|
335
|
+
def build_tree(path: str, depth: int = 0) -> list:
|
|
336
|
+
if depth > 2:
|
|
337
|
+
return []
|
|
338
|
+
nodes = []
|
|
339
|
+
try:
|
|
340
|
+
for item in sorted(os.listdir(path)):
|
|
341
|
+
if item.startswith('.') or item in IGNORED:
|
|
342
|
+
continue
|
|
343
|
+
full_path = os.path.join(path, item)
|
|
344
|
+
is_dir = os.path.isdir(full_path)
|
|
345
|
+
node = {
|
|
346
|
+
"name": item,
|
|
347
|
+
"path": full_path,
|
|
348
|
+
"isDir": is_dir
|
|
349
|
+
}
|
|
350
|
+
if is_dir and depth < 2:
|
|
351
|
+
node["children"] = build_tree(full_path, depth + 1)
|
|
352
|
+
nodes.append(node)
|
|
353
|
+
except PermissionError:
|
|
354
|
+
pass
|
|
355
|
+
except Exception:
|
|
356
|
+
pass
|
|
357
|
+
# Directories first, then files, both alphabetical
|
|
358
|
+
return sorted(nodes, key=lambda x: (not x["isDir"], x["name"]))
|
|
359
|
+
|
|
360
|
+
# Run synchronous tree scan off the event loop
|
|
361
|
+
tree = await asyncio.to_thread(build_tree, root_path)
|
|
362
|
+
await self.websocket.send(json.dumps({
|
|
363
|
+
"type": "FILE_TREE",
|
|
364
|
+
"tree": tree
|
|
365
|
+
}))
|
|
366
|
+
|
|
367
|
+
async def read_file(self, path: str):
|
|
368
|
+
import pathlib
|
|
369
|
+
workspace = pathlib.Path(os.getcwd()).resolve()
|
|
370
|
+
p = pathlib.Path(path)
|
|
371
|
+
if not p.is_absolute():
|
|
372
|
+
p = workspace / p
|
|
373
|
+
p = p.resolve()
|
|
374
|
+
|
|
375
|
+
try:
|
|
376
|
+
p.relative_to(workspace)
|
|
377
|
+
except ValueError:
|
|
378
|
+
await self.websocket.send(json.dumps({
|
|
379
|
+
"type": "ERROR",
|
|
380
|
+
"message": "Path traversal rejected"
|
|
381
|
+
}))
|
|
382
|
+
return
|
|
383
|
+
|
|
384
|
+
try:
|
|
385
|
+
with open(p, 'r', encoding='utf-8') as f:
|
|
386
|
+
content = f.read()
|
|
387
|
+
await self.websocket.send(json.dumps({
|
|
388
|
+
"type": "FILE_CONTENT",
|
|
389
|
+
"path": str(p),
|
|
390
|
+
"content": content,
|
|
391
|
+
"language": "markdown" if str(p).endswith(".md") else "text"
|
|
392
|
+
}))
|
|
393
|
+
except Exception as e:
|
|
394
|
+
await self.websocket.send(json.dumps({
|
|
395
|
+
"type": "ERROR",
|
|
396
|
+
"message": f"Failed to read file: {e}"
|
|
397
|
+
}))
|
|
398
|
+
|
|
399
|
+
async def process_message(self, text: str, images: list = None):
|
|
400
|
+
if images is None:
|
|
401
|
+
images = []
|
|
402
|
+
|
|
403
|
+
# Process images
|
|
404
|
+
if images:
|
|
405
|
+
import base64
|
|
406
|
+
import time
|
|
407
|
+
scratch_dir = os.path.join(self.app_data_dir, "brain", self.conversation_id, "scratch")
|
|
408
|
+
os.makedirs(scratch_dir, exist_ok=True)
|
|
409
|
+
|
|
410
|
+
for i, b64_str in enumerate(images):
|
|
411
|
+
try:
|
|
412
|
+
if "," in b64_str:
|
|
413
|
+
b64_str = b64_str.split(",")[1]
|
|
414
|
+
|
|
415
|
+
img_data = base64.b64decode(b64_str)
|
|
416
|
+
timestamp = int(time.time() * 1000)
|
|
417
|
+
filename = f"upload_{timestamp}_{i}.jpg"
|
|
418
|
+
filepath = os.path.join(scratch_dir, filename)
|
|
419
|
+
|
|
420
|
+
with open(filepath, "wb") as f:
|
|
421
|
+
f.write(img_data)
|
|
422
|
+
|
|
423
|
+
text += f"\\n\\n"
|
|
424
|
+
except Exception as e:
|
|
425
|
+
print(f"Failed to decode image {i}: {e}")
|
|
426
|
+
|
|
427
|
+
# We DO NOT send userInput directly anymore because Agent.chat() or agentapi will append it to the file,
|
|
428
|
+
# and poll_transcript will pick it up and broadcast it properly, avoiding duplicates.
|
|
429
|
+
await self._send_generating()
|
|
430
|
+
|
|
431
|
+
try:
|
|
432
|
+
import asyncio
|
|
433
|
+
agentapi_path = os.path.expanduser("~/.gemini/antigravity/bin/agentapi")
|
|
434
|
+
|
|
435
|
+
proc = await asyncio.create_subprocess_exec(
|
|
436
|
+
agentapi_path, "send-message", str(self.conversation_id), str(text),
|
|
437
|
+
stdout=asyncio.subprocess.PIPE,
|
|
438
|
+
stderr=asyncio.subprocess.PIPE
|
|
439
|
+
)
|
|
440
|
+
stdout, stderr = await proc.communicate()
|
|
441
|
+
|
|
442
|
+
if proc.returncode != 0:
|
|
443
|
+
if b"ANTIGRAVITY_LS_ADDRESS is not set" in stderr:
|
|
444
|
+
logger.error("FATAL: Adapter MUST be started from inside the Antigravity IDE integrated terminal!")
|
|
445
|
+
await self._send_step("errorMessage", f"Failed to inject message into IDE: {stderr.decode()}")
|
|
446
|
+
else:
|
|
447
|
+
await self._send_step("ephemeralMessage", "Message queued to IDE Agent.")
|
|
448
|
+
except Exception as e:
|
|
449
|
+
await self._send_step("errorMessage", f"Agent Error: {str(e)}")
|
|
450
|
+
finally:
|
|
451
|
+
await self._send_response_complete()
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
async def cleanup(self):
|
|
455
|
+
# BUG-006: Signal poll_transcript to stop gracefully
|
|
456
|
+
self._stop_event.set()
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: antimatter-ag2
|
|
3
|
+
description: "Starts the Antimatter AG2 adapter to connect the IDE to the Gateway. Use this skill when the user asks to start, manage, or connect their AG2 adapter."
|
|
4
|
+
---
|
|
5
|
+
# Antimatter AG2 Adapter Skill
|
|
6
|
+
|
|
7
|
+
This skill teaches the agent how to launch and manage the Antimatter AG2 Adapter natively.
|
|
8
|
+
|
|
9
|
+
## How to use this skill
|
|
10
|
+
|
|
11
|
+
1. When the user asks to start the adapter, execute the following command (using the `run_command` tool):
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# Start the Antigravity 2.0 adapter (it will automatically run in the background)
|
|
15
|
+
antimatter-ag2 start
|
|
16
|
+
```
|
|
17
|
+
*(If developing locally, you can run `uv run antimatter-ag2 start` from its directory)*
|
|
18
|
+
|
|
19
|
+
2. The daemon runs indefinitely and handles all IDE communication seamlessly via the Google Antigravity SDK. You do not need to intervene in the process once it has started.
|
|
20
|
+
|
|
21
|
+
## Stopping or Restarting the Adapter
|
|
22
|
+
|
|
23
|
+
If the user asks to "stop", "terminate", or "kill" the Antimatter Adapter, simply run:
|
|
24
|
+
```bash
|
|
25
|
+
antimatter-ag2 stop
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
If the user asks to "restart" the adapter, run `stop` followed by `start`.
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import shutil
|
|
4
|
+
import argparse
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from antimatter_ag2 import server
|
|
7
|
+
|
|
8
|
+
def init_plugin():
|
|
9
|
+
"""Initializes the plugin by copying assets into the Antigravity IDE plugins directory."""
|
|
10
|
+
print("Initializing Antimatter AG2 plugin...")
|
|
11
|
+
|
|
12
|
+
home_dir = Path.home()
|
|
13
|
+
plugin_dir = home_dir / ".gemini" / "config" / "plugins" / "antimatter-ag2"
|
|
14
|
+
|
|
15
|
+
# Define source assets path
|
|
16
|
+
assets_dir = Path(__file__).parent / "assets"
|
|
17
|
+
|
|
18
|
+
if not assets_dir.exists():
|
|
19
|
+
print(f"Error: Could not find assets directory at {assets_dir}", file=sys.stderr)
|
|
20
|
+
sys.exit(1)
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
# Create target directories
|
|
24
|
+
print(f"Creating directory: {plugin_dir}")
|
|
25
|
+
plugin_dir.mkdir(parents=True, exist_ok=True)
|
|
26
|
+
|
|
27
|
+
# 1. Copy plugin.json
|
|
28
|
+
print("Copying plugin.json...")
|
|
29
|
+
shutil.copy2(assets_dir / "plugin.json", plugin_dir / "plugin.json")
|
|
30
|
+
|
|
31
|
+
# 2. Create skills directory and copy SKILL.md
|
|
32
|
+
skill_dir = plugin_dir / "skills" / "antimatter-ag2"
|
|
33
|
+
print(f"Creating directory: {skill_dir}")
|
|
34
|
+
skill_dir.mkdir(parents=True, exist_ok=True)
|
|
35
|
+
|
|
36
|
+
print("Copying SKILL.md...")
|
|
37
|
+
shutil.copy2(assets_dir / "skills" / "antimatter-ag2" / "SKILL.md", skill_dir / "SKILL.md")
|
|
38
|
+
|
|
39
|
+
print("\n✅ Successfully initialized Antimatter AG2 Adapter!")
|
|
40
|
+
print("You can now open Antigravity 2.0 and say: 'Start my Antimatter adapter'")
|
|
41
|
+
|
|
42
|
+
except Exception as e:
|
|
43
|
+
print(f"Failed to initialize plugin: {e}", file=sys.stderr)
|
|
44
|
+
sys.exit(1)
|
|
45
|
+
|
|
46
|
+
def start_server():
|
|
47
|
+
"""Starts the WebSocket bridge adapter client in the background."""
|
|
48
|
+
print("Starting Antimatter AG2 Adapter in background...")
|
|
49
|
+
|
|
50
|
+
app_data = Path.home() / ".gemini" / "antigravity"
|
|
51
|
+
app_data.mkdir(parents=True, exist_ok=True)
|
|
52
|
+
pid_file = app_data / ".ag2.pid"
|
|
53
|
+
|
|
54
|
+
if pid_file.exists():
|
|
55
|
+
try:
|
|
56
|
+
pid = int(pid_file.read_text())
|
|
57
|
+
os.kill(pid, 0)
|
|
58
|
+
print("Adapter is already running.")
|
|
59
|
+
return
|
|
60
|
+
except (ValueError, OSError):
|
|
61
|
+
pid_file.unlink(missing_ok=True)
|
|
62
|
+
|
|
63
|
+
import subprocess
|
|
64
|
+
|
|
65
|
+
process = subprocess.Popen(
|
|
66
|
+
[sys.executable, "-m", "antimatter_ag2.cli", "run_server"],
|
|
67
|
+
stdout=subprocess.DEVNULL,
|
|
68
|
+
stderr=subprocess.DEVNULL,
|
|
69
|
+
start_new_session=True
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
pid_file.write_text(str(process.pid))
|
|
73
|
+
print(f"✅ Adapter started successfully (PID: {process.pid}).")
|
|
74
|
+
|
|
75
|
+
def stop_server():
|
|
76
|
+
"""Stops the background adapter."""
|
|
77
|
+
pid_file = Path.home() / ".gemini" / "antigravity" / ".ag2.pid"
|
|
78
|
+
if not pid_file.exists():
|
|
79
|
+
print("Adapter is not running.")
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
pid = int(pid_file.read_text())
|
|
84
|
+
import signal
|
|
85
|
+
os.kill(pid, signal.SIGTERM)
|
|
86
|
+
print("Adapter stopped successfully.")
|
|
87
|
+
except Exception as e:
|
|
88
|
+
print(f"Failed to stop adapter or it was not running: {e}")
|
|
89
|
+
finally:
|
|
90
|
+
pid_file.unlink(missing_ok=True)
|
|
91
|
+
|
|
92
|
+
def run_server():
|
|
93
|
+
"""Actually runs the server loop (internal use)."""
|
|
94
|
+
try:
|
|
95
|
+
import asyncio
|
|
96
|
+
asyncio.run(server.main())
|
|
97
|
+
except KeyboardInterrupt:
|
|
98
|
+
pass
|
|
99
|
+
|
|
100
|
+
def main():
|
|
101
|
+
parser = argparse.ArgumentParser(description="Antimatter AG2 CLI")
|
|
102
|
+
subparsers = parser.add_subparsers(dest="command", help="Available commands")
|
|
103
|
+
|
|
104
|
+
# Init command
|
|
105
|
+
init_parser = subparsers.add_parser("init", help="Initialize the plugin in the Antigravity SDK directory")
|
|
106
|
+
|
|
107
|
+
# Start command
|
|
108
|
+
start_parser = subparsers.add_parser("start", help="Start the WebSocket daemon in the background")
|
|
109
|
+
|
|
110
|
+
# Stop command
|
|
111
|
+
stop_parser = subparsers.add_parser("stop", help="Stop the background daemon")
|
|
112
|
+
|
|
113
|
+
# Run server command (internal)
|
|
114
|
+
run_parser = subparsers.add_parser("run_server", help=argparse.SUPPRESS)
|
|
115
|
+
|
|
116
|
+
args = parser.parse_args()
|
|
117
|
+
|
|
118
|
+
if args.command == "init":
|
|
119
|
+
init_plugin()
|
|
120
|
+
elif args.command == "start":
|
|
121
|
+
start_server()
|
|
122
|
+
elif args.command == "stop":
|
|
123
|
+
stop_server()
|
|
124
|
+
elif args.command == "run_server":
|
|
125
|
+
run_server()
|
|
126
|
+
else:
|
|
127
|
+
parser.print_help()
|
|
128
|
+
sys.exit(1)
|
|
129
|
+
|
|
130
|
+
if __name__ == "__main__":
|
|
131
|
+
main()
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
import websockets
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from .agent_bridge import AgentBridge
|
|
8
|
+
|
|
9
|
+
logging.basicConfig(level=logging.WARNING)
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
APP_DATA_DIR = Path(os.environ.get("AGY_APP_DATA_DIR", os.path.expanduser("~/.gemini/antigravity")))
|
|
13
|
+
|
|
14
|
+
def get_latest_conversation_id(app_data_dir: Path) -> str:
|
|
15
|
+
brain_dir = app_data_dir / "brain"
|
|
16
|
+
if not brain_dir.exists():
|
|
17
|
+
return "default"
|
|
18
|
+
|
|
19
|
+
conv_dirs = [d.name for d in brain_dir.iterdir() if d.is_dir() and (brain_dir / d / ".system_generated").exists()]
|
|
20
|
+
if not conv_dirs:
|
|
21
|
+
return "default"
|
|
22
|
+
|
|
23
|
+
conv_dirs.sort(key=lambda d: (brain_dir / d).stat().st_mtime, reverse=True)
|
|
24
|
+
return conv_dirs[0]
|
|
25
|
+
|
|
26
|
+
def get_default_workspace() -> str:
|
|
27
|
+
if "AGY_WORKSPACE_DIR" in os.environ:
|
|
28
|
+
return os.environ["AGY_WORKSPACE_DIR"]
|
|
29
|
+
try:
|
|
30
|
+
current = Path(__file__).resolve().parent
|
|
31
|
+
while current != current.parent:
|
|
32
|
+
if (current / ".git").exists():
|
|
33
|
+
return str(current)
|
|
34
|
+
current = current.parent
|
|
35
|
+
except Exception:
|
|
36
|
+
pass
|
|
37
|
+
return os.getcwd()
|
|
38
|
+
|
|
39
|
+
async def main():
|
|
40
|
+
uri = "ws://127.0.0.1:8765"
|
|
41
|
+
logger.info(f"[AG2 Adapter] Connecting to Gateway at {uri}...")
|
|
42
|
+
|
|
43
|
+
while True:
|
|
44
|
+
try:
|
|
45
|
+
async with websockets.connect(uri) as websocket:
|
|
46
|
+
logger.info("[AG2 Adapter] Connected to Gateway IPC.")
|
|
47
|
+
|
|
48
|
+
import uuid
|
|
49
|
+
import os
|
|
50
|
+
id_file = APP_DATA_DIR / ".agent_id"
|
|
51
|
+
if id_file.exists():
|
|
52
|
+
agent_id = id_file.read_text().strip()
|
|
53
|
+
else:
|
|
54
|
+
agent_id = str(uuid.uuid4())
|
|
55
|
+
id_file.write_text(agent_id)
|
|
56
|
+
|
|
57
|
+
# Register
|
|
58
|
+
await websocket.send(json.dumps({
|
|
59
|
+
"type": "REGISTER_ADAPTER",
|
|
60
|
+
"id": agent_id,
|
|
61
|
+
"name": "ag2",
|
|
62
|
+
"workspaceRoot": get_default_workspace()
|
|
63
|
+
}))
|
|
64
|
+
|
|
65
|
+
convo_id = os.environ.get("AGY_CONVERSATION_ID") or get_latest_conversation_id(APP_DATA_DIR)
|
|
66
|
+
bridge = AgentBridge(websocket, str(APP_DATA_DIR), convo_id)
|
|
67
|
+
bridge._watch_task = asyncio.create_task(bridge.watch_brain_dir())
|
|
68
|
+
bridge._poll_task = asyncio.create_task(bridge.poll_transcript())
|
|
69
|
+
|
|
70
|
+
try:
|
|
71
|
+
async for message in websocket:
|
|
72
|
+
try:
|
|
73
|
+
data = json.loads(message)
|
|
74
|
+
msg_type = data.get("type")
|
|
75
|
+
|
|
76
|
+
if msg_type == "SUBSCRIBE_CONVERSATION":
|
|
77
|
+
cid = data.get("conversationId")
|
|
78
|
+
if cid:
|
|
79
|
+
bridge.conversation_id = cid
|
|
80
|
+
if bridge.conversation_id:
|
|
81
|
+
await bridge.send_transcript(bridge.conversation_id)
|
|
82
|
+
await bridge.send_artifacts(bridge.conversation_id)
|
|
83
|
+
await bridge.send_workspace(get_default_workspace())
|
|
84
|
+
|
|
85
|
+
elif msg_type == "NEW_CONVERSATION":
|
|
86
|
+
# ag2 does not support new conversations, but we shouldn't throw an error
|
|
87
|
+
# because the Android app calls this automatically when switching agents.
|
|
88
|
+
# Instead, just send the current session state.
|
|
89
|
+
if bridge.conversation_id:
|
|
90
|
+
await bridge.send_transcript(bridge.conversation_id)
|
|
91
|
+
|
|
92
|
+
elif msg_type == "GET_HISTORY":
|
|
93
|
+
await bridge.send_history()
|
|
94
|
+
|
|
95
|
+
elif msg_type == "GET_ARTIFACTS":
|
|
96
|
+
cid = data.get("conversationId", bridge.conversation_id)
|
|
97
|
+
if cid:
|
|
98
|
+
await bridge.send_artifacts(cid)
|
|
99
|
+
|
|
100
|
+
elif msg_type == "READ_ARTIFACT":
|
|
101
|
+
path = data.get("path")
|
|
102
|
+
if path:
|
|
103
|
+
await bridge.read_artifact(path)
|
|
104
|
+
|
|
105
|
+
elif msg_type == "GET_FILES":
|
|
106
|
+
await bridge.send_workspace(get_default_workspace())
|
|
107
|
+
|
|
108
|
+
elif msg_type == "READ_FILE":
|
|
109
|
+
path = data.get("path")
|
|
110
|
+
if path:
|
|
111
|
+
await bridge.read_file(path)
|
|
112
|
+
|
|
113
|
+
elif msg_type == "SEND_MESSAGE":
|
|
114
|
+
logger.info(f"[AG2 Adapter] Received SEND_MESSAGE: {data.get('text', '')[:50]}...")
|
|
115
|
+
text = data.get("text", "")
|
|
116
|
+
images = data.get("images", [])
|
|
117
|
+
asyncio.create_task(bridge.process_message(text, images))
|
|
118
|
+
|
|
119
|
+
elif msg_type == "PING":
|
|
120
|
+
await websocket.send(json.dumps({"type": "PONG"}))
|
|
121
|
+
|
|
122
|
+
except json.JSONDecodeError:
|
|
123
|
+
pass
|
|
124
|
+
finally:
|
|
125
|
+
await bridge.cleanup()
|
|
126
|
+
|
|
127
|
+
except Exception as e:
|
|
128
|
+
logger.warning(f"[AG2 Adapter] Connection error: {e}. Reconnecting in 3s...")
|
|
129
|
+
await asyncio.sleep(3)
|
|
130
|
+
|
|
131
|
+
if __name__ == "__main__":
|
|
132
|
+
try:
|
|
133
|
+
asyncio.run(main())
|
|
134
|
+
except KeyboardInterrupt:
|
|
135
|
+
print("\nAdapter shut down gracefully.")
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "antimatter-ag2"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Antimatter AG2 Adapter — companion daemon for Google AntiGravity IDE"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { file = "LICENSE" }
|
|
11
|
+
authors = [
|
|
12
|
+
{ name = "Saif Mukhtar", email = "saifmukhtar@saifmukhtar.dev" }
|
|
13
|
+
]
|
|
14
|
+
requires-python = ">=3.11"
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 4 - Beta",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
"Topic :: Software Development",
|
|
19
|
+
"Topic :: Communications",
|
|
20
|
+
"Intended Audience :: Developers",
|
|
21
|
+
"Environment :: Console",
|
|
22
|
+
]
|
|
23
|
+
keywords = ["antigravity", "ai", "ide", "mobile", "bridge", "websocket", "android"]
|
|
24
|
+
dependencies = [
|
|
25
|
+
"websockets==15.0.1",
|
|
26
|
+
"google-antigravity==0.1.2",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[project.optional-dependencies]
|
|
30
|
+
test = [
|
|
31
|
+
"pytest>=7.0.0",
|
|
32
|
+
"pytest-asyncio>=0.23.0",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
[project.urls]
|
|
36
|
+
Homepage = "https://saifmukhtar.dev"
|
|
37
|
+
Repository = "https://github.com/saifmukhtar/antimatter"
|
|
38
|
+
Issues = "https://github.com/saifmukhtar/antimatter/issues"
|
|
39
|
+
Documentation = "https://saifmukhtar.dev/docs/antimatter"
|
|
40
|
+
Changelog = "https://github.com/saifmukhtar/antimatter/releases"
|
|
41
|
+
|
|
42
|
+
[project.scripts]
|
|
43
|
+
antimatter-ag2 = "antimatter_ag2.cli:main"
|
|
44
|
+
|
|
45
|
+
[tool.hatch.build.targets.wheel]
|
|
46
|
+
packages = ["antimatter_ag2"]
|
|
47
|
+
|
|
48
|
+
[tool.hatch.build.targets.sdist]
|
|
49
|
+
include = [
|
|
50
|
+
"antimatter_ag2",
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
[tool.pytest.ini_options]
|
|
54
|
+
asyncio_mode = "auto"
|
|
55
|
+
testpaths = ["tests"]
|