argus-code 0.2.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 (86) hide show
  1. argus/__init__.py +3 -0
  2. argus/adapters/__init__.py +7 -0
  3. argus/adapters/base.py +108 -0
  4. argus/adapters/claude_code/__init__.py +5 -0
  5. argus/adapters/claude_code/adapter.py +63 -0
  6. argus/adapters/claude_code/discover.py +72 -0
  7. argus/adapters/claude_code/extract_tool_calls.py +86 -0
  8. argus/adapters/claude_code/extract_transcript.py +111 -0
  9. argus/adapters/claude_code/extract_turns.py +69 -0
  10. argus/adapters/claude_code/history_jsonl.py +138 -0
  11. argus/adapters/claude_code/ingest_file.py +137 -0
  12. argus/adapters/claude_code/model.py +11 -0
  13. argus/adapters/claude_code/schemas.py +77 -0
  14. argus/adapters/registry.py +30 -0
  15. argus/cli.py +384 -0
  16. argus/collector/__init__.py +0 -0
  17. argus/collector/aggregate.py +102 -0
  18. argus/collector/first_run.py +189 -0
  19. argus/collector/pipeline.py +140 -0
  20. argus/collector/rollup_subagents.py +27 -0
  21. argus/collector/scheduler.py +89 -0
  22. argus/collector/search_backfill.py +109 -0
  23. argus/collector/watcher.py +178 -0
  24. argus/dashboard-dist/_astro/charts.BIevw6Es.js +1 -0
  25. argus/dashboard-dist/_astro/format.DxC1NGYT.js +1 -0
  26. argus/dashboard-dist/_astro/index.astro_astro_type_script_index_0_lang.CgwSARdD.js +24 -0
  27. argus/dashboard-dist/_astro/index.astro_astro_type_script_index_0_lang.W18SJsr7.js +11 -0
  28. argus/dashboard-dist/_astro/installCanvasRenderer.D_tC6TXz.js +18 -0
  29. argus/dashboard-dist/_astro/models.astro_astro_type_script_index_0_lang.BHTHXYHC.js +13 -0
  30. argus/dashboard-dist/_astro/prompts.astro_astro_type_script_index_0_lang.DfNgiDv9.js +17 -0
  31. argus/dashboard-dist/_astro/session.astro_astro_type_script_index_0_lang.Dj_bfrIa.js +86 -0
  32. argus/dashboard-dist/_astro/settings.astro_astro_type_script_index_0_lang.d_a-uvdi.js +24 -0
  33. argus/dashboard-dist/_astro/tools.astro_astro_type_script_index_0_lang.Dzzau3Yt.js +12 -0
  34. argus/dashboard-dist/_astro/trends.astro_astro_type_script_index_0_lang.BLLeGRNa.js +5 -0
  35. argus/dashboard-dist/index.html +2 -0
  36. argus/dashboard-dist/models/index.html +1 -0
  37. argus/dashboard-dist/prompts/index.html +18 -0
  38. argus/dashboard-dist/session/index.html +2 -0
  39. argus/dashboard-dist/sessions/index.html +1 -0
  40. argus/dashboard-dist/settings/index.html +8 -0
  41. argus/dashboard-dist/styles/global.css +307 -0
  42. argus/dashboard-dist/tools/index.html +1 -0
  43. argus/dashboard-dist/trends/index.html +1 -0
  44. argus/detectors/__init__.py +6 -0
  45. argus/detectors/base.py +34 -0
  46. argus/detectors/registry.py +20 -0
  47. argus/detectors/tool_error_rate_spike.py +138 -0
  48. argus/pricing/2026-05-02.json +24 -0
  49. argus/pricing/__init__.py +0 -0
  50. argus/pricing/compute.py +46 -0
  51. argus/pricing/load.py +45 -0
  52. argus/pricing/refresh.py +91 -0
  53. argus/pricing/types.py +21 -0
  54. argus/scaffold/__init__.py +0 -0
  55. argus/scaffold/scaffolder.py +45 -0
  56. argus/scaffold/snapshot.py +73 -0
  57. argus/scaffold/storage.py +60 -0
  58. argus/schema/__init__.py +0 -0
  59. argus/schema/types.py +157 -0
  60. argus/server/__init__.py +0 -0
  61. argus/server/api.py +661 -0
  62. argus/server/app.py +97 -0
  63. argus/store/__init__.py +0 -0
  64. argus/store/db.py +103 -0
  65. argus/store/migrations/__init__.py +0 -0
  66. argus/store/migrations/inline.py +180 -0
  67. argus/store/repository.py +778 -0
  68. argus/templates/default/.claude/agents/code-reviewer.md +27 -0
  69. argus/templates/default/.claude/agents/security-auditor.md +28 -0
  70. argus/templates/default/.claude/commands/commit.md +38 -0
  71. argus/templates/default/.claude/commands/deploy.md +13 -0
  72. argus/templates/default/.claude/commands/fix-issue.md +15 -0
  73. argus/templates/default/.claude/commands/pr.md +38 -0
  74. argus/templates/default/.claude/commands/review.md +14 -0
  75. argus/templates/default/.claude/rules/api-conventions.md +27 -0
  76. argus/templates/default/.claude/rules/code-style.md +25 -0
  77. argus/templates/default/.claude/rules/testing.md +19 -0
  78. argus/templates/default/.claude/settings.json +28 -0
  79. argus/templates/default/.claude/skills/example/SKILL.md +11 -0
  80. argus/templates/default/CLAUDE.md +57 -0
  81. argus_code-0.2.0.dist-info/METADATA +247 -0
  82. argus_code-0.2.0.dist-info/RECORD +86 -0
  83. argus_code-0.2.0.dist-info/WHEEL +4 -0
  84. argus_code-0.2.0.dist-info/entry_points.txt +2 -0
  85. argus_code-0.2.0.dist-info/licenses/LICENSE +21 -0
  86. argus_code-0.2.0.dist-info/licenses/NOTICE +22 -0
argus/server/app.py ADDED
@@ -0,0 +1,97 @@
1
+ """FastAPI app builder + uvicorn launcher."""
2
+ from __future__ import annotations
3
+
4
+ import asyncio
5
+ import logging
6
+ from dataclasses import dataclass
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ import uvicorn
11
+ from fastapi import FastAPI, Request
12
+ from fastapi.exceptions import RequestValidationError
13
+ from fastapi.responses import JSONResponse
14
+ from fastapi.staticfiles import StaticFiles
15
+
16
+ from ..adapters.base import Adapter
17
+ from ..collector.first_run import FirstRunHandle
18
+ from ..pricing.types import PricingTable
19
+ from ..store.repository import Repository
20
+ from .api import ApiDeps, build_api, is_allowed_origin
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+
25
+ @dataclass
26
+ class ServerOpts:
27
+ pricing_table_version: str
28
+ ingest_status: Any # callable returning IngestStatus
29
+ dashboard_dir: Path
30
+ port: int
31
+ adapters: list[Adapter]
32
+ pricing_table: PricingTable
33
+ host: str = "127.0.0.1"
34
+
35
+
36
+ def build_app(repo: Repository, opts: ServerOpts) -> FastAPI:
37
+ """Build the FastAPI app with CSRF middleware, API routes, static mount."""
38
+ app = FastAPI(title="Argus", docs_url=None, redoc_url=None, openapi_url=None)
39
+
40
+ @app.exception_handler(RequestValidationError)
41
+ async def on_validation_error(request: Request, exc: RequestValidationError):
42
+ return JSONResponse(
43
+ {"error": "bad request", "detail": exc.errors()}, status_code=400
44
+ )
45
+
46
+ # CSRF origin guard for state-changing requests. GETs are unrestricted
47
+ # because the dashboard issues same-origin GETs without an Origin
48
+ # header in some setups; POSTs from browsers always carry Origin.
49
+ @app.middleware("http")
50
+ async def csrf_origin_check(request: Request, call_next):
51
+ method = request.method
52
+ if method in ("GET", "HEAD", "OPTIONS"):
53
+ return await call_next(request)
54
+ # Allow API-prefixed paths to enforce the check; non-API endpoints
55
+ # don't exist on this server today, but apply uniformly.
56
+ if not is_allowed_origin(request.headers.get("origin")):
57
+ return JSONResponse(
58
+ {"error": "cross-origin requests not allowed"}, status_code=403
59
+ )
60
+ return await call_next(request)
61
+
62
+ api = build_api(
63
+ repo,
64
+ ApiDeps(
65
+ pricing_table_version=opts.pricing_table_version,
66
+ ingest_status=opts.ingest_status,
67
+ adapters=opts.adapters,
68
+ pricing_table=opts.pricing_table,
69
+ ),
70
+ )
71
+ app.include_router(api)
72
+
73
+ # Static dashboard mount LAST so /api/* routes win.
74
+ if opts.dashboard_dir.exists():
75
+ app.mount(
76
+ "/",
77
+ StaticFiles(directory=str(opts.dashboard_dir), html=True),
78
+ name="dashboard",
79
+ )
80
+
81
+ return app
82
+
83
+
84
+ def serve_blocking(app: FastAPI, *, host: str, port: int) -> None:
85
+ """Run uvicorn in the current thread until SIGINT or .should_exit."""
86
+ config = uvicorn.Config(
87
+ app,
88
+ host=host,
89
+ port=port,
90
+ log_level="warning", # quiet access log; user opts in via --verbose
91
+ access_log=False,
92
+ )
93
+ server = uvicorn.Server(config)
94
+ try:
95
+ server.run()
96
+ except KeyboardInterrupt:
97
+ pass
File without changes
argus/store/db.py ADDED
@@ -0,0 +1,103 @@
1
+ """SQLite connection opener + migration runner.
2
+
3
+ Verifies FTS5 is compiled in at startup; failing fast with a clear error
4
+ is better than a confusing OperationalError deep in searchPrompts later.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import sqlite3
9
+ from pathlib import Path
10
+
11
+ from .migrations.inline import (
12
+ MIGRATION_001,
13
+ MIGRATION_002,
14
+ MIGRATION_003,
15
+ MIGRATION_004,
16
+ MIGRATION_005,
17
+ )
18
+
19
+ SCHEMA_VERSION = 5
20
+
21
+
22
+ class FTS5NotAvailableError(RuntimeError):
23
+ """Raised when the Python sqlite3 build lacks FTS5 support."""
24
+
25
+
26
+ def _assert_fts5(conn: sqlite3.Connection) -> None:
27
+ """Fail loudly if FTS5 isn't compiled into this Python's sqlite3.
28
+
29
+ CPython's standard distributions for Windows / macOS / Linux all ship
30
+ FTS5 since 3.11. Alpine minimal builds and some custom builds don't.
31
+ """
32
+ cur = conn.execute("PRAGMA compile_options")
33
+ opts = {row[0] for row in cur.fetchall()}
34
+ if "ENABLE_FTS5" not in opts:
35
+ raise FTS5NotAvailableError(
36
+ "Your Python's sqlite3 was built without FTS5 (ENABLE_FTS5). "
37
+ "Argus requires FTS5 for prompt and transcript search. "
38
+ "Use the official CPython distribution or build sqlite with FTS5."
39
+ )
40
+
41
+
42
+ def open_db(path: str | Path) -> sqlite3.Connection:
43
+ """Open the Argus SQLite DB at ``path``, applying migrations 1..N."""
44
+ p = Path(path)
45
+ p.parent.mkdir(parents=True, exist_ok=True)
46
+
47
+ # check_same_thread=False because the Repository may be accessed from
48
+ # the request thread, the watcher thread, the first-run worker, and
49
+ # the scheduler thread. SQLite itself is thread-safe in serialized
50
+ # mode; better-sqlite3 in TS makes the same assumption.
51
+ #
52
+ # cached_statements=0 disables Python sqlite3's per-Connection
53
+ # prepared-statement cache. With the cache enabled, two threads that
54
+ # call execute(SAME_SQL, ...) concurrently can both pull the cached
55
+ # sqlite3_stmt*, both call reset+bind on it, and one loses with
56
+ # SQLITE_MISUSE ("bad parameter or other API misuse"). The cost of
57
+ # disabling the cache is ~10µs of statement prep per query, which is
58
+ # well below the cost of the queries themselves. SQLite's own internal
59
+ # compilation cache (sqlite3_prepare_v2 fast path) still applies.
60
+ conn = sqlite3.connect(
61
+ str(p),
62
+ check_same_thread=False,
63
+ isolation_level=None,
64
+ cached_statements=0,
65
+ )
66
+ conn.row_factory = sqlite3.Row
67
+ conn.execute("PRAGMA journal_mode = WAL")
68
+ conn.execute("PRAGMA foreign_keys = ON")
69
+ conn.execute("PRAGMA busy_timeout = 5000")
70
+
71
+ _assert_fts5(conn)
72
+
73
+ # MIGRATION_001 is fully idempotent (CREATE TABLE IF NOT EXISTS) so we
74
+ # always run it. After this point app_meta is guaranteed to exist.
75
+ conn.executescript(MIGRATION_001)
76
+
77
+ # Versioned migrations: ALTER TABLE has no IF NOT EXISTS in SQLite, so
78
+ # we gate later migrations on a schema_version row. Fresh DBs start at
79
+ # 1 (everything in MIGRATION_001 has been applied) and step up.
80
+ row = conn.execute(
81
+ "SELECT value FROM app_meta WHERE key = 'schema_version'"
82
+ ).fetchone()
83
+ current = int(row["value"]) if row else 1
84
+
85
+ if current < 2:
86
+ conn.executescript(MIGRATION_002)
87
+ current = 2
88
+ if current < 3:
89
+ conn.executescript(MIGRATION_003)
90
+ current = 3
91
+ if current < 4:
92
+ conn.executescript(MIGRATION_004)
93
+ current = 4
94
+ if current < 5:
95
+ conn.executescript(MIGRATION_005)
96
+ current = 5
97
+
98
+ conn.execute(
99
+ "INSERT OR REPLACE INTO app_meta (key, value) VALUES ('schema_version', ?)",
100
+ (str(SCHEMA_VERSION),),
101
+ )
102
+
103
+ return conn
File without changes
@@ -0,0 +1,180 @@
1
+ """Schema migrations.
2
+
3
+ Append-only. Never edit a published migration — production users have data
4
+ that depends on the exact SQL that already ran. To change the schema, add
5
+ a new MIGRATION_N+1 and bump SCHEMA_VERSION in db.py.
6
+ """
7
+
8
+ MIGRATION_001 = """
9
+ CREATE TABLE IF NOT EXISTS sessions (
10
+ id TEXT PRIMARY KEY,
11
+ agent TEXT NOT NULL,
12
+ agent_version TEXT,
13
+ project_path TEXT NOT NULL,
14
+ started_at TEXT NOT NULL,
15
+ ended_at TEXT,
16
+ duration_sec INTEGER,
17
+ total_fresh_input_tokens INTEGER NOT NULL DEFAULT 0,
18
+ total_output_tokens INTEGER NOT NULL DEFAULT 0,
19
+ total_cache_read_tokens INTEGER NOT NULL DEFAULT 0,
20
+ total_cache_write_tokens INTEGER NOT NULL DEFAULT 0,
21
+ total_cost_usd REAL NOT NULL DEFAULT 0,
22
+ primary_model TEXT NOT NULL,
23
+ turn_count INTEGER NOT NULL DEFAULT 0,
24
+ pricing_table_version TEXT NOT NULL,
25
+ computed_at TEXT NOT NULL,
26
+ agent_reported_cost_usd REAL,
27
+ metadata TEXT NOT NULL DEFAULT '{}'
28
+ );
29
+
30
+ CREATE INDEX IF NOT EXISTS idx_sessions_started_at ON sessions(started_at DESC);
31
+ CREATE INDEX IF NOT EXISTS idx_sessions_agent ON sessions(agent);
32
+
33
+ CREATE TABLE IF NOT EXISTS turns (
34
+ id TEXT PRIMARY KEY,
35
+ session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
36
+ sequence INTEGER NOT NULL,
37
+ timestamp TEXT NOT NULL,
38
+ model TEXT NOT NULL,
39
+ model_raw TEXT NOT NULL,
40
+ fresh_input_tokens INTEGER NOT NULL DEFAULT 0,
41
+ output_tokens INTEGER NOT NULL DEFAULT 0,
42
+ cache_read_tokens INTEGER NOT NULL DEFAULT 0,
43
+ cache_write_tokens INTEGER NOT NULL DEFAULT 0,
44
+ cache_write_5m_tokens INTEGER,
45
+ cache_write_1h_tokens INTEGER,
46
+ tool_calls_count INTEGER NOT NULL DEFAULT 0,
47
+ cost_usd REAL NOT NULL DEFAULT 0,
48
+ metadata TEXT NOT NULL DEFAULT '{}'
49
+ );
50
+
51
+ CREATE INDEX IF NOT EXISTS idx_turns_session ON turns(session_id, sequence);
52
+
53
+ CREATE TABLE IF NOT EXISTS file_offsets (
54
+ path TEXT PRIMARY KEY,
55
+ byte_offset INTEGER NOT NULL,
56
+ last_seen TEXT NOT NULL
57
+ );
58
+
59
+ CREATE TABLE IF NOT EXISTS parse_errors (
60
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
61
+ file TEXT NOT NULL,
62
+ byte_offset INTEGER NOT NULL,
63
+ reason TEXT NOT NULL,
64
+ raw_line_truncated TEXT NOT NULL,
65
+ occurred_at TEXT NOT NULL
66
+ );
67
+
68
+ CREATE TABLE IF NOT EXISTS app_meta (
69
+ key TEXT PRIMARY KEY,
70
+ value TEXT NOT NULL
71
+ );
72
+ """
73
+
74
+ MIGRATION_002 = """
75
+ ALTER TABLE sessions ADD COLUMN started_at_ms INTEGER;
76
+ ALTER TABLE sessions ADD COLUMN ended_at_ms INTEGER;
77
+ CREATE INDEX IF NOT EXISTS idx_sessions_time ON sessions(project_path, started_at_ms, ended_at_ms);
78
+
79
+ UPDATE sessions
80
+ SET started_at_ms = CAST(strftime('%s', started_at) AS INTEGER) * 1000,
81
+ ended_at_ms = CASE WHEN ended_at IS NULL OR ended_at = '' THEN NULL
82
+ ELSE CAST(strftime('%s', ended_at) AS INTEGER) * 1000 END
83
+ WHERE started_at_ms IS NULL;
84
+
85
+ CREATE TABLE IF NOT EXISTS tool_calls (
86
+ id TEXT PRIMARY KEY,
87
+ session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
88
+ turn_index INTEGER NOT NULL,
89
+ tool_name TEXT NOT NULL,
90
+ is_error INTEGER NOT NULL DEFAULT 0,
91
+ input_size INTEGER NOT NULL DEFAULT 0,
92
+ subagent_type TEXT,
93
+ timestamp TEXT NOT NULL
94
+ );
95
+ CREATE INDEX IF NOT EXISTS idx_tool_calls_session ON tool_calls(session_id);
96
+ CREATE INDEX IF NOT EXISTS idx_tool_calls_name ON tool_calls(tool_name);
97
+ CREATE INDEX IF NOT EXISTS idx_tool_calls_ts ON tool_calls(timestamp);
98
+
99
+ CREATE TABLE IF NOT EXISTS prompts (
100
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
101
+ timestamp_ms INTEGER NOT NULL,
102
+ project_path TEXT NOT NULL,
103
+ display TEXT NOT NULL,
104
+ pasted_chars INTEGER NOT NULL DEFAULT 0,
105
+ is_slash INTEGER NOT NULL DEFAULT 0
106
+ );
107
+ CREATE INDEX IF NOT EXISTS idx_prompts_ts ON prompts(timestamp_ms);
108
+ CREATE INDEX IF NOT EXISTS idx_prompts_project ON prompts(project_path);
109
+
110
+ CREATE VIRTUAL TABLE IF NOT EXISTS prompts_fts USING fts5(
111
+ display,
112
+ content='prompts',
113
+ content_rowid='id',
114
+ tokenize='unicode61'
115
+ );
116
+
117
+ CREATE TRIGGER IF NOT EXISTS prompts_ai AFTER INSERT ON prompts BEGIN
118
+ INSERT INTO prompts_fts(rowid, display) VALUES (new.id, new.display);
119
+ END;
120
+ CREATE TRIGGER IF NOT EXISTS prompts_ad AFTER DELETE ON prompts BEGIN
121
+ INSERT INTO prompts_fts(prompts_fts, rowid, display) VALUES('delete', old.id, old.display);
122
+ END;
123
+ CREATE TRIGGER IF NOT EXISTS prompts_au AFTER UPDATE ON prompts BEGIN
124
+ INSERT INTO prompts_fts(prompts_fts, rowid, display) VALUES('delete', old.id, old.display);
125
+ INSERT INTO prompts_fts(rowid, display) VALUES (new.id, new.display);
126
+ END;
127
+ """
128
+
129
+ MIGRATION_003 = """
130
+ CREATE TABLE IF NOT EXISTS transcript_segments (
131
+ rowid INTEGER PRIMARY KEY AUTOINCREMENT,
132
+ uid TEXT UNIQUE NOT NULL,
133
+ session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
134
+ timestamp TEXT NOT NULL,
135
+ role TEXT NOT NULL,
136
+ text TEXT NOT NULL
137
+ );
138
+ CREATE INDEX IF NOT EXISTS idx_segments_session ON transcript_segments(session_id);
139
+ CREATE INDEX IF NOT EXISTS idx_segments_ts ON transcript_segments(timestamp);
140
+
141
+ CREATE VIRTUAL TABLE IF NOT EXISTS transcript_fts USING fts5(
142
+ text,
143
+ content='transcript_segments',
144
+ content_rowid='rowid',
145
+ tokenize='unicode61'
146
+ );
147
+
148
+ CREATE TRIGGER IF NOT EXISTS segments_ai AFTER INSERT ON transcript_segments BEGIN
149
+ INSERT INTO transcript_fts(rowid, text) VALUES (new.rowid, new.text);
150
+ END;
151
+ CREATE TRIGGER IF NOT EXISTS segments_ad AFTER DELETE ON transcript_segments BEGIN
152
+ INSERT INTO transcript_fts(transcript_fts, rowid, text) VALUES('delete', old.rowid, old.text);
153
+ END;
154
+ CREATE TRIGGER IF NOT EXISTS segments_au AFTER UPDATE ON transcript_segments BEGIN
155
+ INSERT INTO transcript_fts(transcript_fts, rowid, text) VALUES('delete', old.rowid, old.text);
156
+ INSERT INTO transcript_fts(rowid, text) VALUES (new.rowid, new.text);
157
+ END;
158
+ """
159
+
160
+ MIGRATION_004 = """
161
+ CREATE TABLE IF NOT EXISTS alerts (
162
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
163
+ detector TEXT NOT NULL,
164
+ dedup_key TEXT NOT NULL,
165
+ severity TEXT NOT NULL CHECK (severity IN ('info', 'warning', 'critical')),
166
+ title TEXT NOT NULL,
167
+ message TEXT NOT NULL,
168
+ metadata TEXT NOT NULL DEFAULT '{}',
169
+ first_seen_at TEXT NOT NULL,
170
+ last_seen_at TEXT NOT NULL,
171
+ seen_at TEXT,
172
+ UNIQUE (detector, dedup_key)
173
+ );
174
+ CREATE INDEX IF NOT EXISTS idx_alerts_unseen ON alerts(seen_at, severity);
175
+ CREATE INDEX IF NOT EXISTS idx_alerts_recent ON alerts(last_seen_at DESC);
176
+ """
177
+
178
+ MIGRATION_005 = """
179
+ ALTER TABLE alerts ADD COLUMN resolved_at TEXT;
180
+ """