gdmcode 0.1.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.
Files changed (131) hide show
  1. gdmcode-0.1.0.dist-info/METADATA +240 -0
  2. gdmcode-0.1.0.dist-info/RECORD +131 -0
  3. gdmcode-0.1.0.dist-info/WHEEL +4 -0
  4. gdmcode-0.1.0.dist-info/entry_points.txt +2 -0
  5. src/__init__.py +1 -0
  6. src/_internal/__init__.py +0 -0
  7. src/_internal/constants.py +244 -0
  8. src/_internal/domain_skills.py +339 -0
  9. src/agent/__init__.py +0 -0
  10. src/agent/commit_classifier.py +91 -0
  11. src/agent/context_budget.py +391 -0
  12. src/agent/daemon.py +681 -0
  13. src/agent/dag_validator.py +153 -0
  14. src/agent/debug_loop.py +473 -0
  15. src/agent/impact_analyzer.py +149 -0
  16. src/agent/impact_graph.py +117 -0
  17. src/agent/loop.py +1410 -0
  18. src/agent/orchestrator.py +141 -0
  19. src/agent/regression_guard.py +251 -0
  20. src/agent/review_gate.py +648 -0
  21. src/agent/risk_scorer.py +169 -0
  22. src/agent/self_healing.py +145 -0
  23. src/agent/smart_test_selector.py +89 -0
  24. src/agent/system_prompt.py +226 -0
  25. src/agent/task_tracker.py +320 -0
  26. src/agent/test_validator.py +210 -0
  27. src/agent/tool_orchestrator.py +402 -0
  28. src/agent/transcript.py +230 -0
  29. src/agent/verification_loop.py +133 -0
  30. src/agent/work_director.py +136 -0
  31. src/agent/worktree_manager.py +53 -0
  32. src/artifacts/__init__.py +16 -0
  33. src/artifacts/artifact_store.py +456 -0
  34. src/artifacts/verification_graph.py +75 -0
  35. src/auth.py +411 -0
  36. src/cli.py +1290 -0
  37. src/commands.py +1398 -0
  38. src/config.py +762 -0
  39. src/cost_tracker.py +348 -0
  40. src/db/__init__.py +4 -0
  41. src/db/migrations.py +337 -0
  42. src/enterprise/__init__.py +3 -0
  43. src/enterprise/audit_log.py +182 -0
  44. src/enterprise/identity.py +90 -0
  45. src/enterprise/rbac.py +100 -0
  46. src/enterprise/team_config.py +125 -0
  47. src/enterprise/usage_analytics.py +261 -0
  48. src/exceptions.py +207 -0
  49. src/git_workflow.py +651 -0
  50. src/integrations/__init__.py +6 -0
  51. src/integrations/github_actions.py +106 -0
  52. src/integrations/mcp_server.py +333 -0
  53. src/integrations/sentry_integration.py +100 -0
  54. src/integrations/sentry_server.py +82 -0
  55. src/integrations/webhook_security.py +19 -0
  56. src/main.py +27 -0
  57. src/memory/__init__.py +0 -0
  58. src/memory/code_index.py +376 -0
  59. src/memory/compressor.py +378 -0
  60. src/memory/context_memory.py +135 -0
  61. src/memory/continuous_memory.py +234 -0
  62. src/memory/conventions.py +495 -0
  63. src/memory/db.py +1119 -0
  64. src/memory/document_index.py +205 -0
  65. src/memory/file_cache.py +128 -0
  66. src/memory/project_scanner.py +178 -0
  67. src/memory/session_store.py +201 -0
  68. src/models/__init__.py +0 -0
  69. src/models/client.py +715 -0
  70. src/models/definitions.py +459 -0
  71. src/models/router.py +418 -0
  72. src/models/schemas.py +389 -0
  73. src/permissions.py +294 -0
  74. src/remote/__init__.py +5 -0
  75. src/remote/command_filter.py +33 -0
  76. src/remote/models.py +31 -0
  77. src/remote/permission_handler.py +79 -0
  78. src/remote/phone_ui.py +48 -0
  79. src/remote/protocol.py +59 -0
  80. src/remote/qr.py +65 -0
  81. src/remote/server.py +586 -0
  82. src/remote/token_manager.py +61 -0
  83. src/remote/tunnel.py +212 -0
  84. src/repl.py +475 -0
  85. src/runtime/__init__.py +1 -0
  86. src/runtime/branch_farm.py +372 -0
  87. src/runtime/replay.py +351 -0
  88. src/sandbox/__init__.py +2 -0
  89. src/sandbox/hermetic.py +214 -0
  90. src/sandbox/policy.py +44 -0
  91. src/sdk/__init__.py +3 -0
  92. src/sdk/plugin_base.py +39 -0
  93. src/sdk/plugin_host.py +100 -0
  94. src/sdk/plugin_loader.py +101 -0
  95. src/security.py +409 -0
  96. src/server/__init__.py +7 -0
  97. src/server/bridge.py +427 -0
  98. src/server/bridge_cli.py +103 -0
  99. src/server/bridge_client.py +170 -0
  100. src/server/protocol_version.py +103 -0
  101. src/session/__init__.py +10 -0
  102. src/session/event_fanout.py +46 -0
  103. src/session/input_broker.py +38 -0
  104. src/session/permission_bridge.py +100 -0
  105. src/tools/__init__.py +160 -0
  106. src/tools/_atomic.py +72 -0
  107. src/tools/agent_tools.py +423 -0
  108. src/tools/ask_user_tool.py +83 -0
  109. src/tools/bash_tool.py +384 -0
  110. src/tools/browser_tool.py +352 -0
  111. src/tools/browser_tools.py +179 -0
  112. src/tools/dep_tools.py +210 -0
  113. src/tools/document_reader.py +167 -0
  114. src/tools/document_tool.py +240 -0
  115. src/tools/document_writer.py +171 -0
  116. src/tools/impact_tools.py +240 -0
  117. src/tools/playwright_tool.py +172 -0
  118. src/tools/quality_tools.py +366 -0
  119. src/tools/read_tools.py +318 -0
  120. src/tools/result_cache.py +157 -0
  121. src/tools/search_tools.py +310 -0
  122. src/tools/shell_tools.py +311 -0
  123. src/tools/write_tools.py +337 -0
  124. src/voice/__init__.py +25 -0
  125. src/voice/audio_capture.py +92 -0
  126. src/voice/audio_playback.py +68 -0
  127. src/voice/errors.py +14 -0
  128. src/voice/models.py +35 -0
  129. src/voice/providers.py +143 -0
  130. src/voice/vad.py +55 -0
  131. src/voice/voice_loop.py +156 -0
@@ -0,0 +1,261 @@
1
+ """
2
+ Usage analytics and cost attribution for enterprise deployments.
3
+ Tracks token usage, tool calls, session costs, and team-level rollups.
4
+ """
5
+ from __future__ import annotations
6
+
7
+ import csv
8
+ import sqlite3
9
+ import threading
10
+ from dataclasses import dataclass, field
11
+ from datetime import datetime, timezone
12
+ from pathlib import Path
13
+ from typing import Optional
14
+
15
+ DB_PATH = Path.home() / ".config" / "gdm" / "analytics.db"
16
+
17
+
18
+ @dataclass
19
+ class UsageEvent:
20
+ session_id: str
21
+ actor_id: str
22
+ model: str
23
+ prompt_tokens: int
24
+ completion_tokens: int
25
+ tool_calls: int
26
+ task_id: Optional[str] = None
27
+ team: Optional[str] = None
28
+ timestamp: str = field(
29
+ default_factory=lambda: datetime.now(timezone.utc).isoformat()
30
+ )
31
+
32
+ @property
33
+ def total_tokens(self) -> int:
34
+ return self.prompt_tokens + self.completion_tokens
35
+
36
+ def estimated_cost_usd(
37
+ self,
38
+ price_per_1k_input: float = 0.003,
39
+ price_per_1k_output: float = 0.015,
40
+ ) -> float:
41
+ return (
42
+ self.prompt_tokens / 1000 * price_per_1k_input
43
+ + self.completion_tokens / 1000 * price_per_1k_output
44
+ )
45
+
46
+
47
+ @dataclass
48
+ class SessionSummary:
49
+ session_id: str
50
+ actor_id: str
51
+ total_prompt_tokens: int
52
+ total_completion_tokens: int
53
+ total_tool_calls: int
54
+ total_cost_usd: float
55
+ model: str
56
+ started_at: str
57
+ last_event_at: str
58
+
59
+
60
+ @dataclass
61
+ class TeamRollup:
62
+ team: str
63
+ period: str # "daily" | "weekly" | "monthly"
64
+ total_cost_usd: float
65
+ total_tokens: int
66
+ total_sessions: int
67
+ top_users: list[dict] # [{actor_id, cost_usd, sessions}]
68
+
69
+
70
+ class UsageAnalytics:
71
+ def __init__(self, db_path: Path = None):
72
+ self._path = db_path or DB_PATH
73
+ self._lock = threading.Lock()
74
+ self._conn: Optional[sqlite3.Connection] = None
75
+ self._init_db()
76
+
77
+ def _get_conn(self) -> sqlite3.Connection:
78
+ if self._conn is None:
79
+ self._path.parent.mkdir(parents=True, exist_ok=True)
80
+ self._conn = sqlite3.connect(str(self._path), check_same_thread=False)
81
+ self._conn.row_factory = sqlite3.Row
82
+ return self._conn
83
+
84
+ def _init_db(self) -> None:
85
+ with self._lock:
86
+ conn = self._get_conn()
87
+ conn.executescript("""
88
+ CREATE TABLE IF NOT EXISTS usage_events (
89
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
90
+ session_id TEXT NOT NULL,
91
+ actor_id TEXT NOT NULL,
92
+ model TEXT NOT NULL,
93
+ prompt_tokens INTEGER NOT NULL DEFAULT 0,
94
+ completion_tokens INTEGER NOT NULL DEFAULT 0,
95
+ tool_calls INTEGER NOT NULL DEFAULT 0,
96
+ task_id TEXT,
97
+ team TEXT,
98
+ timestamp TEXT NOT NULL,
99
+ cost_usd REAL NOT NULL DEFAULT 0.0
100
+ );
101
+ CREATE INDEX IF NOT EXISTS idx_usage_session ON usage_events(session_id);
102
+ CREATE INDEX IF NOT EXISTS idx_usage_actor ON usage_events(actor_id);
103
+ CREATE INDEX IF NOT EXISTS idx_usage_team ON usage_events(team);
104
+ CREATE INDEX IF NOT EXISTS idx_usage_timestamp ON usage_events(timestamp);
105
+ CREATE TABLE IF NOT EXISTS cost_budgets (
106
+ team TEXT PRIMARY KEY,
107
+ monthly_limit_usd REAL NOT NULL,
108
+ alert_threshold REAL NOT NULL DEFAULT 0.8
109
+ );
110
+ """)
111
+ conn.commit()
112
+
113
+ def record(self, event: "UsageEvent") -> None:
114
+ cost = event.estimated_cost_usd()
115
+ with self._lock:
116
+ self._get_conn().execute(
117
+ """INSERT INTO usage_events
118
+ (session_id, actor_id, model, prompt_tokens, completion_tokens,
119
+ tool_calls, task_id, team, timestamp, cost_usd)
120
+ VALUES (?,?,?,?,?,?,?,?,?,?)""",
121
+ (
122
+ event.session_id, event.actor_id, event.model,
123
+ event.prompt_tokens, event.completion_tokens, event.tool_calls,
124
+ event.task_id, event.team, event.timestamp, cost,
125
+ ),
126
+ )
127
+ self._get_conn().commit()
128
+
129
+ def get_session_summary(self, session_id: str) -> "Optional[SessionSummary]":
130
+ with self._lock:
131
+ row = self._get_conn().execute(
132
+ """SELECT session_id, actor_id, model,
133
+ SUM(prompt_tokens) as total_prompt,
134
+ SUM(completion_tokens) as total_completion,
135
+ SUM(tool_calls) as total_tools,
136
+ SUM(cost_usd) as total_cost,
137
+ MIN(timestamp) as started_at,
138
+ MAX(timestamp) as last_event_at
139
+ FROM usage_events WHERE session_id=? GROUP BY session_id""",
140
+ (session_id,),
141
+ ).fetchone()
142
+ if row is None:
143
+ return None
144
+ return SessionSummary(
145
+ session_id=row["session_id"],
146
+ actor_id=row["actor_id"],
147
+ total_prompt_tokens=row["total_prompt"],
148
+ total_completion_tokens=row["total_completion"],
149
+ total_tool_calls=row["total_tools"],
150
+ total_cost_usd=row["total_cost"],
151
+ model=row["model"],
152
+ started_at=row["started_at"],
153
+ last_event_at=row["last_event_at"],
154
+ )
155
+
156
+ def get_team_rollup(self, team: str, period: str = "monthly") -> "TeamRollup":
157
+ since = _period_start(period)
158
+ with self._lock:
159
+ conn = self._get_conn()
160
+ agg = conn.execute(
161
+ """SELECT SUM(cost_usd) as total_cost,
162
+ SUM(prompt_tokens + completion_tokens) as total_tokens,
163
+ COUNT(DISTINCT session_id) as total_sessions
164
+ FROM usage_events WHERE team=? AND timestamp>=?""",
165
+ (team, since),
166
+ ).fetchone()
167
+ top = conn.execute(
168
+ """SELECT actor_id, SUM(cost_usd) as cost_usd,
169
+ COUNT(DISTINCT session_id) as sessions
170
+ FROM usage_events WHERE team=? AND timestamp>=?
171
+ GROUP BY actor_id ORDER BY cost_usd DESC LIMIT 10""",
172
+ (team, since),
173
+ ).fetchall()
174
+ return TeamRollup(
175
+ team=team,
176
+ period=period,
177
+ total_cost_usd=agg["total_cost"] or 0.0,
178
+ total_tokens=agg["total_tokens"] or 0,
179
+ total_sessions=agg["total_sessions"] or 0,
180
+ top_users=[
181
+ {"actor_id": r["actor_id"], "cost_usd": r["cost_usd"], "sessions": r["sessions"]}
182
+ for r in top
183
+ ],
184
+ )
185
+
186
+ def check_budget(self, team: str) -> dict:
187
+ """Returns {over_budget, alert, spent_usd, limit_usd}."""
188
+ since = _period_start("monthly")
189
+ with self._lock:
190
+ budget_row = self._get_conn().execute(
191
+ "SELECT monthly_limit_usd, alert_threshold FROM cost_budgets WHERE team=?",
192
+ (team,),
193
+ ).fetchone()
194
+ if budget_row is None:
195
+ return {"over_budget": False, "alert": False, "spent_usd": 0.0, "limit_usd": None}
196
+ spent = (
197
+ self._get_conn().execute(
198
+ "SELECT SUM(cost_usd) as total FROM usage_events WHERE team=? AND timestamp>=?",
199
+ (team, since),
200
+ ).fetchone()["total"]
201
+ or 0.0
202
+ )
203
+ limit = budget_row["monthly_limit_usd"]
204
+ threshold = budget_row["alert_threshold"]
205
+ return {
206
+ "over_budget": spent >= limit,
207
+ "alert": spent >= limit * threshold,
208
+ "spent_usd": spent,
209
+ "limit_usd": limit,
210
+ }
211
+
212
+ def set_budget(
213
+ self, team: str, monthly_limit_usd: float, alert_threshold: float = 0.8
214
+ ) -> None:
215
+ with self._lock:
216
+ self._get_conn().execute(
217
+ """INSERT OR REPLACE INTO cost_budgets (team, monthly_limit_usd, alert_threshold)
218
+ VALUES (?,?,?)""",
219
+ (team, monthly_limit_usd, alert_threshold),
220
+ )
221
+ self._get_conn().commit()
222
+
223
+ def export_csv(
224
+ self, output_path: "Path", team: str = None, since: str = None
225
+ ) -> int:
226
+ clauses, params = [], []
227
+ if team:
228
+ clauses.append("team=?")
229
+ params.append(team)
230
+ if since:
231
+ clauses.append("timestamp>=?")
232
+ params.append(since)
233
+ where = ("WHERE " + " AND ".join(clauses)) if clauses else ""
234
+ with self._lock:
235
+ rows = self._get_conn().execute(
236
+ f"SELECT * FROM usage_events {where} ORDER BY timestamp", params
237
+ ).fetchall()
238
+ if not rows:
239
+ return 0
240
+ with open(output_path, "w", newline="") as f:
241
+ writer = csv.DictWriter(f, fieldnames=rows[0].keys())
242
+ writer.writeheader()
243
+ writer.writerows([dict(r) for r in rows])
244
+ return len(rows)
245
+
246
+ def close(self) -> None:
247
+ if self._conn:
248
+ self._conn.close()
249
+ self._conn = None
250
+
251
+
252
+ def _period_start(period: str) -> str:
253
+ import datetime as _dt
254
+ now = datetime.now(timezone.utc)
255
+ if period == "daily":
256
+ return now.replace(hour=0, minute=0, second=0, microsecond=0).isoformat()
257
+ if period == "weekly":
258
+ monday = now - _dt.timedelta(days=now.weekday())
259
+ return monday.replace(hour=0, minute=0, second=0, microsecond=0).isoformat()
260
+ # monthly
261
+ return now.replace(day=1, hour=0, minute=0, second=0, microsecond=0).isoformat()
src/exceptions.py ADDED
@@ -0,0 +1,207 @@
1
+ """Custom exception hierarchy for gdm code.
2
+
3
+ All gdm exceptions inherit from GdmError so callers can catch the entire
4
+ family with a single `except GdmError` when needed.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ __all__ = [
9
+ "GdmError",
10
+ "ConfigError",
11
+ "ApiError",
12
+ "ApiRateLimitError",
13
+ "ApiBudgetError",
14
+ "FatalApiError",
15
+ "ToolError",
16
+ "ToolPermissionError",
17
+ "SecurityError",
18
+ "InjectionDetectedError",
19
+ "GitError",
20
+ "BudgetError",
21
+ "DatabaseError",
22
+ "SchemaError",
23
+ "IndexError",
24
+ "AgentLoopError",
25
+ "PermissionDeniedError",
26
+ "BudgetExceededError",
27
+ "DirtyWorkingTreeError",
28
+ "MergeConflictError",
29
+ "DaemonUnavailableError",
30
+ ]
31
+
32
+
33
+ class GdmError(Exception):
34
+ """Base class for all gdm errors."""
35
+
36
+
37
+ # ---------------------------------------------------------------------------
38
+ # Configuration
39
+ # ---------------------------------------------------------------------------
40
+
41
+ class ConfigError(GdmError):
42
+ """Invalid or missing configuration (e.g. XAI_API_KEY not set)."""
43
+
44
+
45
+ # ---------------------------------------------------------------------------
46
+ # API / model
47
+ # ---------------------------------------------------------------------------
48
+
49
+ class ApiError(GdmError):
50
+ """xAI API call failed."""
51
+
52
+ def __init__(self, message: str, status_code: int | None = None) -> None:
53
+ super().__init__(message)
54
+ self.status_code = status_code
55
+
56
+
57
+ class FatalApiError(ApiError):
58
+ """Both primary and fallback providers failed — no recovery possible."""
59
+
60
+
61
+ class ApiRateLimitError(ApiError):
62
+ """429 rate limit hit — caller should back off and retry."""
63
+
64
+
65
+ class ApiBudgetError(ApiError):
66
+ """Session cost cap reached — stop calling the API."""
67
+
68
+
69
+ # ---------------------------------------------------------------------------
70
+ # Tools
71
+ # ---------------------------------------------------------------------------
72
+
73
+ class ToolError(GdmError):
74
+ """A tool execution failed."""
75
+
76
+ def __init__(self, tool_name: str, message: str) -> None:
77
+ super().__init__(f"[{tool_name}] {message}")
78
+ self.tool_name = tool_name
79
+
80
+
81
+ class ToolPermissionError(ToolError):
82
+ """Tool was denied by ToolPermissionContext."""
83
+
84
+ def __init__(self, tool: str, reason: str) -> None:
85
+ super().__init__(tool_name=tool, message=reason)
86
+ self.tool = tool
87
+ self.reason = reason
88
+
89
+
90
+ # ---------------------------------------------------------------------------
91
+ # Security
92
+ # ---------------------------------------------------------------------------
93
+
94
+ class SecurityError(GdmError):
95
+ """A security invariant was violated."""
96
+
97
+
98
+ class InjectionDetectedError(SecurityError):
99
+ """Prompt injection pattern found in file content."""
100
+
101
+ def __init__(self, filename: str, pattern: str) -> None:
102
+ super().__init__(
103
+ f"Injection pattern '{pattern}' detected in '{filename}'. "
104
+ "Content blocked from prompt injection."
105
+ )
106
+ self.filename = filename
107
+ self.pattern = pattern
108
+
109
+
110
+ # ---------------------------------------------------------------------------
111
+ # Git
112
+ # ---------------------------------------------------------------------------
113
+
114
+ class GitError(GdmError):
115
+ """Git operation failed."""
116
+
117
+ def __init__(self, command: str, message: str) -> None:
118
+ super().__init__(f"git {command}: {message}")
119
+ self.command = command
120
+
121
+
122
+ # ---------------------------------------------------------------------------
123
+ # Cost / budget
124
+ # ---------------------------------------------------------------------------
125
+
126
+ class BudgetError(GdmError):
127
+ """Session cost limit exceeded."""
128
+
129
+ def __init__(self, limit_usd: float, current_usd: float) -> None:
130
+ super().__init__(
131
+ f"Cost limit ${limit_usd:.2f} exceeded (current: ${current_usd:.2f})"
132
+ )
133
+ self.limit_usd = limit_usd
134
+ self.current_usd = current_usd
135
+
136
+
137
+ # ---------------------------------------------------------------------------
138
+ # Storage
139
+ # ---------------------------------------------------------------------------
140
+
141
+ class DatabaseError(GdmError):
142
+ """SQLite operation failed."""
143
+
144
+
145
+ class SchemaError(GdmError):
146
+ """Pydantic model parse/validation failed."""
147
+
148
+
149
+ class IndexError(GdmError):
150
+ """Code index (Tree-sitter/FAISS) operation failed."""
151
+
152
+
153
+ # ---------------------------------------------------------------------------
154
+ # Agent loop
155
+ # ---------------------------------------------------------------------------
156
+
157
+ class AgentLoopError(GdmError):
158
+ """Agent loop hit an unrecoverable state (max turns, bad API response, etc.)."""
159
+
160
+
161
+ # ---------------------------------------------------------------------------
162
+ # Aliases (backwards-compatible names)
163
+ # ---------------------------------------------------------------------------
164
+
165
+ class PermissionDeniedError(ToolPermissionError):
166
+ """Alias for ToolPermissionError — backwards-compatible name."""
167
+
168
+
169
+ class BudgetExceededError(ApiBudgetError):
170
+ """Alias for ApiBudgetError — raised by cost_tracker when limit exceeded."""
171
+
172
+
173
+ # ---------------------------------------------------------------------------
174
+ # Branch Farm
175
+ # ---------------------------------------------------------------------------
176
+
177
+ class DirtyWorkingTreeError(GitError):
178
+ """Branch farm requires a clean working tree but uncommitted changes exist."""
179
+
180
+ def __init__(self) -> None:
181
+ super().__init__(
182
+ command="status --porcelain",
183
+ message=(
184
+ "Branch farm requires a clean working tree. "
185
+ "Commit or stash your changes before running a branch farm."
186
+ ),
187
+ )
188
+
189
+
190
+ class MergeConflictError(GitError):
191
+ """Dry-run merge detected conflicts; cannot auto-merge the farm branch."""
192
+
193
+ def __init__(self, branch: str, details: str = "") -> None:
194
+ super().__init__(
195
+ command=f"merge-tree HEAD {branch}",
196
+ message=f"Conflicts detected when merging {branch!r}.{' ' + details if details else ''}",
197
+ )
198
+ self.branch = branch
199
+
200
+
201
+ # ---------------------------------------------------------------------------
202
+ # Daemon
203
+ # ---------------------------------------------------------------------------
204
+
205
+ class DaemonUnavailableError(GdmError):
206
+ """Daemon crashed too frequently or is otherwise unavailable."""
207
+