cade-cli 0.3.3__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 (44) hide show
  1. cade_cli-0.3.3.dist-info/METADATA +151 -0
  2. cade_cli-0.3.3.dist-info/RECORD +44 -0
  3. cade_cli-0.3.3.dist-info/WHEEL +4 -0
  4. cade_cli-0.3.3.dist-info/entry_points.txt +2 -0
  5. cadecoder/__init__.py +1 -0
  6. cadecoder/ai/__init__.py +6 -0
  7. cadecoder/ai/prompts.py +572 -0
  8. cadecoder/cli/__init__.py +0 -0
  9. cadecoder/cli/app.py +147 -0
  10. cadecoder/cli/auth.py +483 -0
  11. cadecoder/cli/commands/__init__.py +5 -0
  12. cadecoder/cli/commands/auth.py +143 -0
  13. cadecoder/cli/commands/chat.py +264 -0
  14. cadecoder/cli/commands/mcp.py +477 -0
  15. cadecoder/cli/commands/tools.py +226 -0
  16. cadecoder/core/__init__.py +12 -0
  17. cadecoder/core/config.py +380 -0
  18. cadecoder/core/constants.py +281 -0
  19. cadecoder/core/errors.py +145 -0
  20. cadecoder/core/logging.py +148 -0
  21. cadecoder/core/types.py +235 -0
  22. cadecoder/core/utils.py +279 -0
  23. cadecoder/execution/__init__.py +46 -0
  24. cadecoder/execution/context_window.py +521 -0
  25. cadecoder/execution/orchestrator.py +562 -0
  26. cadecoder/execution/parallel.py +287 -0
  27. cadecoder/providers/__init__.py +60 -0
  28. cadecoder/providers/base.py +294 -0
  29. cadecoder/providers/openai.py +251 -0
  30. cadecoder/storage/__init__.py +0 -0
  31. cadecoder/storage/threads.py +489 -0
  32. cadecoder/templates/login_failed.html +21 -0
  33. cadecoder/templates/login_success.html +21 -0
  34. cadecoder/templates/styles.css +87 -0
  35. cadecoder/tools/__init__.py +19 -0
  36. cadecoder/tools/builtin.py +644 -0
  37. cadecoder/tools/filesystem.py +315 -0
  38. cadecoder/tools/git.py +221 -0
  39. cadecoder/tools/manager.py +1635 -0
  40. cadecoder/ui/__init__.py +7 -0
  41. cadecoder/ui/display.py +338 -0
  42. cadecoder/ui/input.py +145 -0
  43. cadecoder/ui/session.py +455 -0
  44. cadecoder/ui/state.py +20 -0
@@ -0,0 +1,489 @@
1
+ """Thread storage system for CadeCoder.
2
+
3
+ This module provides persistent storage for chat threads and messages.
4
+ """
5
+
6
+ import json
7
+ import sqlite3
8
+ from abc import ABC, abstractmethod
9
+ from datetime import UTC, datetime
10
+ from functools import lru_cache
11
+ from pathlib import Path
12
+ from typing import Any, TypeVar
13
+
14
+ from pydantic import BaseModel, ConfigDict, Field
15
+ from ulid import ulid
16
+
17
+ from cadecoder.core.config import get_config
18
+ from cadecoder.core.errors import StorageError
19
+ from cadecoder.core.logging import log
20
+
21
+ # --- Message Models ---
22
+
23
+
24
+ class ToolCallInfo(BaseModel):
25
+ """Tool call information."""
26
+
27
+ call_id: str
28
+ tool_name: str
29
+ tool_type: str = "function"
30
+ parameters: dict[str, Any] = Field(default_factory=dict)
31
+ timestamp: datetime = Field(default_factory=lambda: datetime.now(UTC))
32
+ duration_ms: int | None = None
33
+ status: str | None = None
34
+ error_message: str | None = None
35
+
36
+ def model_dump(self, **kwargs) -> dict[str, Any]:
37
+ """Custom dump that converts datetime to ISO format."""
38
+ data = super().model_dump(**kwargs)
39
+ if isinstance(data.get("timestamp"), datetime):
40
+ data["timestamp"] = data["timestamp"].isoformat()
41
+ return data
42
+
43
+
44
+ class ModelInfo(BaseModel):
45
+ """Model information for a message."""
46
+
47
+ provider: str
48
+ model_name: str
49
+ model_version: str | None = None
50
+ temperature: float | None = None
51
+ max_tokens: int | None = None
52
+ prompt_tokens: int | None = None
53
+ completion_tokens: int | None = None
54
+ total_tokens: int | None = None
55
+
56
+
57
+ class Message(BaseModel):
58
+ """Message model."""
59
+
60
+ model_config = ConfigDict(populate_by_name=True)
61
+
62
+ message_id: str = Field(alias="id")
63
+ thread_id: str
64
+ role: str
65
+ content: str | None = None
66
+ timestamp: datetime = Field(default_factory=lambda: datetime.now(UTC))
67
+ tool_calls: list[ToolCallInfo] = Field(default_factory=list)
68
+ responding_tool_call_id: str | None = None
69
+ model_info: ModelInfo | None = None
70
+ parent_message_id: str | None = None
71
+ conversation_turn: int | None = None
72
+
73
+ def model_dump_db(self) -> dict[str, Any]:
74
+ """Prepare model for DB storage."""
75
+ dump = self.model_dump(by_alias=True)
76
+ dump["timestamp"] = dump["timestamp"].isoformat()
77
+ dump["tool_calls_json"] = json.dumps([tc.model_dump() for tc in self.tool_calls])
78
+ dump.pop("tool_calls", None)
79
+
80
+ if self.model_info:
81
+ dump["model_info_json"] = json.dumps(self.model_info.model_dump())
82
+ else:
83
+ dump["model_info_json"] = None
84
+ dump.pop("model_info", None)
85
+
86
+ dump["conversation_id"] = dump.pop("thread_id")
87
+ dump["tool_call_id"] = self.responding_tool_call_id
88
+ dump.pop("responding_tool_call_id", None)
89
+
90
+ return dump
91
+
92
+ @classmethod
93
+ def model_validate_db(cls, data: dict[str, Any]) -> "Message":
94
+ """Validate data coming from DB."""
95
+ db_data = data.copy()
96
+ db_data["thread_id"] = db_data.pop("conversation_id")
97
+ db_data["responding_tool_call_id"] = db_data.pop("tool_call_id", None)
98
+
99
+ tool_calls_json = db_data.pop("tool_calls_json", None)
100
+ if tool_calls_json:
101
+ tool_calls_data = json.loads(tool_calls_json)
102
+ db_data["tool_calls"] = [ToolCallInfo.model_validate(tc) for tc in tool_calls_data]
103
+ else:
104
+ db_data["tool_calls"] = []
105
+
106
+ model_info_json = db_data.pop("model_info_json", None)
107
+ if model_info_json:
108
+ db_data["model_info"] = ModelInfo.model_validate(json.loads(model_info_json))
109
+
110
+ db_data["timestamp"] = datetime.fromisoformat(db_data["timestamp"])
111
+
112
+ return cls.model_validate(db_data)
113
+
114
+
115
+ class Thread(BaseModel):
116
+ """Thread model."""
117
+
118
+ model_config = ConfigDict(populate_by_name=True)
119
+
120
+ thread_id: str = Field(alias="id")
121
+ name: str | None = None
122
+ git_branch: str | None = None
123
+ model: str = Field(default="unknown")
124
+ user_id: str | None = None
125
+ created_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
126
+ last_modified_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
127
+ tags: list[str] = Field(default_factory=list)
128
+ metadata: dict[str, Any] = Field(default_factory=dict)
129
+
130
+ def model_dump_db(self) -> dict[str, Any]:
131
+ dump = self.model_dump(by_alias=True)
132
+ dump["created_at"] = dump["created_at"].isoformat()
133
+ dump["last_modified_at"] = dump["last_modified_at"].isoformat()
134
+ dump["tags_json"] = json.dumps(dump.pop("tags", []))
135
+ dump["metadata_json"] = json.dumps(dump.pop("metadata", {}))
136
+ return dump
137
+
138
+ @classmethod
139
+ def model_validate_db(cls, data: dict[str, Any]) -> "Thread":
140
+ db_data = data.copy()
141
+ db_data["created_at"] = datetime.fromisoformat(db_data["created_at"])
142
+ db_data["last_modified_at"] = datetime.fromisoformat(db_data["last_modified_at"])
143
+
144
+ tags_json = db_data.pop("tags_json", None)
145
+ if tags_json:
146
+ db_data["tags"] = json.loads(tags_json)
147
+
148
+ metadata_json = db_data.pop("metadata_json", None)
149
+ if metadata_json:
150
+ db_data["metadata"] = json.loads(metadata_json)
151
+
152
+ if "model" not in db_data:
153
+ db_data["model"] = "unknown"
154
+ if "user_id" not in db_data:
155
+ db_data["user_id"] = None
156
+
157
+ return cls.model_validate(db_data)
158
+
159
+
160
+ # --- Storage Interfaces ---
161
+
162
+ T = TypeVar("T")
163
+
164
+
165
+ class BaseThreadHistory(ABC):
166
+ """Abstract base class for chat thread history storage."""
167
+
168
+ @abstractmethod
169
+ def add_message(self, message: Message) -> None:
170
+ """Adds a message to the history."""
171
+ raise NotImplementedError
172
+
173
+ @abstractmethod
174
+ def get_messages(self, thread_id: str) -> list[Message]:
175
+ """Retrieves all messages for a given thread."""
176
+ raise NotImplementedError
177
+
178
+ @abstractmethod
179
+ def get_thread(self, thread_id: str) -> Thread | None:
180
+ """Retrieves thread metadata."""
181
+ raise NotImplementedError
182
+
183
+ @abstractmethod
184
+ def list_threads(self) -> list[Thread]:
185
+ """Lists all stored threads."""
186
+ raise NotImplementedError
187
+
188
+ @abstractmethod
189
+ def create_thread(
190
+ self,
191
+ name: str | None = None,
192
+ git_branch: str | None = None,
193
+ model: str = "unknown",
194
+ user_id: str | None = None,
195
+ tags: list[str] | None = None,
196
+ metadata: dict[str, Any] | None = None,
197
+ ) -> Thread:
198
+ """Creates a new thread."""
199
+ raise NotImplementedError
200
+
201
+ @abstractmethod
202
+ def delete_thread(self, thread_id: str) -> None:
203
+ """Deletes a thread and its messages."""
204
+ raise NotImplementedError
205
+
206
+ @abstractmethod
207
+ def update_thread_timestamp(self, thread_id: str) -> None:
208
+ """Updates the last modified timestamp of a thread."""
209
+ raise NotImplementedError
210
+
211
+
212
+ # --- SQLite Implementation ---
213
+
214
+ DEFAULT_DB_NAME = "cadecoder_history.db"
215
+
216
+ SCHEMA = """
217
+ CREATE TABLE IF NOT EXISTS conversations (
218
+ id TEXT PRIMARY KEY,
219
+ name TEXT,
220
+ git_branch TEXT,
221
+ model TEXT DEFAULT 'unknown',
222
+ user_id TEXT,
223
+ created_at TEXT NOT NULL,
224
+ last_modified_at TEXT NOT NULL,
225
+ tags_json TEXT DEFAULT '[]',
226
+ metadata_json TEXT DEFAULT '{}'
227
+ );
228
+
229
+ CREATE TABLE IF NOT EXISTS messages (
230
+ id TEXT PRIMARY KEY,
231
+ conversation_id TEXT NOT NULL,
232
+ role TEXT NOT NULL,
233
+ content TEXT,
234
+ timestamp TEXT NOT NULL,
235
+ tool_calls_json TEXT DEFAULT '[]',
236
+ tool_call_id TEXT,
237
+ model_info_json TEXT,
238
+ parent_message_id TEXT,
239
+ conversation_turn INTEGER,
240
+ FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE CASCADE
241
+ );
242
+
243
+ CREATE INDEX IF NOT EXISTS idx_messages_conversation ON messages(conversation_id);
244
+ CREATE INDEX IF NOT EXISTS idx_messages_timestamp ON messages(timestamp);
245
+ CREATE INDEX IF NOT EXISTS idx_conversations_last_modified ON conversations(last_modified_at);
246
+ """
247
+
248
+
249
+ def get_db_path() -> Path:
250
+ """Determines the path for the SQLite database using config."""
251
+ try:
252
+ app_directory = get_config().ensure_app_dir()
253
+ base_path = Path(app_directory)
254
+ except Exception as e:
255
+ log.error(f"Failed to get or ensure app directory from config: {e}")
256
+ raise StorageError(f"Could not determine storage directory: {e}") from e
257
+
258
+ return base_path / DEFAULT_DB_NAME
259
+
260
+
261
+ class SqliteThreadHistory(BaseThreadHistory):
262
+ """SQLite-based chat thread history implementation."""
263
+
264
+ def __init__(self, db_path: str | Path | None = None):
265
+ self.db_path = Path(db_path) if db_path else get_db_path()
266
+ self._conn: sqlite3.Connection | None = None
267
+ self._connect()
268
+ self._initialize_db()
269
+ log.info(f"Initialized SQLite thread history at {self.db_path}")
270
+
271
+ def _connect(self):
272
+ """Establish SQLite connection."""
273
+ if self._conn is not None:
274
+ return
275
+ try:
276
+ self._conn = sqlite3.connect(
277
+ self.db_path, isolation_level=None, check_same_thread=False
278
+ )
279
+ self._conn.row_factory = sqlite3.Row
280
+ log.debug(f"Connected to SQLite database: {self.db_path}")
281
+ except sqlite3.Error as e:
282
+ log.error(f"Error connecting to SQLite database: {e}")
283
+ raise StorageError(f"Failed to connect to database: {e}") from e
284
+
285
+ def _initialize_db(self):
286
+ """Create database schema."""
287
+ if not self._conn:
288
+ self._connect()
289
+
290
+ try:
291
+ self._conn.executescript(SCHEMA)
292
+ log.debug("Database schema initialized.")
293
+ except Exception as e:
294
+ log.error(f"Error initializing database schema: {e}")
295
+ raise StorageError(f"Failed to initialize database schema: {e}") from e
296
+
297
+ def _execute(self, query: str, params: tuple | dict = ()) -> sqlite3.Cursor:
298
+ """Executes a SQL query with error handling."""
299
+ if not self._conn:
300
+ raise StorageError("Database connection is not available.")
301
+ try:
302
+ cursor = self._conn.cursor()
303
+ cursor.execute(query, params)
304
+ return cursor
305
+ except sqlite3.Error as e:
306
+ log.error(f"SQLite error: {e}")
307
+ raise StorageError(f"Database error: {e}") from e
308
+
309
+ def close(self):
310
+ """Closes the database connection."""
311
+ if self._conn:
312
+ self._conn.close()
313
+ self._conn = None
314
+ log.debug("Closed SQLite database connection.")
315
+
316
+ def add_message(self, message: Message) -> None:
317
+ """Adds a message to the database."""
318
+ if not self.get_thread(message.thread_id):
319
+ raise StorageError(f"Cannot add message to non-existent thread: {message.thread_id}")
320
+
321
+ if not message.message_id:
322
+ message.message_id = str(ulid()).lower()
323
+
324
+ msg_data = message.model_dump_db()
325
+ query = """
326
+ INSERT INTO messages (
327
+ id, conversation_id, role, content, timestamp,
328
+ tool_calls_json, tool_call_id, model_info_json,
329
+ parent_message_id, conversation_turn
330
+ )
331
+ VALUES (
332
+ :id, :conversation_id, :role, :content, :timestamp,
333
+ :tool_calls_json, :tool_call_id, :model_info_json,
334
+ :parent_message_id, :conversation_turn
335
+ )
336
+ """
337
+ try:
338
+ self._execute(query, msg_data)
339
+ self.update_thread_timestamp(message.thread_id)
340
+ log.debug(f"Added message {message.message_id} to thread {message.thread_id}")
341
+ except Exception as e:
342
+ log.error(f"Failed to add message: {e}")
343
+ raise
344
+
345
+ def get_messages(self, thread_id: str) -> list[Message]:
346
+ """Retrieves all messages for a thread, ordered by timestamp."""
347
+ query = "SELECT * FROM messages WHERE conversation_id = ? ORDER BY timestamp ASC"
348
+ cursor = self._execute(query, (thread_id,))
349
+ rows = cursor.fetchall()
350
+ cursor.close()
351
+ return [Message.model_validate_db(dict(row)) for row in rows]
352
+
353
+ def get_thread(self, thread_id: str) -> Thread | None:
354
+ """Retrieves thread metadata."""
355
+ query = "SELECT * FROM conversations WHERE id = ?"
356
+ cursor = self._execute(query, (thread_id,))
357
+ row = cursor.fetchone()
358
+ cursor.close()
359
+ if row:
360
+ return Thread.model_validate_db(dict(row))
361
+ return None
362
+
363
+ def list_threads(self) -> list[Thread]:
364
+ """Lists all threads, ordered by last modified."""
365
+ query = "SELECT * FROM conversations ORDER BY last_modified_at DESC"
366
+ cursor = self._execute(query)
367
+ rows = cursor.fetchall()
368
+ cursor.close()
369
+ return [Thread.model_validate_db(dict(row)) for row in rows]
370
+
371
+ def create_thread(
372
+ self,
373
+ name: str | None = None,
374
+ git_branch: str | None = None,
375
+ model: str = "unknown",
376
+ user_id: str | None = None,
377
+ tags: list[str] | None = None,
378
+ metadata: dict[str, Any] | None = None,
379
+ ) -> Thread:
380
+ """Creates a new thread record."""
381
+ thread = Thread(
382
+ id=str(ulid()).lower(),
383
+ name=name,
384
+ git_branch=git_branch,
385
+ model=model,
386
+ user_id=user_id,
387
+ tags=tags or [],
388
+ metadata=metadata or {},
389
+ )
390
+
391
+ thread_data = thread.model_dump_db()
392
+ query = """
393
+ INSERT INTO conversations (
394
+ id, name, git_branch, model, user_id,
395
+ created_at, last_modified_at, tags_json, metadata_json
396
+ )
397
+ VALUES (
398
+ :id, :name, :git_branch, :model, :user_id,
399
+ :created_at, :last_modified_at, :tags_json, :metadata_json
400
+ )
401
+ """
402
+ try:
403
+ self._execute(query, thread_data)
404
+ log.info(f"Created thread: {thread.thread_id}")
405
+ return thread
406
+ except sqlite3.IntegrityError:
407
+ log.error(f"Failed to create thread: {thread.thread_id}")
408
+ raise StorageError(f"Thread ID collision for {thread.thread_id}")
409
+
410
+ def delete_thread(self, thread_id: str) -> None:
411
+ """Deletes a thread and its associated messages."""
412
+ if not self.get_thread(thread_id):
413
+ log.warning(f"Attempted to delete non-existent thread: {thread_id}")
414
+ return
415
+
416
+ # Delete messages first
417
+ self._execute("DELETE FROM messages WHERE conversation_id = ?", (thread_id,))
418
+ # Delete thread
419
+ cursor = self._execute("DELETE FROM conversations WHERE id = ?", (thread_id,))
420
+ if cursor.rowcount > 0:
421
+ log.info(f"Deleted thread {thread_id} and its messages.")
422
+ cursor.close()
423
+
424
+ def update_thread_timestamp(self, thread_id: str) -> None:
425
+ """Updates the last modified timestamp."""
426
+ now_iso = datetime.now(UTC).isoformat()
427
+ query = "UPDATE conversations SET last_modified_at = ? WHERE id = ?"
428
+ cursor = self._execute(query, (now_iso, thread_id))
429
+ cursor.close()
430
+
431
+ def find_thread_by_name_and_branch(self, name: str, git_branch: str) -> Thread | None:
432
+ """Find a thread by name and git branch."""
433
+ query = "SELECT * FROM conversations WHERE name = ? AND git_branch = ?"
434
+ cursor = self._execute(query, (name, git_branch))
435
+ row = cursor.fetchone()
436
+ cursor.close()
437
+ if row:
438
+ return Thread.model_validate_db(dict(row))
439
+ return None
440
+
441
+ def update_thread_git_branch(self, thread_id: str, git_branch: str) -> None:
442
+ """Updates the git branch of a thread."""
443
+ query = "UPDATE conversations SET git_branch = ? WHERE id = ?"
444
+ cursor = self._execute(query, (git_branch, thread_id))
445
+ if cursor.rowcount == 0:
446
+ raise StorageError(f"Thread {thread_id} not found.")
447
+ cursor.close()
448
+ self.update_thread_timestamp(thread_id)
449
+
450
+ def update_thread_model(self, thread_id: str, model: str) -> None:
451
+ """Updates the AI model of a thread."""
452
+ query = "UPDATE conversations SET model = ? WHERE id = ?"
453
+ cursor = self._execute(query, (model, thread_id))
454
+ if cursor.rowcount == 0:
455
+ raise StorageError(f"Thread {thread_id} not found.")
456
+ cursor.close()
457
+ self.update_thread_timestamp(thread_id)
458
+
459
+ def update_thread_user_id(self, thread_id: str, user_id: str) -> None:
460
+ """Updates the user ID of a thread."""
461
+ query = "UPDATE conversations SET user_id = ? WHERE id = ?"
462
+ cursor = self._execute(query, (user_id, thread_id))
463
+ if cursor.rowcount == 0:
464
+ raise StorageError(f"Thread {thread_id} not found.")
465
+ cursor.close()
466
+ self.update_thread_timestamp(thread_id)
467
+
468
+
469
+ # --- Helper / Factory ---
470
+
471
+ _thread_history_instance: BaseThreadHistory | None = None
472
+
473
+
474
+ @lru_cache(maxsize=1)
475
+ def get_thread_history() -> BaseThreadHistory:
476
+ """Gets the chat thread history instance (singleton pattern)."""
477
+ log.debug("Initializing thread history instance...")
478
+
479
+ try:
480
+ get_config().ensure_app_dir()
481
+ instance = SqliteThreadHistory()
482
+ except StorageError as e:
483
+ log.error(f"Failed to initialize SQLite history: {e}")
484
+ raise
485
+ except Exception as e:
486
+ log.exception("Unexpected error initializing thread history")
487
+ raise StorageError(f"Failed to initialize thread history: {e}") from e
488
+
489
+ return instance
@@ -0,0 +1,21 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Login failed</title>
7
+ <link rel="icon" href="https://cdn.arcade.dev/favicons/favicon.ico" sizes="any">
8
+ <link rel="apple-touch-icon" href="https://cdn.arcade.dev/favicons/apple-touch-icon.png">
9
+ <link rel="icon" type="image/png" sizes="32x32" href="https://cdn.arcade.dev/favicons/favicon-32x32.png">
10
+ <link rel="icon" type="image/png" sizes="16x16" href="https://cdn.arcade.dev/favicons/favicon-16x16.png">
11
+ <link rel="apple-touch-icon" sizes="180x180" href="https://cdn.arcade.dev/favicons/apple-touch-icon.png">
12
+ <link rel="stylesheet" href="styles.css">
13
+ </head>
14
+ <body>
15
+ <div class="container">
16
+ <img src="https://cdn.arcade.dev/logos/a-icon.png" alt="Arcade logo" class="logo">
17
+ <h2>Log in to Arcade CLI</h2>
18
+ <p class="message error">Something went wrong. Please close this window and try again.</p>
19
+ </div>
20
+ </body>
21
+ </html>
@@ -0,0 +1,21 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Success!</title>
7
+ <link rel="icon" href="https://cdn.arcade.dev/favicons/favicon.ico" sizes="any">
8
+ <link rel="apple-touch-icon" href="https://cdn.arcade.dev/favicons/apple-touch-icon.png">
9
+ <link rel="icon" type="image/png" sizes="32x32" href="https://cdn.arcade.dev/favicons/favicon-32x32.png">
10
+ <link rel="icon" type="image/png" sizes="16x16" href="https://cdn.arcade.dev/favicons/favicon-16x16.png">
11
+ <link rel="apple-touch-icon" sizes="180x180" href="https://cdn.arcade.dev/favicons/apple-touch-icon.png">
12
+ <link rel="stylesheet" href="styles.css">
13
+ </head>
14
+ <body>
15
+ <div class="container">
16
+ <img src="https://cdn.arcade.dev/logos/a-icon.png" alt="Arcade logo" class="logo">
17
+ <h2>Log in to Arcade CLI</h2>
18
+ <p class="message info">Success! You can close this window.</p>
19
+ </div>
20
+ </body>
21
+ </html>
@@ -0,0 +1,87 @@
1
+ body {
2
+ display: flex;
3
+ justify-content: center;
4
+ align-items: center;
5
+ height: 100vh;
6
+ margin: 0;
7
+ background: linear-gradient(135deg, #1a1a1a, #0f0f0f);
8
+ font-family: Arial, sans-serif;
9
+ }
10
+
11
+ .container {
12
+ background-color: #333;
13
+ padding: 40px;
14
+ border-radius: 8px;
15
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
16
+ width: 300px;
17
+ }
18
+
19
+ .container h2 {
20
+ color: #fff;
21
+ margin-bottom: 20px;
22
+ text-align: center;
23
+ }
24
+
25
+ .container label {
26
+ display: block;
27
+ color: #bbb;
28
+ margin-bottom: 5px;
29
+ font-size: 14px;
30
+ }
31
+
32
+ .container input[type="text"],
33
+ .container input[type="password"] {
34
+ width: 100%;
35
+ padding: 10px;
36
+ margin-bottom: 15px;
37
+ border: none;
38
+ border-radius: 4px;
39
+ background-color: #444;
40
+ color: #ddd;
41
+ font-size: 16px;
42
+ box-sizing: border-box;
43
+ }
44
+
45
+ .container input[type="text"]::placeholder,
46
+ .container input[type="password"]::placeholder {
47
+ color: #aaa;
48
+ }
49
+
50
+ .container input[type="submit"] {
51
+ width: 100%;
52
+ padding: 10px;
53
+ border: none;
54
+ border-radius: 4px;
55
+ background-color: #ED155D;
56
+ color: #fff;
57
+ font-size: 16px;
58
+ cursor: pointer;
59
+ transition: background-color 0.3s ease;
60
+ }
61
+
62
+ .container input[type="submit"]:hover {
63
+ background-color: #C0104A;
64
+ }
65
+
66
+ .message {
67
+ background-color: #1e1e1e;
68
+ padding: 10px;
69
+ border-radius: 4px;
70
+ margin-bottom: 15px;
71
+ font-size: 14px;
72
+ text-align: center;
73
+ }
74
+ .info {
75
+ color: #fff;
76
+ }
77
+
78
+ .error {
79
+ color: #ff4d4d;
80
+ }
81
+
82
+ .logo {
83
+ display: block;
84
+ max-width: 100%;
85
+ max-height: 90px;
86
+ margin: 0 auto 20px;
87
+ }
@@ -0,0 +1,19 @@
1
+ """Tools for AI agents."""
2
+
3
+ from cadecoder.tools.manager import (
4
+ CacheEntry,
5
+ CompositeToolManager,
6
+ LocalToolManager,
7
+ RemoteToolManager,
8
+ ToolCache,
9
+ ToolManager,
10
+ )
11
+
12
+ __all__ = [
13
+ "ToolManager",
14
+ "LocalToolManager",
15
+ "RemoteToolManager",
16
+ "CompositeToolManager",
17
+ "ToolCache",
18
+ "CacheEntry",
19
+ ]