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.
- cade_cli-0.3.3.dist-info/METADATA +151 -0
- cade_cli-0.3.3.dist-info/RECORD +44 -0
- cade_cli-0.3.3.dist-info/WHEEL +4 -0
- cade_cli-0.3.3.dist-info/entry_points.txt +2 -0
- cadecoder/__init__.py +1 -0
- cadecoder/ai/__init__.py +6 -0
- cadecoder/ai/prompts.py +572 -0
- cadecoder/cli/__init__.py +0 -0
- cadecoder/cli/app.py +147 -0
- cadecoder/cli/auth.py +483 -0
- cadecoder/cli/commands/__init__.py +5 -0
- cadecoder/cli/commands/auth.py +143 -0
- cadecoder/cli/commands/chat.py +264 -0
- cadecoder/cli/commands/mcp.py +477 -0
- cadecoder/cli/commands/tools.py +226 -0
- cadecoder/core/__init__.py +12 -0
- cadecoder/core/config.py +380 -0
- cadecoder/core/constants.py +281 -0
- cadecoder/core/errors.py +145 -0
- cadecoder/core/logging.py +148 -0
- cadecoder/core/types.py +235 -0
- cadecoder/core/utils.py +279 -0
- cadecoder/execution/__init__.py +46 -0
- cadecoder/execution/context_window.py +521 -0
- cadecoder/execution/orchestrator.py +562 -0
- cadecoder/execution/parallel.py +287 -0
- cadecoder/providers/__init__.py +60 -0
- cadecoder/providers/base.py +294 -0
- cadecoder/providers/openai.py +251 -0
- cadecoder/storage/__init__.py +0 -0
- cadecoder/storage/threads.py +489 -0
- cadecoder/templates/login_failed.html +21 -0
- cadecoder/templates/login_success.html +21 -0
- cadecoder/templates/styles.css +87 -0
- cadecoder/tools/__init__.py +19 -0
- cadecoder/tools/builtin.py +644 -0
- cadecoder/tools/filesystem.py +315 -0
- cadecoder/tools/git.py +221 -0
- cadecoder/tools/manager.py +1635 -0
- cadecoder/ui/__init__.py +7 -0
- cadecoder/ui/display.py +338 -0
- cadecoder/ui/input.py +145 -0
- cadecoder/ui/session.py +455 -0
- 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
|
+
]
|