codeframe-ai 0.9.0__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.
- codeframe/__init__.py +11 -0
- codeframe/__main__.py +20 -0
- codeframe/adapters/__init__.py +5 -0
- codeframe/adapters/e2b/__init__.py +13 -0
- codeframe/adapters/e2b/adapter.py +342 -0
- codeframe/adapters/e2b/budget.py +71 -0
- codeframe/adapters/e2b/credential_scanner.py +134 -0
- codeframe/adapters/llm/__init__.py +92 -0
- codeframe/adapters/llm/anthropic.py +414 -0
- codeframe/adapters/llm/base.py +444 -0
- codeframe/adapters/llm/mock.py +281 -0
- codeframe/adapters/llm/openai.py +483 -0
- codeframe/agents/__init__.py +8 -0
- codeframe/agents/dependency_resolver.py +714 -0
- codeframe/auth/__init__.py +16 -0
- codeframe/auth/api_key_router.py +238 -0
- codeframe/auth/api_keys.py +156 -0
- codeframe/auth/dependencies.py +358 -0
- codeframe/auth/manager.py +178 -0
- codeframe/auth/models.py +30 -0
- codeframe/auth/router.py +93 -0
- codeframe/auth/schemas.py +15 -0
- codeframe/auth/scopes.py +53 -0
- codeframe/cli/__init__.py +12 -0
- codeframe/cli/__main__.py +20 -0
- codeframe/cli/api_client.py +275 -0
- codeframe/cli/app.py +5688 -0
- codeframe/cli/auth.py +122 -0
- codeframe/cli/auth_commands.py +958 -0
- codeframe/cli/commands/__init__.py +5 -0
- codeframe/cli/config_commands.py +79 -0
- codeframe/cli/dashboard_commands.py +67 -0
- codeframe/cli/engines_commands.py +205 -0
- codeframe/cli/env_commands.py +409 -0
- codeframe/cli/helpers.py +56 -0
- codeframe/cli/hooks_commands.py +208 -0
- codeframe/cli/import_commands.py +129 -0
- codeframe/cli/pr_commands.py +549 -0
- codeframe/cli/proof_commands.py +415 -0
- codeframe/cli/stats_commands.py +311 -0
- codeframe/cli/telemetry_runtime.py +153 -0
- codeframe/cli/validators.py +123 -0
- codeframe/config/rate_limits.py +165 -0
- codeframe/core/__init__.py +15 -0
- codeframe/core/adapters/__init__.py +43 -0
- codeframe/core/adapters/agent_adapter.py +114 -0
- codeframe/core/adapters/builtin.py +326 -0
- codeframe/core/adapters/claude_code.py +62 -0
- codeframe/core/adapters/codex.py +393 -0
- codeframe/core/adapters/git_utils.py +40 -0
- codeframe/core/adapters/kilocode.py +126 -0
- codeframe/core/adapters/opencode.py +48 -0
- codeframe/core/adapters/streaming_chat.py +483 -0
- codeframe/core/adapters/subprocess_adapter.py +213 -0
- codeframe/core/adapters/verification_wrapper.py +269 -0
- codeframe/core/agent.py +2183 -0
- codeframe/core/agents_config.py +569 -0
- codeframe/core/api_key_service.py +211 -0
- codeframe/core/artifacts.py +428 -0
- codeframe/core/blocker_detection.py +218 -0
- codeframe/core/blockers.py +433 -0
- codeframe/core/checkpoints.py +481 -0
- codeframe/core/conductor.py +2255 -0
- codeframe/core/config.py +827 -0
- codeframe/core/config_watcher.py +268 -0
- codeframe/core/context.py +542 -0
- codeframe/core/context_packager.py +234 -0
- codeframe/core/credentials.py +735 -0
- codeframe/core/dependency_analyzer.py +229 -0
- codeframe/core/dependency_graph.py +290 -0
- codeframe/core/diagnostic_agent.py +712 -0
- codeframe/core/diagnostics.py +616 -0
- codeframe/core/editor.py +556 -0
- codeframe/core/engine_registry.py +256 -0
- codeframe/core/engine_stats.py +231 -0
- codeframe/core/environment.py +697 -0
- codeframe/core/events.py +375 -0
- codeframe/core/executor.py +1005 -0
- codeframe/core/fix_tracker.py +480 -0
- codeframe/core/gates.py +1322 -0
- codeframe/core/git.py +477 -0
- codeframe/core/github_connect_service.py +178 -0
- codeframe/core/github_integration_config.py +118 -0
- codeframe/core/github_issues_service.py +449 -0
- codeframe/core/hooks.py +184 -0
- codeframe/core/importers/__init__.py +1 -0
- codeframe/core/importers/ralph.py +540 -0
- codeframe/core/installer.py +650 -0
- codeframe/core/models.py +1026 -0
- codeframe/core/notifications_config.py +183 -0
- codeframe/core/planner.py +437 -0
- codeframe/core/prd.py +670 -0
- codeframe/core/prd_discovery.py +1118 -0
- codeframe/core/prd_stress_test.py +499 -0
- codeframe/core/progress.py +126 -0
- codeframe/core/proof/__init__.py +34 -0
- codeframe/core/proof/capture.py +79 -0
- codeframe/core/proof/evidence.py +56 -0
- codeframe/core/proof/ledger.py +574 -0
- codeframe/core/proof/models.py +162 -0
- codeframe/core/proof/obligations.py +103 -0
- codeframe/core/proof/runner.py +233 -0
- codeframe/core/proof/scope.py +81 -0
- codeframe/core/proof/stubs.py +156 -0
- codeframe/core/quick_fixes.py +558 -0
- codeframe/core/react_agent.py +1650 -0
- codeframe/core/reconciliation.py +183 -0
- codeframe/core/replay.py +788 -0
- codeframe/core/review.py +285 -0
- codeframe/core/runtime.py +1134 -0
- codeframe/core/sandbox/__init__.py +27 -0
- codeframe/core/sandbox/context.py +98 -0
- codeframe/core/sandbox/worktree.py +20 -0
- codeframe/core/schedule.py +396 -0
- codeframe/core/stall_detector.py +71 -0
- codeframe/core/stall_monitor.py +134 -0
- codeframe/core/state_machine.py +121 -0
- codeframe/core/streaming.py +502 -0
- codeframe/core/task_tree.py +400 -0
- codeframe/core/tasks.py +1022 -0
- codeframe/core/telemetry.py +232 -0
- codeframe/core/templates.py +221 -0
- codeframe/core/tools.py +942 -0
- codeframe/core/workspace.py +887 -0
- codeframe/core/worktrees.py +276 -0
- codeframe/git/__init__.py +5 -0
- codeframe/git/github_integration.py +505 -0
- codeframe/lib/__init__.py +0 -0
- codeframe/lib/audit_logger.py +248 -0
- codeframe/lib/metrics_tracker.py +800 -0
- codeframe/lib/quality/__init__.py +7 -0
- codeframe/lib/quality/complexity_analyzer.py +316 -0
- codeframe/lib/quality/owasp_patterns.py +284 -0
- codeframe/lib/quality/security_scanner.py +250 -0
- codeframe/lib/rate_limiter.py +312 -0
- codeframe/notifications/__init__.py +0 -0
- codeframe/notifications/webhook.py +380 -0
- codeframe/planning/__init__.py +30 -0
- codeframe/planning/issue_generator.py +219 -0
- codeframe/planning/prd_template_functions.py +137 -0
- codeframe/planning/prd_templates.py +975 -0
- codeframe/planning/task_scheduler.py +511 -0
- codeframe/planning/task_templates.py +533 -0
- codeframe/platform_store/__init__.py +5 -0
- codeframe/platform_store/database.py +277 -0
- codeframe/platform_store/repositories/__init__.py +24 -0
- codeframe/platform_store/repositories/api_key_repository.py +245 -0
- codeframe/platform_store/repositories/audit_repository.py +67 -0
- codeframe/platform_store/repositories/base.py +295 -0
- codeframe/platform_store/repositories/interactive_sessions.py +165 -0
- codeframe/platform_store/repositories/token_repository.py +598 -0
- codeframe/platform_store/repositories/workspace_registry_repository.py +175 -0
- codeframe/platform_store/schema_manager.py +321 -0
- codeframe/templates/AGENTS.md.default +94 -0
- codeframe/tui/__init__.py +5 -0
- codeframe/tui/app.py +256 -0
- codeframe/tui/data_service.py +103 -0
- codeframe/ui/__init__.py +0 -0
- codeframe/ui/dependencies.py +103 -0
- codeframe/ui/models.py +999 -0
- codeframe/ui/response_models.py +201 -0
- codeframe/ui/routers/__init__.py +5 -0
- codeframe/ui/routers/_helpers.py +29 -0
- codeframe/ui/routers/batches_v2.py +315 -0
- codeframe/ui/routers/blockers_v2.py +320 -0
- codeframe/ui/routers/checkpoints_v2.py +310 -0
- codeframe/ui/routers/costs_v2.py +322 -0
- codeframe/ui/routers/diagnose_v2.py +225 -0
- codeframe/ui/routers/discovery_v2.py +417 -0
- codeframe/ui/routers/environment_v2.py +284 -0
- codeframe/ui/routers/events_v2.py +75 -0
- codeframe/ui/routers/gates_v2.py +166 -0
- codeframe/ui/routers/git_v2.py +284 -0
- codeframe/ui/routers/github_integrations_v2.py +532 -0
- codeframe/ui/routers/interactive_sessions_v2.py +238 -0
- codeframe/ui/routers/pr_v2.py +709 -0
- codeframe/ui/routers/prd_v2.py +695 -0
- codeframe/ui/routers/proof_v2.py +755 -0
- codeframe/ui/routers/review_v2.py +360 -0
- codeframe/ui/routers/schedule_v2.py +214 -0
- codeframe/ui/routers/session_chat_ws.py +354 -0
- codeframe/ui/routers/settings_v2.py +562 -0
- codeframe/ui/routers/streaming_v2.py +155 -0
- codeframe/ui/routers/tasks_v2.py +1098 -0
- codeframe/ui/routers/templates_v2.py +232 -0
- codeframe/ui/routers/terminal_ws.py +267 -0
- codeframe/ui/routers/workspace_v2.py +527 -0
- codeframe/ui/server.py +568 -0
- codeframe/ui/shared.py +241 -0
- codeframe/workspace/__init__.py +5 -0
- codeframe/workspace/manager.py +249 -0
- codeframe_ai-0.9.0.dist-info/METADATA +517 -0
- codeframe_ai-0.9.0.dist-info/RECORD +197 -0
- codeframe_ai-0.9.0.dist-info/WHEEL +5 -0
- codeframe_ai-0.9.0.dist-info/entry_points.txt +3 -0
- codeframe_ai-0.9.0.dist-info/licenses/LICENSE +661 -0
- codeframe_ai-0.9.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""Repository for the workspace registry (issue #601).
|
|
2
|
+
|
|
3
|
+
The registry lives in the global control-plane DB and stores only project
|
|
4
|
+
metadata + a pointer (``repo_path``) to each per-workspace
|
|
5
|
+
``.codeframe/state.db``. It enables server-side listing of "your projects" so
|
|
6
|
+
the web UI switcher can be backed by the server (with a localStorage fallback)
|
|
7
|
+
instead of living purely in the browser.
|
|
8
|
+
|
|
9
|
+
It is intentionally NOT a revival of the v1 global ``projects`` table — no
|
|
10
|
+
domain data is stored here, only pointers and metadata.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import uuid
|
|
14
|
+
from datetime import datetime, timezone
|
|
15
|
+
from typing import Any, Dict, List, Optional
|
|
16
|
+
import logging
|
|
17
|
+
|
|
18
|
+
from codeframe.platform_store.repositories.base import BaseRepository
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class WorkspaceRegistryRepository(BaseRepository):
|
|
24
|
+
"""Repository for workspace registry operations."""
|
|
25
|
+
|
|
26
|
+
def upsert(
|
|
27
|
+
self,
|
|
28
|
+
repo_path: str,
|
|
29
|
+
name: Optional[str] = None,
|
|
30
|
+
tech_stack: Optional[str] = None,
|
|
31
|
+
owner_user_id: Optional[int] = None,
|
|
32
|
+
) -> Dict[str, Any]:
|
|
33
|
+
"""Register (or refresh) a workspace by its repo path.
|
|
34
|
+
|
|
35
|
+
Idempotent on ``repo_path``: a second call for the same path updates the
|
|
36
|
+
existing row (name/tech_stack/owner + ``last_opened_at``) instead of
|
|
37
|
+
creating a duplicate, preserving the original ``id`` and ``created_at``.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
repo_path: Absolute path to the repository (unique key).
|
|
41
|
+
name: Human-readable display name (defaults to last path segment).
|
|
42
|
+
tech_stack: Natural-language tech stack description.
|
|
43
|
+
owner_user_id: Owning user id (nullable until auth is enforced).
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
The registry entry as a dict.
|
|
47
|
+
"""
|
|
48
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
49
|
+
new_id = str(uuid.uuid4())
|
|
50
|
+
|
|
51
|
+
# INSERT ... ON CONFLICT(repo_path) DO UPDATE keeps the original id and
|
|
52
|
+
# created_at while refreshing the mutable metadata and recency stamp.
|
|
53
|
+
self._execute(
|
|
54
|
+
"""
|
|
55
|
+
INSERT INTO workspaces_registry (
|
|
56
|
+
id, repo_path, name, owner_user_id, tech_stack,
|
|
57
|
+
created_at, last_opened_at
|
|
58
|
+
)
|
|
59
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
60
|
+
ON CONFLICT(repo_path) DO UPDATE SET
|
|
61
|
+
-- COALESCE so a refresh that omits name/tech_stack (None) keeps the
|
|
62
|
+
-- previously-stored value instead of nulling it.
|
|
63
|
+
name = COALESCE(excluded.name, workspaces_registry.name),
|
|
64
|
+
owner_user_id = excluded.owner_user_id,
|
|
65
|
+
tech_stack = COALESCE(excluded.tech_stack, workspaces_registry.tech_stack),
|
|
66
|
+
last_opened_at = excluded.last_opened_at
|
|
67
|
+
""",
|
|
68
|
+
(new_id, repo_path, name, owner_user_id, tech_stack, now, now),
|
|
69
|
+
)
|
|
70
|
+
self._commit()
|
|
71
|
+
|
|
72
|
+
# Re-read so callers always get the canonical row (stable id on conflict).
|
|
73
|
+
entry = self.get_by_path(repo_path)
|
|
74
|
+
if entry is None:
|
|
75
|
+
# Should be unreachable right after a successful upsert. Raise a real
|
|
76
|
+
# error (not assert, which `python -O` strips) so the dict-return
|
|
77
|
+
# contract holds in every execution mode.
|
|
78
|
+
raise RuntimeError(
|
|
79
|
+
f"workspaces_registry upsert succeeded but row not found: {repo_path}"
|
|
80
|
+
)
|
|
81
|
+
return entry
|
|
82
|
+
|
|
83
|
+
def list_all(self, owner_user_id: Optional[int] = None) -> List[Dict[str, Any]]:
|
|
84
|
+
"""List all registered workspaces, optionally filtered by owner.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
owner_user_id: When provided, only entries owned by this user are
|
|
88
|
+
returned. When ``None``, all entries are returned (the current
|
|
89
|
+
behaviour until auth is enforced on v2 routers).
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
List of registry entries ordered by ``last_opened_at`` descending.
|
|
93
|
+
"""
|
|
94
|
+
if owner_user_id is not None:
|
|
95
|
+
rows = self._fetchall(
|
|
96
|
+
"""
|
|
97
|
+
SELECT id, repo_path, name, owner_user_id, tech_stack,
|
|
98
|
+
created_at, last_opened_at
|
|
99
|
+
FROM workspaces_registry
|
|
100
|
+
WHERE owner_user_id = ?
|
|
101
|
+
ORDER BY last_opened_at DESC
|
|
102
|
+
""",
|
|
103
|
+
(owner_user_id,),
|
|
104
|
+
)
|
|
105
|
+
else:
|
|
106
|
+
rows = self._fetchall(
|
|
107
|
+
"""
|
|
108
|
+
SELECT id, repo_path, name, owner_user_id, tech_stack,
|
|
109
|
+
created_at, last_opened_at
|
|
110
|
+
FROM workspaces_registry
|
|
111
|
+
ORDER BY last_opened_at DESC
|
|
112
|
+
"""
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
return [self._row_to_workspace_registry(row) for row in rows]
|
|
116
|
+
|
|
117
|
+
def get_by_id(self, workspace_id: str) -> Optional[Dict[str, Any]]:
|
|
118
|
+
"""Get a registry entry by its id, or None if not found."""
|
|
119
|
+
row = self._fetchone(
|
|
120
|
+
"""
|
|
121
|
+
SELECT id, repo_path, name, owner_user_id, tech_stack,
|
|
122
|
+
created_at, last_opened_at
|
|
123
|
+
FROM workspaces_registry
|
|
124
|
+
WHERE id = ?
|
|
125
|
+
""",
|
|
126
|
+
(workspace_id,),
|
|
127
|
+
)
|
|
128
|
+
return self._row_to_workspace_registry(row) if row is not None else None
|
|
129
|
+
|
|
130
|
+
def get_by_path(self, repo_path: str) -> Optional[Dict[str, Any]]:
|
|
131
|
+
"""Get a registry entry by its repo path, or None if not found."""
|
|
132
|
+
row = self._fetchone(
|
|
133
|
+
"""
|
|
134
|
+
SELECT id, repo_path, name, owner_user_id, tech_stack,
|
|
135
|
+
created_at, last_opened_at
|
|
136
|
+
FROM workspaces_registry
|
|
137
|
+
WHERE repo_path = ?
|
|
138
|
+
""",
|
|
139
|
+
(repo_path,),
|
|
140
|
+
)
|
|
141
|
+
return self._row_to_workspace_registry(row) if row is not None else None
|
|
142
|
+
|
|
143
|
+
def update_last_opened(self, workspace_id: str) -> None:
|
|
144
|
+
"""Bump ``last_opened_at`` for a registry entry to maintain recency."""
|
|
145
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
146
|
+
self._execute(
|
|
147
|
+
"UPDATE workspaces_registry SET last_opened_at = ? WHERE id = ?",
|
|
148
|
+
(now, workspace_id),
|
|
149
|
+
)
|
|
150
|
+
self._commit()
|
|
151
|
+
|
|
152
|
+
def delete(self, workspace_id: str) -> bool:
|
|
153
|
+
"""Deregister a workspace (registry-only — never touches disk files).
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
True if a row was removed, False if the id was not found.
|
|
157
|
+
"""
|
|
158
|
+
cursor = self._execute(
|
|
159
|
+
"DELETE FROM workspaces_registry WHERE id = ?",
|
|
160
|
+
(workspace_id,),
|
|
161
|
+
)
|
|
162
|
+
self._commit()
|
|
163
|
+
return cursor.rowcount > 0
|
|
164
|
+
|
|
165
|
+
def _row_to_workspace_registry(self, row) -> Dict[str, Any]:
|
|
166
|
+
"""Convert a database row to a registry dict."""
|
|
167
|
+
return {
|
|
168
|
+
"id": row["id"],
|
|
169
|
+
"repo_path": row["repo_path"],
|
|
170
|
+
"name": row["name"],
|
|
171
|
+
"owner_user_id": row["owner_user_id"],
|
|
172
|
+
"tech_stack": row["tech_stack"],
|
|
173
|
+
"created_at": self._ensure_rfc3339(row["created_at"]),
|
|
174
|
+
"last_opened_at": self._ensure_rfc3339(row["last_opened_at"]),
|
|
175
|
+
}
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
"""Database schema management for CodeFRAME.
|
|
2
|
+
|
|
3
|
+
Handles schema creation, migrations, and database initialization.
|
|
4
|
+
Extracted from the monolithic Database class for better maintainability.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import sqlite3
|
|
8
|
+
import logging
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SchemaManager:
|
|
14
|
+
"""Manages database schema creation and migrations.
|
|
15
|
+
|
|
16
|
+
Responsible for creating all database tables, indexes, and ensuring
|
|
17
|
+
schema consistency across the application.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, conn: sqlite3.Connection):
|
|
21
|
+
"""Initialize schema manager with database connection.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
conn: Active sqlite3.Connection
|
|
25
|
+
"""
|
|
26
|
+
self.conn = conn
|
|
27
|
+
|
|
28
|
+
def create_schema(self) -> None:
|
|
29
|
+
"""Create all database tables and indexes.
|
|
30
|
+
|
|
31
|
+
Creates the minimal control-plane schema (auth, api keys, audit log,
|
|
32
|
+
interactive sessions). v2 domain data lives in per-workspace DBs.
|
|
33
|
+
This method is idempotent - safe to call multiple times.
|
|
34
|
+
"""
|
|
35
|
+
cursor = self.conn.cursor()
|
|
36
|
+
|
|
37
|
+
# Authentication tables (users, accounts, sessions, verification, api_keys)
|
|
38
|
+
self._create_auth_tables(cursor)
|
|
39
|
+
|
|
40
|
+
# Audit log table
|
|
41
|
+
self._create_audit_log_table(cursor)
|
|
42
|
+
|
|
43
|
+
# Interactive session tables
|
|
44
|
+
self._create_interactive_session_tables(cursor)
|
|
45
|
+
|
|
46
|
+
# Workspace registry table (issue #601)
|
|
47
|
+
self._create_workspaces_registry_table(cursor)
|
|
48
|
+
|
|
49
|
+
# Create indexes
|
|
50
|
+
self._create_indexes(cursor)
|
|
51
|
+
|
|
52
|
+
self.conn.commit()
|
|
53
|
+
|
|
54
|
+
# Ensure default admin user exists
|
|
55
|
+
self._ensure_default_admin_user()
|
|
56
|
+
|
|
57
|
+
def _create_auth_tables(self, cursor: sqlite3.Cursor) -> None:
|
|
58
|
+
"""Create authentication tables (fastapi-users compatible)."""
|
|
59
|
+
cursor.execute(
|
|
60
|
+
"""
|
|
61
|
+
CREATE TABLE IF NOT EXISTS users (
|
|
62
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
63
|
+
email TEXT UNIQUE NOT NULL,
|
|
64
|
+
name TEXT,
|
|
65
|
+
hashed_password TEXT NOT NULL,
|
|
66
|
+
is_active INTEGER DEFAULT 1,
|
|
67
|
+
is_superuser INTEGER DEFAULT 0,
|
|
68
|
+
is_verified INTEGER DEFAULT 0,
|
|
69
|
+
email_verified INTEGER DEFAULT 0,
|
|
70
|
+
image TEXT,
|
|
71
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
72
|
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
73
|
+
)
|
|
74
|
+
"""
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
# Accounts table (BetterAuth compatible - stores passwords and OAuth)
|
|
78
|
+
cursor.execute(
|
|
79
|
+
"""
|
|
80
|
+
CREATE TABLE IF NOT EXISTS accounts (
|
|
81
|
+
id TEXT PRIMARY KEY,
|
|
82
|
+
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
83
|
+
account_id TEXT NOT NULL,
|
|
84
|
+
provider_id TEXT NOT NULL,
|
|
85
|
+
password TEXT,
|
|
86
|
+
access_token TEXT,
|
|
87
|
+
refresh_token TEXT,
|
|
88
|
+
id_token TEXT,
|
|
89
|
+
access_token_expires_at TIMESTAMP,
|
|
90
|
+
refresh_token_expires_at TIMESTAMP,
|
|
91
|
+
scope TEXT,
|
|
92
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
93
|
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
94
|
+
UNIQUE(user_id, provider_id)
|
|
95
|
+
)
|
|
96
|
+
"""
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
# Create index on user_id for faster login lookups
|
|
100
|
+
cursor.execute(
|
|
101
|
+
"""
|
|
102
|
+
CREATE INDEX IF NOT EXISTS idx_accounts_user_id ON accounts(user_id)
|
|
103
|
+
"""
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
# Sessions table (BetterAuth compatible)
|
|
107
|
+
cursor.execute(
|
|
108
|
+
"""
|
|
109
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
110
|
+
id TEXT PRIMARY KEY,
|
|
111
|
+
token TEXT UNIQUE NOT NULL,
|
|
112
|
+
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
113
|
+
expires_at TIMESTAMP NOT NULL,
|
|
114
|
+
ip_address TEXT,
|
|
115
|
+
user_agent TEXT,
|
|
116
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
117
|
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
118
|
+
)
|
|
119
|
+
"""
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
# Verification table (BetterAuth compatible)
|
|
123
|
+
# Used for email verification tokens when requireEmailVerification is enabled
|
|
124
|
+
cursor.execute(
|
|
125
|
+
"""
|
|
126
|
+
CREATE TABLE IF NOT EXISTS verification (
|
|
127
|
+
id TEXT PRIMARY KEY,
|
|
128
|
+
identifier TEXT NOT NULL,
|
|
129
|
+
value TEXT NOT NULL,
|
|
130
|
+
expires_at TIMESTAMP NOT NULL
|
|
131
|
+
)
|
|
132
|
+
"""
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
# API Keys table for programmatic access
|
|
136
|
+
cursor.execute(
|
|
137
|
+
"""
|
|
138
|
+
CREATE TABLE IF NOT EXISTS api_keys (
|
|
139
|
+
id TEXT PRIMARY KEY,
|
|
140
|
+
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
141
|
+
name TEXT NOT NULL,
|
|
142
|
+
key_hash TEXT NOT NULL,
|
|
143
|
+
prefix TEXT NOT NULL,
|
|
144
|
+
scopes TEXT NOT NULL,
|
|
145
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
146
|
+
last_used_at TIMESTAMP,
|
|
147
|
+
expires_at TIMESTAMP,
|
|
148
|
+
is_active INTEGER DEFAULT 1
|
|
149
|
+
)
|
|
150
|
+
"""
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
# Indexes for api_keys table
|
|
154
|
+
cursor.execute(
|
|
155
|
+
"""
|
|
156
|
+
CREATE INDEX IF NOT EXISTS idx_api_keys_prefix ON api_keys(prefix)
|
|
157
|
+
"""
|
|
158
|
+
)
|
|
159
|
+
cursor.execute(
|
|
160
|
+
"""
|
|
161
|
+
CREATE INDEX IF NOT EXISTS idx_api_keys_user_id ON api_keys(user_id)
|
|
162
|
+
"""
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
def _create_interactive_session_tables(self, cursor: sqlite3.Cursor) -> None:
|
|
166
|
+
"""Create interactive_sessions and session_messages tables."""
|
|
167
|
+
cursor.execute(
|
|
168
|
+
"""
|
|
169
|
+
CREATE TABLE IF NOT EXISTS interactive_sessions (
|
|
170
|
+
id TEXT PRIMARY KEY,
|
|
171
|
+
workspace_path TEXT NOT NULL,
|
|
172
|
+
task_id TEXT,
|
|
173
|
+
state TEXT NOT NULL DEFAULT 'active'
|
|
174
|
+
CHECK (state IN ('active', 'paused', 'ended')),
|
|
175
|
+
agent_type TEXT NOT NULL DEFAULT 'claude',
|
|
176
|
+
model TEXT,
|
|
177
|
+
cost_usd REAL DEFAULT 0.0,
|
|
178
|
+
input_tokens INTEGER DEFAULT 0,
|
|
179
|
+
output_tokens INTEGER DEFAULT 0,
|
|
180
|
+
created_at TEXT NOT NULL,
|
|
181
|
+
updated_at TEXT NOT NULL,
|
|
182
|
+
ended_at TEXT
|
|
183
|
+
)
|
|
184
|
+
"""
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
cursor.execute(
|
|
188
|
+
"""
|
|
189
|
+
CREATE TABLE IF NOT EXISTS session_messages (
|
|
190
|
+
id TEXT PRIMARY KEY,
|
|
191
|
+
session_id TEXT NOT NULL REFERENCES interactive_sessions(id) ON DELETE CASCADE,
|
|
192
|
+
role TEXT NOT NULL
|
|
193
|
+
CHECK (role IN ('user', 'assistant', 'tool_use', 'tool_result', 'thinking', 'system', 'error')),
|
|
194
|
+
content TEXT NOT NULL,
|
|
195
|
+
metadata TEXT,
|
|
196
|
+
created_at TEXT NOT NULL
|
|
197
|
+
)
|
|
198
|
+
"""
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
def _create_workspaces_registry_table(self, cursor: sqlite3.Cursor) -> None:
|
|
202
|
+
"""Create the workspaces_registry table (issue #601).
|
|
203
|
+
|
|
204
|
+
Stores cross-workspace, cross-device project metadata plus a pointer
|
|
205
|
+
(``repo_path``) to each per-workspace ``.codeframe/state.db``. It does
|
|
206
|
+
NOT hold any domain data — per-workspace isolation is unchanged. This is
|
|
207
|
+
deliberately not a revival of the v1 global ``projects`` table.
|
|
208
|
+
"""
|
|
209
|
+
cursor.execute(
|
|
210
|
+
"""
|
|
211
|
+
CREATE TABLE IF NOT EXISTS workspaces_registry (
|
|
212
|
+
id TEXT PRIMARY KEY,
|
|
213
|
+
repo_path TEXT UNIQUE NOT NULL,
|
|
214
|
+
name TEXT,
|
|
215
|
+
owner_user_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
|
|
216
|
+
tech_stack TEXT,
|
|
217
|
+
created_at TEXT NOT NULL,
|
|
218
|
+
last_opened_at TEXT NOT NULL
|
|
219
|
+
)
|
|
220
|
+
"""
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
def _create_audit_log_table(self, cursor: sqlite3.Cursor) -> None:
|
|
224
|
+
"""Create the audit log table."""
|
|
225
|
+
cursor.execute(
|
|
226
|
+
"""
|
|
227
|
+
CREATE TABLE IF NOT EXISTS audit_logs (
|
|
228
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
229
|
+
event_type TEXT NOT NULL,
|
|
230
|
+
user_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
|
|
231
|
+
resource_type TEXT NOT NULL,
|
|
232
|
+
resource_id INTEGER,
|
|
233
|
+
ip_address TEXT,
|
|
234
|
+
metadata TEXT,
|
|
235
|
+
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
236
|
+
)
|
|
237
|
+
"""
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
def _create_indexes(self, cursor: sqlite3.Cursor) -> None:
|
|
241
|
+
"""Create indexes for the live platform tables."""
|
|
242
|
+
# Interactive session indexes
|
|
243
|
+
cursor.execute(
|
|
244
|
+
"CREATE INDEX IF NOT EXISTS idx_interactive_sessions_workspace "
|
|
245
|
+
"ON interactive_sessions(workspace_path, state)"
|
|
246
|
+
)
|
|
247
|
+
cursor.execute(
|
|
248
|
+
"CREATE INDEX IF NOT EXISTS idx_interactive_sessions_state "
|
|
249
|
+
"ON interactive_sessions(state, created_at DESC)"
|
|
250
|
+
)
|
|
251
|
+
cursor.execute(
|
|
252
|
+
"CREATE INDEX IF NOT EXISTS idx_session_messages_session "
|
|
253
|
+
"ON session_messages(session_id, created_at)"
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
# Audit log indexes
|
|
257
|
+
cursor.execute(
|
|
258
|
+
"CREATE INDEX IF NOT EXISTS idx_audit_logs_user_id "
|
|
259
|
+
"ON audit_logs(user_id, timestamp DESC)"
|
|
260
|
+
)
|
|
261
|
+
cursor.execute(
|
|
262
|
+
"CREATE INDEX IF NOT EXISTS idx_audit_logs_event_type "
|
|
263
|
+
"ON audit_logs(event_type, timestamp DESC)"
|
|
264
|
+
)
|
|
265
|
+
cursor.execute(
|
|
266
|
+
"CREATE INDEX IF NOT EXISTS idx_audit_logs_resource "
|
|
267
|
+
"ON audit_logs(resource_type, resource_id, timestamp DESC)"
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
# Authentication indexes (api_keys/accounts indexes are created inline
|
|
271
|
+
# with their tables in _create_auth_tables)
|
|
272
|
+
cursor.execute("CREATE INDEX IF NOT EXISTS idx_users_email ON users(email)")
|
|
273
|
+
cursor.execute("CREATE INDEX IF NOT EXISTS idx_sessions_user_id ON sessions(user_id)")
|
|
274
|
+
cursor.execute(
|
|
275
|
+
"CREATE INDEX IF NOT EXISTS idx_sessions_expires_at ON sessions(expires_at)"
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
# Workspace registry indexes (issue #601)
|
|
279
|
+
cursor.execute(
|
|
280
|
+
"CREATE INDEX IF NOT EXISTS idx_workspaces_registry_owner "
|
|
281
|
+
"ON workspaces_registry(owner_user_id)"
|
|
282
|
+
)
|
|
283
|
+
cursor.execute(
|
|
284
|
+
"CREATE INDEX IF NOT EXISTS idx_workspaces_registry_last_opened "
|
|
285
|
+
"ON workspaces_registry(last_opened_at DESC)"
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
def _ensure_default_admin_user(self) -> None:
|
|
289
|
+
"""Ensure default admin user exists in database for initial setup.
|
|
290
|
+
|
|
291
|
+
Creates admin user with id=1 if it doesn't exist. This provides
|
|
292
|
+
a bootstrap user for test fixtures and initial database setup.
|
|
293
|
+
|
|
294
|
+
SECURITY: The admin user has a disabled password placeholder
|
|
295
|
+
that cannot match any bcrypt hash, so it cannot be used for
|
|
296
|
+
direct login. Users must register through the auth system.
|
|
297
|
+
|
|
298
|
+
Uses INSERT OR IGNORE to avoid conflicts with test fixtures.
|
|
299
|
+
"""
|
|
300
|
+
cursor = self.conn.cursor()
|
|
301
|
+
|
|
302
|
+
# Create user record (FastAPI Users compatible)
|
|
303
|
+
# hashed_password uses a placeholder that cannot match any bcrypt hash
|
|
304
|
+
cursor.execute(
|
|
305
|
+
"""
|
|
306
|
+
INSERT OR IGNORE INTO users (
|
|
307
|
+
id, email, name, hashed_password,
|
|
308
|
+
is_active, is_superuser, is_verified, email_verified
|
|
309
|
+
)
|
|
310
|
+
VALUES (1, 'admin@localhost', 'Admin User', '!DISABLED!', 1, 1, 1, 1)
|
|
311
|
+
"""
|
|
312
|
+
)
|
|
313
|
+
user_created = cursor.rowcount > 0
|
|
314
|
+
|
|
315
|
+
if user_created:
|
|
316
|
+
logger.debug(
|
|
317
|
+
"Created default admin user (id=1) for test fixtures. "
|
|
318
|
+
"This account has a disabled password and cannot be used for login."
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
self.conn.commit()
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# AGENTS.md - Project Instructions for AI Coding Agents
|
|
2
|
+
|
|
3
|
+
This file provides instructions for AI coding agents working in this repository.
|
|
4
|
+
For more information, see: https://agents.md/
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Tooling
|
|
9
|
+
|
|
10
|
+
- **package_manager**: uv (for Python), npm (for JavaScript)
|
|
11
|
+
- **test_runner**: pytest (Python), jest (JavaScript)
|
|
12
|
+
- **linter**: ruff (Python), eslint (JavaScript)
|
|
13
|
+
- **formatter**: black (Python), prettier (JavaScript)
|
|
14
|
+
|
|
15
|
+
## Commands
|
|
16
|
+
|
|
17
|
+
- **install**: `uv sync` or `npm install`
|
|
18
|
+
- **test**: `uv run pytest` or `npm test`
|
|
19
|
+
- **lint**: `uv run ruff check --fix .` or `npm run lint`
|
|
20
|
+
- **build**: `uv run python -m build` or `npm run build`
|
|
21
|
+
|
|
22
|
+
## Always Do
|
|
23
|
+
|
|
24
|
+
These actions are safe and the agent should perform autonomously:
|
|
25
|
+
|
|
26
|
+
- Choose between equivalent implementation approaches
|
|
27
|
+
- Decide file organization and naming conventions
|
|
28
|
+
- Select library versions (prefer latest stable)
|
|
29
|
+
- Handle existing files appropriately (overwrite, merge, or extend)
|
|
30
|
+
- Choose test frameworks and configurations based on project patterns
|
|
31
|
+
- Make code style decisions following existing codebase conventions
|
|
32
|
+
- Install dependencies using the project's package manager
|
|
33
|
+
- Create directories and files as needed
|
|
34
|
+
- Fix linting and formatting errors automatically
|
|
35
|
+
- Run tests after making changes
|
|
36
|
+
- Use type hints in Python code
|
|
37
|
+
- Follow existing import ordering patterns
|
|
38
|
+
|
|
39
|
+
## Ask First
|
|
40
|
+
|
|
41
|
+
These decisions require user confirmation before proceeding:
|
|
42
|
+
|
|
43
|
+
- Delete critical configuration files (.env, package.json, pyproject.toml)
|
|
44
|
+
- Make breaking changes to public APIs
|
|
45
|
+
- Modify security-sensitive code (auth, encryption, access control)
|
|
46
|
+
- Remove or disable existing tests
|
|
47
|
+
- Change database schemas or migrations
|
|
48
|
+
- Upgrade major versions of core dependencies
|
|
49
|
+
- Refactor code outside the immediate scope of the task
|
|
50
|
+
|
|
51
|
+
## Never Do
|
|
52
|
+
|
|
53
|
+
These actions are forbidden:
|
|
54
|
+
|
|
55
|
+
- Commit secrets, API keys, passwords, or credentials to the repository
|
|
56
|
+
- Delete the .git directory or git history
|
|
57
|
+
- Push directly to main/master branch without explicit permission
|
|
58
|
+
- Modify files outside the workspace directory
|
|
59
|
+
- Execute commands that could cause data loss (rm -rf /, DROP DATABASE)
|
|
60
|
+
- Bypass security validation or access controls
|
|
61
|
+
- Install packages from untrusted sources
|
|
62
|
+
- Disable or ignore security warnings
|
|
63
|
+
- Access external services without explicit permission
|
|
64
|
+
- Make network requests to unknown hosts
|
|
65
|
+
|
|
66
|
+
## Code Style
|
|
67
|
+
|
|
68
|
+
- **python_imports**: isort with black profile, absolute imports preferred
|
|
69
|
+
- **python_docstrings**: Google style docstrings for public functions
|
|
70
|
+
- **python_type_hints**: Required for function signatures
|
|
71
|
+
- **javascript_modules**: ES modules (import/export)
|
|
72
|
+
- **line_length**: 88 for Python, 100 for JavaScript
|
|
73
|
+
- **indentation**: 4 spaces for Python, 2 spaces for JavaScript
|
|
74
|
+
|
|
75
|
+
## Project Structure
|
|
76
|
+
|
|
77
|
+
Describe your project structure here:
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
project/
|
|
81
|
+
├── src/ # Source code
|
|
82
|
+
├── tests/ # Test files
|
|
83
|
+
├── docs/ # Documentation
|
|
84
|
+
├── scripts/ # Utility scripts
|
|
85
|
+
└── README.md # Project documentation
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Notes
|
|
89
|
+
|
|
90
|
+
Add any project-specific notes for the agent here:
|
|
91
|
+
|
|
92
|
+
- Describe any unusual patterns or conventions
|
|
93
|
+
- Note any known issues or quirks
|
|
94
|
+
- Explain any domain-specific terminology
|