machinaos 0.0.21 → 0.0.23

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 (60) hide show
  1. package/README.md +32 -6
  2. package/bin/cli.js +0 -0
  3. package/client/dist/assets/index-5BWZnM6b.js +703 -0
  4. package/client/dist/index.html +1 -1
  5. package/client/package.json +1 -1
  6. package/client/src/Dashboard.tsx +12 -5
  7. package/client/src/ParameterPanel.tsx +6 -5
  8. package/client/src/components/AIAgentNode.tsx +35 -16
  9. package/client/src/components/CredentialsModal.tsx +450 -5
  10. package/client/src/components/TeamMonitorNode.tsx +269 -0
  11. package/client/src/components/parameterPanel/InputSection.tsx +25 -0
  12. package/client/src/contexts/WebSocketContext.tsx +38 -0
  13. package/client/src/hooks/useApiKeys.ts +44 -0
  14. package/client/src/nodeDefinitions/specializedAgentNodes.ts +59 -3
  15. package/client/src/nodeDefinitions/twitterNodes.ts +441 -0
  16. package/client/src/nodeDefinitions/utilityNodes.ts +45 -1
  17. package/client/src/nodeDefinitions.ts +7 -1
  18. package/client/src/services/executionService.ts +4 -1
  19. package/install.sh +63 -1
  20. package/package.json +5 -2
  21. package/scripts/build.js +0 -0
  22. package/scripts/clean.js +0 -0
  23. package/scripts/daemon.js +0 -0
  24. package/scripts/docker.js +0 -0
  25. package/scripts/install.js +0 -0
  26. package/scripts/postinstall.js +29 -0
  27. package/scripts/preinstall.js +67 -0
  28. package/scripts/serve-client.js +0 -0
  29. package/scripts/start.js +0 -0
  30. package/scripts/stop.js +0 -0
  31. package/scripts/sync-version.js +0 -0
  32. package/server/Dockerfile +10 -15
  33. package/server/constants.py +20 -0
  34. package/server/core/database.py +443 -3
  35. package/server/main.py +9 -1
  36. package/server/models/database.py +112 -2
  37. package/server/pyproject.toml +3 -0
  38. package/server/requirements.txt +3 -0
  39. package/server/routers/twitter.py +390 -0
  40. package/server/routers/websocket.py +320 -0
  41. package/server/services/agent_team.py +266 -0
  42. package/server/services/ai.py +43 -0
  43. package/server/services/compaction.py +39 -4
  44. package/server/services/event_waiter.py +41 -0
  45. package/server/services/handlers/__init__.py +13 -0
  46. package/server/services/handlers/ai.py +66 -2
  47. package/server/services/handlers/tools.py +84 -0
  48. package/server/services/handlers/twitter.py +297 -0
  49. package/server/services/handlers/utility.py +91 -0
  50. package/server/services/node_executor.py +15 -1
  51. package/server/services/pricing.py +270 -0
  52. package/server/services/status_broadcaster.py +79 -0
  53. package/server/services/twitter_oauth.py +410 -0
  54. package/server/skills/social_agent/twitter-search-skill/SKILL.md +146 -0
  55. package/server/skills/social_agent/twitter-send-skill/SKILL.md +142 -0
  56. package/server/skills/social_agent/twitter-user-skill/SKILL.md +165 -0
  57. package/workflows/Zeenie_full.json +459 -0
  58. package/workflows/Zeenie_small.json +459 -0
  59. package/client/dist/assets/index-YVvAiByx.js +0 -703
  60. package/server/requirements-docker.txt +0 -86
@@ -0,0 +1,390 @@
1
+ """
2
+ Twitter/X OAuth 2.0 callback and API routes.
3
+
4
+ OAuth flow:
5
+ 1. Frontend calls WebSocket 'twitter_oauth_login' handler
6
+ 2. Backend generates authorization URL, opens browser
7
+ 3. User authorizes on Twitter
8
+ 4. Twitter redirects to /api/twitter/callback with code
9
+ 5. Backend exchanges code for tokens, stores them via auth_service
10
+ 6. Frontend polls WebSocket 'twitter_oauth_status' for completion
11
+
12
+ Tokens stored as API keys with provider prefixes:
13
+ - twitter_access_token: OAuth access token (expires in 2 hours)
14
+ - twitter_refresh_token: OAuth refresh token (for token renewal)
15
+ """
16
+
17
+ from typing import Optional
18
+
19
+ from fastapi import APIRouter, Query
20
+ from fastapi.responses import HTMLResponse
21
+
22
+ from core.container import container
23
+ from core.logging import get_logger
24
+ from services.twitter_oauth import TwitterOAuth, get_pending_state
25
+
26
+ logger = get_logger(__name__)
27
+ router = APIRouter(prefix="/api/twitter", tags=["twitter"])
28
+
29
+
30
+ def get_auth_service():
31
+ """Get auth service for API key storage."""
32
+ return container.auth_service()
33
+
34
+
35
+ def get_twitter_oauth() -> TwitterOAuth:
36
+ """Create TwitterOAuth instance from stored credentials."""
37
+ auth_service = get_auth_service()
38
+ # Twitter client_id and client_secret are stored as API keys
39
+ # These are configured in the Credentials Modal
40
+ import asyncio
41
+ loop = asyncio.get_event_loop()
42
+
43
+ # Get stored Twitter app credentials (client_id, client_secret)
44
+ # These are stored via the Credentials Modal with provider='twitter'
45
+ client_id = loop.run_until_complete(auth_service.get_api_key("twitter_client_id")) or ""
46
+ client_secret = loop.run_until_complete(auth_service.get_api_key("twitter_client_secret"))
47
+
48
+ # Redirect URI is determined by the server host
49
+ # Default to localhost for development
50
+ redirect_uri = "http://localhost:3010/api/twitter/callback"
51
+
52
+ return TwitterOAuth(
53
+ client_id=client_id,
54
+ client_secret=client_secret,
55
+ redirect_uri=redirect_uri,
56
+ )
57
+
58
+
59
+ @router.get("/callback")
60
+ async def twitter_oauth_callback(
61
+ code: Optional[str] = Query(None),
62
+ state: Optional[str] = Query(None),
63
+ error: Optional[str] = Query(None),
64
+ error_description: Optional[str] = Query(None),
65
+ ):
66
+ """
67
+ Handle Twitter OAuth callback.
68
+
69
+ Twitter redirects here after user authorizes (or denies) the app.
70
+ We exchange the code for tokens and store them via auth_service.
71
+ """
72
+ # Handle authorization denied
73
+ if error:
74
+ logger.warning(f"Twitter OAuth denied: {error} - {error_description}")
75
+ return HTMLResponse(
76
+ content=_callback_html(
77
+ success=False,
78
+ error=error_description or error,
79
+ ),
80
+ status_code=200,
81
+ )
82
+
83
+ # Validate required parameters
84
+ if not code or not state:
85
+ logger.error("Twitter OAuth callback missing code or state")
86
+ return HTMLResponse(
87
+ content=_callback_html(
88
+ success=False,
89
+ error="Missing authorization code or state parameter",
90
+ ),
91
+ status_code=400,
92
+ )
93
+
94
+ # Verify state exists (CSRF protection)
95
+ pending_state = get_pending_state(state)
96
+ if not pending_state:
97
+ logger.error("Twitter OAuth callback with invalid/expired state")
98
+ return HTMLResponse(
99
+ content=_callback_html(
100
+ success=False,
101
+ error="Invalid or expired authorization state. Please try again.",
102
+ ),
103
+ status_code=400,
104
+ )
105
+
106
+ # Get stored client credentials to create OAuth instance
107
+ auth_service = get_auth_service()
108
+ client_id = await auth_service.get_api_key("twitter_client_id") or ""
109
+ client_secret = await auth_service.get_api_key("twitter_client_secret")
110
+
111
+ oauth = TwitterOAuth(
112
+ client_id=client_id,
113
+ client_secret=client_secret,
114
+ redirect_uri="http://localhost:3010/api/twitter/callback",
115
+ )
116
+
117
+ # Exchange code for tokens
118
+ result = await oauth.exchange_code(code=code, state=state)
119
+
120
+ if not result.get("success"):
121
+ logger.error(f"Twitter token exchange failed: {result.get('error')}")
122
+ return HTMLResponse(
123
+ content=_callback_html(
124
+ success=False,
125
+ error=result.get("error", "Token exchange failed"),
126
+ ),
127
+ status_code=400,
128
+ )
129
+
130
+ # Get user info to display and store
131
+ access_token = result.get("access_token")
132
+ refresh_token = result.get("refresh_token")
133
+ user_info = await oauth.get_user_info(access_token)
134
+
135
+ if not user_info.get("success"):
136
+ logger.warning(f"Failed to get Twitter user info: {user_info.get('error')}")
137
+ username = "Unknown"
138
+ else:
139
+ username = user_info.get("username", "Unknown")
140
+
141
+ # Store tokens in database via auth_service (same pattern as other API keys)
142
+ # Access token stored with provider='twitter_access_token'
143
+ await auth_service.store_api_key(
144
+ provider="twitter_access_token",
145
+ api_key=access_token,
146
+ models=[], # No models for OAuth tokens
147
+ session_id="default"
148
+ )
149
+
150
+ # Refresh token stored with provider='twitter_refresh_token'
151
+ if refresh_token:
152
+ await auth_service.store_api_key(
153
+ provider="twitter_refresh_token",
154
+ api_key=refresh_token,
155
+ models=[],
156
+ session_id="default"
157
+ )
158
+
159
+ # Store user info for display purposes
160
+ if user_info.get("success"):
161
+ await auth_service.store_api_key(
162
+ provider="twitter_user_info",
163
+ api_key=f"{user_info.get('id')}:{username}:{user_info.get('name', '')}",
164
+ models=[],
165
+ session_id="default"
166
+ )
167
+
168
+ # Broadcast completion event to frontend
169
+ from services.status_broadcaster import get_status_broadcaster
170
+ broadcaster = get_status_broadcaster()
171
+
172
+ await broadcaster.broadcast({
173
+ "type": "twitter_oauth_complete",
174
+ "data": {
175
+ "success": True,
176
+ "username": username,
177
+ "user_id": user_info.get("id"),
178
+ "name": user_info.get("name"),
179
+ "profile_image_url": user_info.get("profile_image_url"),
180
+ }
181
+ })
182
+
183
+ logger.info(f"Twitter OAuth successful for @{username}")
184
+
185
+ return HTMLResponse(
186
+ content=_callback_html(
187
+ success=True,
188
+ username=username,
189
+ ),
190
+ status_code=200,
191
+ )
192
+
193
+
194
+ @router.get("/status")
195
+ async def get_twitter_status():
196
+ """
197
+ Get Twitter connection status.
198
+
199
+ Returns whether the user is authenticated with Twitter.
200
+ """
201
+ auth_service = get_auth_service()
202
+
203
+ # Try to get stored access token
204
+ access_token = await auth_service.get_api_key("twitter_access_token")
205
+
206
+ if not access_token:
207
+ return {
208
+ "connected": False,
209
+ "username": None,
210
+ "user_id": None,
211
+ }
212
+
213
+ # Get stored client credentials
214
+ client_id = await auth_service.get_api_key("twitter_client_id") or ""
215
+ client_secret = await auth_service.get_api_key("twitter_client_secret")
216
+
217
+ oauth = TwitterOAuth(
218
+ client_id=client_id,
219
+ client_secret=client_secret,
220
+ redirect_uri="http://localhost:3010/api/twitter/callback",
221
+ )
222
+
223
+ # Verify token is still valid by getting user info
224
+ user_info = await oauth.get_user_info(access_token)
225
+
226
+ if not user_info.get("success"):
227
+ # Token may be expired, try to refresh
228
+ refresh_token = await auth_service.get_api_key("twitter_refresh_token")
229
+ if refresh_token:
230
+ refresh_result = await oauth.refresh_access_token(refresh_token)
231
+ if refresh_result.get("success"):
232
+ # Store new tokens via auth_service
233
+ await auth_service.store_api_key(
234
+ provider="twitter_access_token",
235
+ api_key=refresh_result["access_token"],
236
+ models=[],
237
+ session_id="default"
238
+ )
239
+ if refresh_result.get("refresh_token"):
240
+ await auth_service.store_api_key(
241
+ provider="twitter_refresh_token",
242
+ api_key=refresh_result["refresh_token"],
243
+ models=[],
244
+ session_id="default"
245
+ )
246
+
247
+ # Retry user info
248
+ user_info = await oauth.get_user_info(refresh_result["access_token"])
249
+
250
+ if not user_info.get("success"):
251
+ return {
252
+ "connected": False,
253
+ "username": None,
254
+ "user_id": None,
255
+ "error": user_info.get("error"),
256
+ }
257
+
258
+ return {
259
+ "connected": True,
260
+ "username": user_info.get("username"),
261
+ "user_id": user_info.get("id"),
262
+ "name": user_info.get("name"),
263
+ "profile_image_url": user_info.get("profile_image_url"),
264
+ "verified": user_info.get("verified"),
265
+ }
266
+
267
+
268
+ @router.post("/logout")
269
+ async def twitter_logout():
270
+ """
271
+ Disconnect Twitter by revoking tokens and clearing stored credentials.
272
+ """
273
+ auth_service = get_auth_service()
274
+
275
+ # Get stored tokens
276
+ access_token = await auth_service.get_api_key("twitter_access_token")
277
+ refresh_token = await auth_service.get_api_key("twitter_refresh_token")
278
+
279
+ # Get client credentials for revocation
280
+ client_id = await auth_service.get_api_key("twitter_client_id") or ""
281
+ client_secret = await auth_service.get_api_key("twitter_client_secret")
282
+
283
+ # Revoke tokens if we have them
284
+ if access_token or refresh_token:
285
+ oauth = TwitterOAuth(
286
+ client_id=client_id,
287
+ client_secret=client_secret,
288
+ redirect_uri="http://localhost:3010/api/twitter/callback",
289
+ )
290
+
291
+ if access_token:
292
+ await oauth.revoke_token(access_token, "access_token")
293
+
294
+ if refresh_token:
295
+ await oauth.revoke_token(refresh_token, "refresh_token")
296
+
297
+ # Clear stored credentials via auth_service
298
+ await auth_service.remove_api_key("twitter_access_token")
299
+ await auth_service.remove_api_key("twitter_refresh_token")
300
+ await auth_service.remove_api_key("twitter_user_info")
301
+
302
+ logger.info("Twitter disconnected and tokens revoked")
303
+
304
+ return {"success": True, "message": "Twitter disconnected"}
305
+
306
+
307
+ def _callback_html(success: bool, username: str = None, error: str = None) -> str:
308
+ """Generate callback HTML page that closes itself and notifies parent."""
309
+ if success:
310
+ title = "Twitter Connected"
311
+ message = f"Successfully connected as @{username}!"
312
+ color = "#00ba7c" # Green
313
+ icon = "check-circle"
314
+ else:
315
+ title = "Connection Failed"
316
+ message = error or "Failed to connect to Twitter"
317
+ color = "#f4212e" # Red
318
+ icon = "x-circle"
319
+
320
+ return f"""
321
+ <!DOCTYPE html>
322
+ <html>
323
+ <head>
324
+ <title>{title}</title>
325
+ <style>
326
+ * {{ margin: 0; padding: 0; box-sizing: border-box; }}
327
+ body {{
328
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
329
+ background: linear-gradient(135deg, #15202b 0%, #1a1a2e 100%);
330
+ min-height: 100vh;
331
+ display: flex;
332
+ align-items: center;
333
+ justify-content: center;
334
+ color: #fff;
335
+ }}
336
+ .container {{
337
+ text-align: center;
338
+ padding: 40px;
339
+ background: rgba(255, 255, 255, 0.05);
340
+ border-radius: 16px;
341
+ border: 1px solid rgba(255, 255, 255, 0.1);
342
+ max-width: 400px;
343
+ }}
344
+ .icon {{
345
+ width: 64px;
346
+ height: 64px;
347
+ margin-bottom: 20px;
348
+ color: {color};
349
+ }}
350
+ h1 {{
351
+ font-size: 24px;
352
+ margin-bottom: 12px;
353
+ color: {color};
354
+ }}
355
+ p {{
356
+ font-size: 16px;
357
+ color: rgba(255, 255, 255, 0.8);
358
+ margin-bottom: 20px;
359
+ }}
360
+ .close-text {{
361
+ font-size: 14px;
362
+ color: rgba(255, 255, 255, 0.5);
363
+ }}
364
+ </style>
365
+ </head>
366
+ <body>
367
+ <div class="container">
368
+ <svg class="icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
369
+ {"<path stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z'/>" if success else "<path stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z'/>"}
370
+ </svg>
371
+ <h1>{title}</h1>
372
+ <p>{message}</p>
373
+ <p class="close-text">This window will close automatically...</p>
374
+ </div>
375
+ <script>
376
+ // Notify parent window and close
377
+ if (window.opener) {{
378
+ window.opener.postMessage({{
379
+ type: 'twitter_oauth_callback',
380
+ success: {str(success).lower()},
381
+ {"username: '" + username + "'," if username else ""}
382
+ {"error: '" + error.replace("'", "\\'") + "'," if error else ""}
383
+ }}, '*');
384
+ }}
385
+ // Close after 2 seconds
386
+ setTimeout(function() {{ window.close(); }}, 2000);
387
+ </script>
388
+ </body>
389
+ </html>
390
+ """