@jeganwrites/claudash 1.0.0
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.
- package/CONTRIBUTING.md +35 -0
- package/LICENSE +21 -0
- package/README.md +261 -0
- package/analyzer.py +890 -0
- package/bin/claudash.js +121 -0
- package/claude_ai_tracker.py +358 -0
- package/cli.py +1034 -0
- package/config.py +100 -0
- package/db.py +1156 -0
- package/fix_tracker.py +539 -0
- package/insights.py +359 -0
- package/mcp_server.py +414 -0
- package/package.json +39 -0
- package/scanner.py +385 -0
- package/server.py +762 -0
- package/templates/accounts.html +936 -0
- package/templates/dashboard.html +1742 -0
- package/tools/get-derived-keys.py +112 -0
- package/tools/mac-sync.py +386 -0
- package/tools/oauth_sync.py +308 -0
- package/tools/setup-pm2.sh +53 -0
- package/waste_patterns.py +334 -0
package/bin/claudash.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { execSync, spawn } = require('child_process');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
|
|
8
|
+
const VERSION = '1.0.0';
|
|
9
|
+
const REPO = 'https://github.com/pnjegan/claudash';
|
|
10
|
+
const INSTALL_DIR = path.join(os.homedir(), '.claudash');
|
|
11
|
+
|
|
12
|
+
function checkPython() {
|
|
13
|
+
try {
|
|
14
|
+
const ver = execSync('python3 --version 2>&1').toString().trim();
|
|
15
|
+
const match = ver.match(/(\d+)\.(\d+)/);
|
|
16
|
+
if (match && parseInt(match[1]) >= 3 && parseInt(match[2]) >= 8) {
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
console.error('Python 3.8+ required. Found: ' + ver);
|
|
20
|
+
process.exit(1);
|
|
21
|
+
} catch (e) {
|
|
22
|
+
console.error('Python 3 not found. Install from https://python.org');
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function checkClaudeData() {
|
|
28
|
+
const candidates = [
|
|
29
|
+
path.join(os.homedir(), '.claude', 'projects'),
|
|
30
|
+
path.join(os.homedir(), 'AppData', 'Roaming', 'Claude', 'projects'),
|
|
31
|
+
path.join(os.homedir(), 'Library', 'Application Support', 'Claude', 'projects'),
|
|
32
|
+
];
|
|
33
|
+
const found = candidates.filter(p => fs.existsSync(p));
|
|
34
|
+
if (found.length === 0) {
|
|
35
|
+
console.log('Warning: No Claude Code data found.');
|
|
36
|
+
console.log(' Run at least one Claude Code session first.');
|
|
37
|
+
console.log(' Looked in:');
|
|
38
|
+
candidates.forEach(c => console.log(' ' + c));
|
|
39
|
+
console.log(' Starting dashboard anyway — it will show instructions.');
|
|
40
|
+
} else {
|
|
41
|
+
console.log('Found Claude Code data at: ' + found[0]);
|
|
42
|
+
}
|
|
43
|
+
return found;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function installClaudash() {
|
|
47
|
+
if (fs.existsSync(path.join(INSTALL_DIR, 'cli.py'))) {
|
|
48
|
+
try {
|
|
49
|
+
execSync('git -C "' + INSTALL_DIR + '" pull --quiet 2>/dev/null');
|
|
50
|
+
console.log('Claudash updated');
|
|
51
|
+
} catch (e) {
|
|
52
|
+
// offline or not a git repo — use what we have
|
|
53
|
+
}
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
console.log('Installing Claudash to ' + INSTALL_DIR + '...');
|
|
58
|
+
try {
|
|
59
|
+
execSync('git clone --depth=1 --quiet "' + REPO + '" "' + INSTALL_DIR + '"');
|
|
60
|
+
console.log('Claudash installed');
|
|
61
|
+
} catch (e) {
|
|
62
|
+
console.error('Failed to clone from GitHub: ' + e.message);
|
|
63
|
+
console.error('Check your internet connection or visit: ' + REPO);
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function openBrowser(port) {
|
|
69
|
+
const url = 'http://localhost:' + port;
|
|
70
|
+
const platform = process.platform;
|
|
71
|
+
setTimeout(() => {
|
|
72
|
+
try {
|
|
73
|
+
if (platform === 'darwin') execSync('open "' + url + '"');
|
|
74
|
+
else if (platform === 'win32') execSync('start "" "' + url + '"');
|
|
75
|
+
else execSync('xdg-open "' + url + '" 2>/dev/null || true');
|
|
76
|
+
} catch (e) { /* headless — no browser */ }
|
|
77
|
+
}, 1500);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function main() {
|
|
81
|
+
const args = process.argv.slice(2);
|
|
82
|
+
|
|
83
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
84
|
+
console.log('Claudash v' + VERSION + ' — Claude Code usage intelligence');
|
|
85
|
+
console.log('');
|
|
86
|
+
console.log('Usage: npx claudash [options]');
|
|
87
|
+
console.log('');
|
|
88
|
+
console.log('Options:');
|
|
89
|
+
console.log(' --port=N Dashboard port (default: 8080)');
|
|
90
|
+
console.log(' --no-browser Skip auto-opening browser');
|
|
91
|
+
console.log(' --help Show this help');
|
|
92
|
+
process.exit(0);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const portArg = args.find(a => a.startsWith('--port='));
|
|
96
|
+
const port = portArg ? portArg.split('=')[1] : '8080';
|
|
97
|
+
const noBrowser = args.includes('--no-browser');
|
|
98
|
+
|
|
99
|
+
console.log('Claudash v' + VERSION);
|
|
100
|
+
console.log('-'.repeat(40));
|
|
101
|
+
|
|
102
|
+
checkPython();
|
|
103
|
+
checkClaudeData();
|
|
104
|
+
installClaudash();
|
|
105
|
+
|
|
106
|
+
console.log('Starting dashboard on http://localhost:' + port + ' ...');
|
|
107
|
+
if (!noBrowser) {
|
|
108
|
+
openBrowser(port);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const cliArgs = [path.join(INSTALL_DIR, 'cli.py'), 'dashboard', '--port', port, '--no-browser'];
|
|
112
|
+
const proc = spawn('python3', cliArgs, {
|
|
113
|
+
stdio: 'inherit',
|
|
114
|
+
cwd: INSTALL_DIR,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
proc.on('exit', code => process.exit(code || 0));
|
|
118
|
+
process.on('SIGINT', () => { proc.kill(); process.exit(0); });
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
main();
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
"""Poll claude.ai browser usage API for configured accounts.
|
|
2
|
+
Session keys stored in SQLite only — never logged or written to files."""
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import sys
|
|
6
|
+
import time
|
|
7
|
+
import ssl
|
|
8
|
+
import threading
|
|
9
|
+
from datetime import datetime, timezone
|
|
10
|
+
from urllib.request import Request, urlopen
|
|
11
|
+
from urllib.error import HTTPError, URLError
|
|
12
|
+
|
|
13
|
+
from db import (
|
|
14
|
+
get_conn, get_claude_ai_accounts_all, get_claude_ai_account,
|
|
15
|
+
update_claude_ai_account_status, insert_claude_ai_snapshot,
|
|
16
|
+
get_latest_claude_ai_snapshot, upsert_claude_ai_account,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
_last_poll_time = 0
|
|
20
|
+
_account_statuses = {}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def get_last_poll_time():
|
|
24
|
+
return _last_poll_time
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def get_account_statuses():
|
|
28
|
+
return dict(_account_statuses)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _ssl_ctx():
|
|
32
|
+
return ssl.create_default_context()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _parse_iso(ts_str):
|
|
36
|
+
"""Parse ISO 8601 timestamp to epoch int."""
|
|
37
|
+
if not ts_str:
|
|
38
|
+
return 0
|
|
39
|
+
try:
|
|
40
|
+
clean = ts_str.replace("Z", "").replace("+00:00", "")
|
|
41
|
+
if "." in clean:
|
|
42
|
+
clean = clean.split(".")[0]
|
|
43
|
+
dt = datetime.strptime(clean, "%Y-%m-%dT%H:%M:%S")
|
|
44
|
+
return int(dt.replace(tzinfo=timezone.utc).timestamp())
|
|
45
|
+
except Exception:
|
|
46
|
+
return 0
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _safe_request(url, session_key, method="GET"):
|
|
50
|
+
"""Make an authenticated request to claude.ai. Returns (data_dict, None) or (None, error_str).
|
|
51
|
+
NEVER logs session_key."""
|
|
52
|
+
headers = {
|
|
53
|
+
"Accept": "application/json",
|
|
54
|
+
"Content-Type": "application/json",
|
|
55
|
+
"Cookie": f"sessionKey={session_key}",
|
|
56
|
+
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
|
|
57
|
+
}
|
|
58
|
+
req = Request(url, headers=headers, method=method)
|
|
59
|
+
try:
|
|
60
|
+
with urlopen(req, timeout=15, context=_ssl_ctx()) as resp:
|
|
61
|
+
body = resp.read().decode("utf-8")
|
|
62
|
+
return json.loads(body), None
|
|
63
|
+
except HTTPError as e:
|
|
64
|
+
if e.code in (401, 403):
|
|
65
|
+
return None, "expired"
|
|
66
|
+
return None, f"http_{e.code}"
|
|
67
|
+
except (URLError, OSError, json.JSONDecodeError, ValueError) as e:
|
|
68
|
+
return None, f"network_error"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# ── Public API ──
|
|
72
|
+
|
|
73
|
+
def fetch_org_id(session_key):
|
|
74
|
+
"""GET https://claude.ai/api/account → extract organizations[0].uuid.
|
|
75
|
+
Returns org_id string or None."""
|
|
76
|
+
data, err = _safe_request("https://claude.ai/api/account", session_key)
|
|
77
|
+
if err or not data:
|
|
78
|
+
return None
|
|
79
|
+
# Response shape: {"account_uuid": ..., "organizations": [{"uuid": "..."}]}
|
|
80
|
+
orgs = data.get("organizations") or data.get("memberships", [])
|
|
81
|
+
if isinstance(orgs, list) and orgs:
|
|
82
|
+
org = orgs[0]
|
|
83
|
+
return org.get("uuid") or org.get("organization", {}).get("uuid") or org.get("id")
|
|
84
|
+
# Fallback: top-level uuid
|
|
85
|
+
return data.get("uuid") or data.get("org_id")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def verify_session(session_key):
|
|
89
|
+
"""Verify a session key is valid. Returns {valid, org_id, error}."""
|
|
90
|
+
if not session_key or not session_key.strip():
|
|
91
|
+
return {"valid": False, "org_id": None, "error": "Session key is empty"}
|
|
92
|
+
|
|
93
|
+
org_id = fetch_org_id(session_key)
|
|
94
|
+
if org_id:
|
|
95
|
+
return {"valid": True, "org_id": org_id, "error": None}
|
|
96
|
+
|
|
97
|
+
# Try direct — maybe the API shape changed
|
|
98
|
+
data, err = _safe_request("https://claude.ai/api/account", session_key)
|
|
99
|
+
if err == "expired":
|
|
100
|
+
return {"valid": False, "org_id": None, "error": "Session key invalid or expired"}
|
|
101
|
+
if err:
|
|
102
|
+
return {"valid": False, "org_id": None, "error": f"Connection error: {err}"}
|
|
103
|
+
return {"valid": False, "org_id": None, "error": "Could not extract org_id from response"}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def fetch_usage(session_key, org_id, plan="max"):
|
|
107
|
+
"""Fetch usage for an account. Returns normalized dict or None.
|
|
108
|
+
For Max: token-based window. For Pro: message-based."""
|
|
109
|
+
if not session_key or not org_id:
|
|
110
|
+
return None
|
|
111
|
+
|
|
112
|
+
url = f"https://claude.ai/api/organizations/{org_id}/usage"
|
|
113
|
+
data, err = _safe_request(url, session_key)
|
|
114
|
+
|
|
115
|
+
if err == "expired":
|
|
116
|
+
return {"error": "expired"}
|
|
117
|
+
if err or not data:
|
|
118
|
+
return {"error": err or "unknown"}
|
|
119
|
+
|
|
120
|
+
raw = json.dumps(data)
|
|
121
|
+
|
|
122
|
+
tokens_used = 0
|
|
123
|
+
tokens_limit = 0
|
|
124
|
+
messages_used = 0
|
|
125
|
+
messages_limit = 0
|
|
126
|
+
window_start = 0
|
|
127
|
+
window_end = 0
|
|
128
|
+
|
|
129
|
+
# Parse reset_at → window_end
|
|
130
|
+
reset_at = data.get("reset_at") or data.get("expires_at") or data.get("window_end")
|
|
131
|
+
if isinstance(reset_at, str):
|
|
132
|
+
window_end = _parse_iso(reset_at)
|
|
133
|
+
elif isinstance(reset_at, (int, float)):
|
|
134
|
+
window_end = int(reset_at)
|
|
135
|
+
|
|
136
|
+
# window_start = window_end - 5 hours (standard Anthropic window)
|
|
137
|
+
if window_end > 0:
|
|
138
|
+
window_start = window_end - (5 * 3600)
|
|
139
|
+
|
|
140
|
+
# Try extracting message counts (Pro plan)
|
|
141
|
+
if "messageLimit" in data:
|
|
142
|
+
ml = data["messageLimit"]
|
|
143
|
+
remaining = ml.get("remaining", 0) or 0
|
|
144
|
+
limit = ml.get("limit", 0) or 0
|
|
145
|
+
messages_limit = limit
|
|
146
|
+
messages_used = limit - remaining if limit > 0 else 0
|
|
147
|
+
|
|
148
|
+
if "raw_message_count" in data:
|
|
149
|
+
messages_used = data["raw_message_count"]
|
|
150
|
+
if "message_limit" in data:
|
|
151
|
+
messages_limit = data["message_limit"]
|
|
152
|
+
|
|
153
|
+
# Token-based usage (Max plan)
|
|
154
|
+
if "usage" in data and isinstance(data["usage"], dict):
|
|
155
|
+
u = data["usage"]
|
|
156
|
+
tokens_used = u.get("tokens_used", 0) or u.get("used", 0) or 0
|
|
157
|
+
tokens_limit = u.get("tokens_limit", 0) or u.get("limit", 0) or 0
|
|
158
|
+
|
|
159
|
+
# Fallback: top-level token fields
|
|
160
|
+
if tokens_used == 0 and tokens_limit == 0:
|
|
161
|
+
for key in ("tokens_used", "used_tokens", "current_usage", "used"):
|
|
162
|
+
if key in data and data[key]:
|
|
163
|
+
tokens_used = data[key]
|
|
164
|
+
break
|
|
165
|
+
for key in ("tokens_limit", "token_limit", "limit", "max_tokens"):
|
|
166
|
+
if key in data and data[key]:
|
|
167
|
+
tokens_limit = data[key]
|
|
168
|
+
break
|
|
169
|
+
|
|
170
|
+
# Compute unified pct_used
|
|
171
|
+
if plan == "pro" and messages_limit > 0:
|
|
172
|
+
pct_used = round(messages_used / messages_limit * 100, 1)
|
|
173
|
+
elif tokens_limit > 0:
|
|
174
|
+
pct_used = round(tokens_used / tokens_limit * 100, 1)
|
|
175
|
+
else:
|
|
176
|
+
pct_used = 0
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
"tokens_used": tokens_used,
|
|
180
|
+
"tokens_limit": tokens_limit,
|
|
181
|
+
"messages_used": messages_used,
|
|
182
|
+
"messages_limit": messages_limit,
|
|
183
|
+
"window_start": window_start,
|
|
184
|
+
"window_end": window_end,
|
|
185
|
+
"pct_used": pct_used,
|
|
186
|
+
"plan": plan,
|
|
187
|
+
"raw": raw,
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def poll_single(account_id, conn=None):
|
|
192
|
+
"""Poll a single account. Returns snapshot dict or None."""
|
|
193
|
+
should_close = False
|
|
194
|
+
if conn is None:
|
|
195
|
+
conn = get_conn()
|
|
196
|
+
should_close = True
|
|
197
|
+
|
|
198
|
+
acct = get_claude_ai_account(conn, account_id)
|
|
199
|
+
if not acct:
|
|
200
|
+
if should_close:
|
|
201
|
+
conn.close()
|
|
202
|
+
return None
|
|
203
|
+
|
|
204
|
+
sk = acct.get("session_key", "").strip()
|
|
205
|
+
oid = acct.get("org_id", "").strip()
|
|
206
|
+
plan = acct.get("plan", "max")
|
|
207
|
+
label = acct.get("label", account_id)
|
|
208
|
+
|
|
209
|
+
# Skip mac-sync accounts — Mac pushes data, VPS doesn't poll
|
|
210
|
+
if acct.get("mac_sync_mode"):
|
|
211
|
+
print(f"[claude.ai] {label}: mac-sync mode, skipping poll", file=sys.stderr)
|
|
212
|
+
if should_close:
|
|
213
|
+
conn.close()
|
|
214
|
+
return None
|
|
215
|
+
|
|
216
|
+
if not sk or not oid:
|
|
217
|
+
_account_statuses[account_id] = {"status": "unconfigured", "label": label}
|
|
218
|
+
if should_close:
|
|
219
|
+
conn.close()
|
|
220
|
+
return None
|
|
221
|
+
|
|
222
|
+
result = fetch_usage(sk, oid, plan)
|
|
223
|
+
if not result:
|
|
224
|
+
update_claude_ai_account_status(conn, account_id, "error", "No response")
|
|
225
|
+
_account_statuses[account_id] = {"status": "error", "label": label, "error": "No response"}
|
|
226
|
+
if should_close:
|
|
227
|
+
conn.close()
|
|
228
|
+
return None
|
|
229
|
+
|
|
230
|
+
if "error" in result:
|
|
231
|
+
err = result["error"]
|
|
232
|
+
if err == "expired":
|
|
233
|
+
update_claude_ai_account_status(conn, account_id, "expired", "Session expired")
|
|
234
|
+
_account_statuses[account_id] = {"status": "expired", "label": label}
|
|
235
|
+
else:
|
|
236
|
+
update_claude_ai_account_status(conn, account_id, "error", err)
|
|
237
|
+
_account_statuses[account_id] = {"status": "error", "label": label, "error": err}
|
|
238
|
+
if should_close:
|
|
239
|
+
conn.close()
|
|
240
|
+
return None
|
|
241
|
+
|
|
242
|
+
# Success
|
|
243
|
+
insert_claude_ai_snapshot(conn, account_id, result)
|
|
244
|
+
update_claude_ai_account_status(conn, account_id, "active", None)
|
|
245
|
+
|
|
246
|
+
_account_statuses[account_id] = {
|
|
247
|
+
"status": "active",
|
|
248
|
+
"label": label,
|
|
249
|
+
"pct_used": result["pct_used"],
|
|
250
|
+
"tokens_used": result["tokens_used"],
|
|
251
|
+
"tokens_limit": result["tokens_limit"],
|
|
252
|
+
"messages_used": result["messages_used"],
|
|
253
|
+
"messages_limit": result["messages_limit"],
|
|
254
|
+
"plan": plan,
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if plan == "pro" and result["messages_limit"] > 0:
|
|
258
|
+
print(f"[claude.ai] {label}: {result['messages_used']}/{result['messages_limit']} messages used", file=sys.stderr)
|
|
259
|
+
else:
|
|
260
|
+
print(f"[claude.ai] {label}: {result['pct_used']}% used", file=sys.stderr)
|
|
261
|
+
|
|
262
|
+
if should_close:
|
|
263
|
+
conn.close()
|
|
264
|
+
return result
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def poll_all():
|
|
268
|
+
"""Poll all configured claude.ai accounts (status != unconfigured)."""
|
|
269
|
+
global _last_poll_time
|
|
270
|
+
conn = get_conn()
|
|
271
|
+
accounts = get_claude_ai_accounts_all(conn)
|
|
272
|
+
count = 0
|
|
273
|
+
|
|
274
|
+
for acct in accounts:
|
|
275
|
+
aid = acct["account_id"]
|
|
276
|
+
sk = (acct.get("session_key") or "").strip()
|
|
277
|
+
oid = (acct.get("org_id") or "").strip()
|
|
278
|
+
label = acct.get("label", aid)
|
|
279
|
+
|
|
280
|
+
# Skip mac-sync accounts — Mac pushes data, VPS doesn't poll
|
|
281
|
+
if acct.get("mac_sync_mode"):
|
|
282
|
+
print(f"[claude.ai] {label}: mac-sync mode, skipping poll", file=sys.stderr)
|
|
283
|
+
continue
|
|
284
|
+
|
|
285
|
+
if not sk or not oid:
|
|
286
|
+
_account_statuses[aid] = {"status": "unconfigured", "label": label}
|
|
287
|
+
continue
|
|
288
|
+
|
|
289
|
+
try:
|
|
290
|
+
result = poll_single(aid, conn)
|
|
291
|
+
if result and "error" not in result:
|
|
292
|
+
count += 1
|
|
293
|
+
except Exception as e:
|
|
294
|
+
print(f"[claude.ai] Error polling {aid}: {e}", file=sys.stderr)
|
|
295
|
+
_account_statuses[aid] = {"status": "error", "label": label, "error": str(e)}
|
|
296
|
+
|
|
297
|
+
conn.close()
|
|
298
|
+
_last_poll_time = int(time.time())
|
|
299
|
+
print(f"[claude.ai] Poll complete: {count}/{len(accounts)} accounts updated", file=sys.stderr)
|
|
300
|
+
return count
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def start_periodic_poll(interval_seconds=300):
|
|
304
|
+
def _run():
|
|
305
|
+
while True:
|
|
306
|
+
try:
|
|
307
|
+
poll_all()
|
|
308
|
+
except Exception as e:
|
|
309
|
+
print(f"[claude.ai] Periodic poll error: {e}", file=sys.stderr)
|
|
310
|
+
time.sleep(interval_seconds)
|
|
311
|
+
|
|
312
|
+
t = threading.Thread(target=_run, daemon=True)
|
|
313
|
+
t.start()
|
|
314
|
+
return t
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def setup_account(account_id, session_key):
|
|
318
|
+
"""Verify session key, extract org_id, store in DB, poll immediately.
|
|
319
|
+
Returns {success, error, org_id, plan, pct_used, label}."""
|
|
320
|
+
conn = get_conn()
|
|
321
|
+
|
|
322
|
+
# Get account info
|
|
323
|
+
acct_row = conn.execute("SELECT label, plan FROM accounts WHERE account_id = ?", (account_id,)).fetchone()
|
|
324
|
+
if not acct_row:
|
|
325
|
+
conn.close()
|
|
326
|
+
return {"success": False, "error": f"Account '{account_id}' not found"}
|
|
327
|
+
|
|
328
|
+
label = acct_row["label"]
|
|
329
|
+
plan = acct_row["plan"]
|
|
330
|
+
|
|
331
|
+
# Verify session
|
|
332
|
+
verification = verify_session(session_key)
|
|
333
|
+
if not verification["valid"]:
|
|
334
|
+
conn.close()
|
|
335
|
+
return {"success": False, "error": verification["error"]}
|
|
336
|
+
|
|
337
|
+
org_id = verification["org_id"]
|
|
338
|
+
|
|
339
|
+
# Store in DB
|
|
340
|
+
upsert_claude_ai_account(conn, account_id, label, org_id, session_key, plan, "active")
|
|
341
|
+
|
|
342
|
+
# Immediately poll
|
|
343
|
+
result = fetch_usage(session_key, org_id, plan)
|
|
344
|
+
pct_used = 0
|
|
345
|
+
if result and "error" not in result:
|
|
346
|
+
insert_claude_ai_snapshot(conn, account_id, result)
|
|
347
|
+
update_claude_ai_account_status(conn, account_id, "active", None)
|
|
348
|
+
pct_used = result.get("pct_used", 0)
|
|
349
|
+
|
|
350
|
+
conn.close()
|
|
351
|
+
|
|
352
|
+
return {
|
|
353
|
+
"success": True,
|
|
354
|
+
"org_id": org_id,
|
|
355
|
+
"plan": plan,
|
|
356
|
+
"pct_used": pct_used,
|
|
357
|
+
"label": label,
|
|
358
|
+
}
|