@optimuslabs/harness-map-staging 1.5.25 → 1.5.27

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.
@@ -0,0 +1,175 @@
1
+ import { closeSync, existsSync, fstatSync, openSync, readdirSync, readFileSync, readSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { homedir } from 'node:os';
4
+ import { readClaudeLoginPlan } from './claude_login_plan.js';
5
+ /**
6
+ * Every org Claude Desktop's signed-in account has used, so both tenants are recorded when one
7
+ * login belongs to a team org and a personal org and switches between them in Desktop.
8
+ *
9
+ * Desktop's "two accounts" in that case are one account (one account UUID, one email) in two
10
+ * orgs, and ~/.claude.json's `oauthAccount` only ever names the active org. Desktop keeps a session
11
+ * directory per org it has used, under the account:
12
+ * ~/Library/Application Support/Claude/claude-code-sessions/<accountUuid>/<orgUuid>/
13
+ * ~/Library/Application Support/Claude/local-agent-mode-sessions/<accountUuid>/<orgUuid>/
14
+ *
15
+ * The folders are only read when one account has ever used Desktop on this Mac. Switching between
16
+ * separate Desktop logins leaves folders (and token-log entries) pairing each account with the
17
+ * other's orgs, so with more than one account they cannot say which org is whose. Then only the org
18
+ * Desktop is signed into now is reported: the newest `[oauth-v2] … orgId=` line of Desktop's own
19
+ * main.log, which is the login in use and cannot belong to another account.
20
+ *
21
+ * Reads `lastKnownAccountUuid` from config.json and nothing else from it -- the same file holds
22
+ * Desktop's encrypted token caches, which are never touched or uploaded. The email, and the active
23
+ * org's name and plan, come from ~/.claude.json only when it is the same account (and, for the name
24
+ * and plan, the same org): Desktop's own files name neither. The plan prefers Claude Code's stored
25
+ * login over that cached profile (see claude_login_plan.ts). macOS only.
26
+ */
27
+ const FILE_TYPE = 'claude_desktop_orgs';
28
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
29
+ const SESSION_ROOTS = ['claude-code-sessions', 'local-agent-mode-sessions'];
30
+ /** Only the end of Desktop's main.log is read: the newest sign-in is all that matters. */
31
+ const DESKTOP_LOG_TAIL_BYTES = 256 * 1024;
32
+ const DESKTOP_ORG_LINE = /\[oauth-v2\].*?orgId=([0-9a-f-]{36})/i;
33
+ const DESKTOP_ACCOUNT = /cacheKey=acct:([0-9a-f-]{36})/i;
34
+ function readTail(path, bytes) {
35
+ let fd;
36
+ try {
37
+ fd = openSync(path, 'r');
38
+ const size = fstatSync(fd).size;
39
+ const length = Math.min(size, bytes);
40
+ const buffer = Buffer.alloc(length);
41
+ readSync(fd, buffer, 0, length, size - length);
42
+ return buffer.toString('utf8');
43
+ }
44
+ finally {
45
+ if (fd !== undefined)
46
+ closeSync(fd);
47
+ }
48
+ }
49
+ /**
50
+ * The org (and, when logged, account) Claude Desktop is signed into now, or null when unknown.
51
+ *
52
+ * Desktop keeps its own login, apart from Claude Code's ~/.claude.json, and logs every token lookup
53
+ * as `[oauth-v2] … orgId=<org>` (with `cacheKey=acct:<account>|…` on lookups): the newest such line
54
+ * is the org Desktop is using.
55
+ */
56
+ export function readDesktopActiveLogin(home = homedir()) {
57
+ const logPath = join(home, 'Library', 'Logs', 'Claude', 'main.log');
58
+ let tail = '';
59
+ try {
60
+ if (existsSync(logPath))
61
+ tail = readTail(logPath, DESKTOP_LOG_TAIL_BYTES);
62
+ }
63
+ catch {
64
+ return null;
65
+ }
66
+ const lines = tail.split('\n').filter((line) => DESKTOP_ORG_LINE.test(line));
67
+ const newest = lines.length ? lines[lines.length - 1] : '';
68
+ const org = newest.match(DESKTOP_ORG_LINE)?.[1]?.toLowerCase() ?? '';
69
+ if (!org)
70
+ return null;
71
+ return { org, account: newest.match(DESKTOP_ACCOUNT)?.[1]?.toLowerCase() ?? '' };
72
+ }
73
+ function claudeSupportDir(home) {
74
+ return join(home, 'Library', 'Application Support', 'Claude');
75
+ }
76
+ function readJson(path) {
77
+ try {
78
+ const parsed = JSON.parse(readFileSync(path, 'utf8'));
79
+ return parsed && typeof parsed === 'object' ? parsed : null;
80
+ }
81
+ catch {
82
+ return null;
83
+ }
84
+ }
85
+ function text(value) {
86
+ return typeof value === 'string' ? value.trim() : '';
87
+ }
88
+ /** The lowercased UUID-named directories in `dir`, or [] when it does not exist. */
89
+ function uuidDirs(dir) {
90
+ try {
91
+ return readdirSync(dir, { withFileTypes: true })
92
+ .filter((entry) => entry.isDirectory() && UUID.test(entry.name))
93
+ .map((entry) => entry.name.toLowerCase());
94
+ }
95
+ catch {
96
+ return [];
97
+ }
98
+ }
99
+ /** Every account with a session directory, in any session root. */
100
+ function accountsWithSessions(supportDir) {
101
+ return new Set(SESSION_ROOTS.flatMap((root) => uuidDirs(join(supportDir, root))));
102
+ }
103
+ /** The org UUIDs with a session directory under this account, in any session root. */
104
+ function orgsForAccount(supportDir, account) {
105
+ return [...new Set(SESSION_ROOTS.flatMap((root) => uuidDirs(join(supportDir, root, account))))].sort();
106
+ }
107
+ /** The orgs to report: every one with a single account on this Mac, else the active one. */
108
+ function orgsToReport(supportDir, account, active) {
109
+ const accounts = accountsWithSessions(supportDir);
110
+ const orgs = new Set(accounts.size === 1 && accounts.has(account) ? orgsForAccount(supportDir, account) : []);
111
+ if (active)
112
+ orgs.add(active.org);
113
+ return [...orgs].sort();
114
+ }
115
+ /** The email, and that org's name and plan, only when ~/.claude.json is signed into the same account. */
116
+ function claudeJsonDetails(home, account, orgUuids) {
117
+ const details = { email: '', orgNames: {}, orgPlans: {} };
118
+ const claudeJson = readJson(join(home, '.claude.json'));
119
+ const oauth = claudeJson?.oauthAccount;
120
+ if (!oauth || text(oauth.accountUuid).toLowerCase() !== account)
121
+ return details;
122
+ details.email = text(oauth.emailAddress);
123
+ const org = text(oauth.organizationUuid).toLowerCase();
124
+ if (!org || !orgUuids.includes(org))
125
+ return details;
126
+ const name = text(oauth.organizationName);
127
+ if (name)
128
+ details.orgNames[org] = name;
129
+ const plan = readClaudeLoginPlan(home) || claudeCodePlan(claudeJson, oauth);
130
+ if (plan)
131
+ details.orgPlans[org] = plan;
132
+ return details;
133
+ }
134
+ /** Raw, in the order the backend reads Claude Code's plan, so both normalise alike. */
135
+ function claudeCodePlan(claudeJson, oauth) {
136
+ return (text(claudeJson?.subscriptionType) ||
137
+ text(oauth.subscriptionType) ||
138
+ text(oauth.organizationType) ||
139
+ text(oauth.seatTier));
140
+ }
141
+ /**
142
+ * One aggregate naming the orgs the signed-in Desktop account has used -- every one with a single
143
+ * account on this Mac, else the active one -- or [] when this isn't macOS or none is known.
144
+ */
145
+ function collectClaudeDesktopOrgs(home = homedir()) {
146
+ if (process.platform !== 'darwin')
147
+ return [];
148
+ const supportDir = claudeSupportDir(home);
149
+ const configPath = join(supportDir, 'config.json');
150
+ if (!existsSync(configPath))
151
+ return [];
152
+ const active = readDesktopActiveLogin(home);
153
+ const account = (active?.account || text(readJson(configPath)?.lastKnownAccountUuid)).toLowerCase();
154
+ if (!UUID.test(account))
155
+ return [];
156
+ const orgUuids = orgsToReport(supportDir, account, active);
157
+ if (orgUuids.length === 0)
158
+ return [];
159
+ const { email, orgNames, orgPlans } = claudeJsonDetails(home, account, orgUuids);
160
+ return [
161
+ {
162
+ file_type: FILE_TYPE,
163
+ file_path: configPath,
164
+ raw_content: {
165
+ account_uuid: account,
166
+ email,
167
+ org_uuids: orgUuids,
168
+ org_names: orgNames,
169
+ org_plans: orgPlans,
170
+ active_org_uuid: active?.org ?? '',
171
+ },
172
+ },
173
+ ];
174
+ }
175
+ export { collectClaudeDesktopOrgs, FILE_TYPE as CLAUDE_DESKTOP_ORGS_FILE_TYPE };
@@ -0,0 +1,72 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { readFileSync } from 'node:fs';
3
+ import { homedir } from 'node:os';
4
+ import { basename, join } from 'node:path';
5
+ /**
6
+ * The plan of the login Claude Code is actually using, from its stored OAuth credentials.
7
+ *
8
+ * `~/.claude.json`'s `oauthAccount` is a cached profile and can disagree with the login: a Team
9
+ * login has been seen there as `organizationType: "claude_pro"` on the person's personal org, while
10
+ * `/status` (which reads the credentials) said "Claude Team account". The credentials'
11
+ * `claudeAiOauth.subscriptionType` is what Claude Code bills and runs prompts against, so it wins.
12
+ *
13
+ * Credentials live in `~/.claude/.credentials.json` on Linux and Windows, and in the macOS login
14
+ * keychain as "Claude Code-credentials". Only `subscriptionType` is read out; the tokens beside it
15
+ * are never kept, logged or uploaded. The keychain is only read for the real home directory: it
16
+ * belongs to the signed-in macOS user, not to any other home a scan is pointed at.
17
+ */
18
+ const KEYCHAIN_SERVICE = 'Claude Code-credentials';
19
+ const PLAN = /^[a-z0-9_-]{1,64}$/i;
20
+ function planFrom(credentials) {
21
+ try {
22
+ const plan = JSON.parse(credentials)?.claudeAiOauth
23
+ ?.subscriptionType;
24
+ return typeof plan === 'string' && PLAN.test(plan.trim()) ? plan.trim() : '';
25
+ }
26
+ catch {
27
+ return '';
28
+ }
29
+ }
30
+ function readCredentialsFile(home) {
31
+ try {
32
+ return readFileSync(join(home, '.claude', '.credentials.json'), 'utf8');
33
+ }
34
+ catch {
35
+ return '';
36
+ }
37
+ }
38
+ function readKeychainCredentials(home) {
39
+ if (process.platform !== 'darwin' || home !== homedir())
40
+ return '';
41
+ try {
42
+ return execFileSync('security', ['find-generic-password', '-s', KEYCHAIN_SERVICE, '-w'], {
43
+ encoding: 'utf8',
44
+ stdio: ['ignore', 'pipe', 'ignore'],
45
+ timeout: 3000,
46
+ });
47
+ }
48
+ catch {
49
+ return '';
50
+ }
51
+ }
52
+ const isObject = (value) => typeof value === 'object' && value !== null;
53
+ /** Claude Code's login plan (e.g. "team", "pro"), or '' when no stored login names one. */
54
+ export function readClaudeLoginPlan(home = homedir()) {
55
+ return planFrom(readCredentialsFile(home)) || planFrom(readKeychainCredentials(home));
56
+ }
57
+ /**
58
+ * Sets the login plan as the top-level `subscriptionType` of each uploaded `~/.claude.json` with a
59
+ * claude.ai login, the field the backend reads first for Claude's plan. Files are returned as given
60
+ * when there is no such upload or no plan.
61
+ */
62
+ export function withClaudeLoginPlan(files, home = homedir()) {
63
+ const claudeJsons = files.filter((f) => basename(f.file_path) === '.claude.json' && isObject(f.raw_content?.oauthAccount));
64
+ if (claudeJsons.length === 0)
65
+ return files;
66
+ const plan = readClaudeLoginPlan(home);
67
+ if (!plan)
68
+ return files;
69
+ for (const file of claudeJsons)
70
+ file.raw_content.subscriptionType = plan;
71
+ return files;
72
+ }
@@ -339,4 +339,4 @@ function collectCodexTokenUsage(home = homedir()) {
339
339
  },
340
340
  ];
341
341
  }
342
- export { collectCodexTokenUsage, TOKEN_USAGE_RECENCY_DAYS, TOKEN_USAGE_FILE_TYPE };
342
+ export { collectCodexTokenUsage, readCodexIdentity, TOKEN_USAGE_RECENCY_DAYS, TOKEN_USAGE_FILE_TYPE };
@@ -236,6 +236,7 @@ export { collectHermesTokenUsage } from './hermes_token_usage_collector.js';
236
236
  export { collectOpenclawTokenUsage } from './openclaw_token_usage_collector.js';
237
237
  export { collectPiTokenUsage } from './pi_token_usage_collector.js';
238
238
  export { collectCoworkDesktopVersion } from './cowork_desktop_version_collector.js';
239
+ export { collectClaudeDesktopOrgs } from './claude_desktop_orgs_collector.js';
239
240
  export { collectGrokCliVersion } from './grok_cli_version_collector.js';
240
241
  export { collectClaudeDesktopExtensionManifests, collectClaudeDesktopExtensionSettingsFiles, enrichClaudeDesktopExtensionsInstallationsUpload, mergeClaudeDesktopExtensionEnableSettings, } from './claude_desktop_extensions_collector.js';
241
242
  export { collectCursorProjectWorkspaceMcpConfigs } from './cursor_project_mcp_collector.js';
@@ -43,6 +43,38 @@ function querySqlite(sqlite3, dbPath, sql) {
43
43
  return null;
44
44
  }
45
45
  }
46
+ /** ItemTable row holding Cursor's persisted app state, including the login's teams. */
47
+ const PERSISTENT_STORAGE_KEY = 'src.vs.platform.reactivestorage.browser.reactiveStorageServiceImpl.persistentStorage.applicationUser';
48
+ /**
49
+ * The Cursor teams the signed-in login belongs to, as strings, or null when they cannot be read.
50
+ *
51
+ * `aiSettings.teamIds` is the list Cursor's own `AllowedTeamId` policy checks. An empty list is a
52
+ * login on no team (a personal account) and is returned as []; only an unreadable or missing value
53
+ * is null, so the backend can tell "personal" from "unknown". Only that one field is extracted --
54
+ * the row is large and holds unrelated state.
55
+ */
56
+ function readCursorTeamIds(sqlite3, dbPath) {
57
+ const raw = querySqlite(sqlite3, dbPath, `SELECT json_extract(value, '$.aiSettings.teamIds') FROM ItemTable WHERE key = '${PERSISTENT_STORAGE_KEY}';`);
58
+ if (raw === null)
59
+ return null;
60
+ try {
61
+ const parsed = JSON.parse(raw.trim());
62
+ if (!Array.isArray(parsed))
63
+ return null;
64
+ return parsed
65
+ .filter((t) => typeof t === 'number' || typeof t === 'string')
66
+ .map((t) => String(t).trim())
67
+ .filter((t) => t.length > 0);
68
+ }
69
+ catch {
70
+ return null;
71
+ }
72
+ }
73
+ /** The email Cursor's signed-in login uses, or '' when there is none or it cannot be read. */
74
+ function readCursorEmail(sqlite3, dbPath) {
75
+ const raw = querySqlite(sqlite3, dbPath, `SELECT value FROM ItemTable WHERE key = 'cursorAuth/cachedEmail';`);
76
+ return (raw ?? '').trim();
77
+ }
46
78
  function num(value) {
47
79
  if (typeof value === 'number' && Number.isFinite(value))
48
80
  return value;
@@ -52,19 +84,44 @@ function num(value) {
52
84
  }
53
85
  return 0;
54
86
  }
55
- /**
56
- * Aggregate Cursor token usage / sessions / version / identity from state.vscdb.
57
- * Returns a single-element array (one per-machine aggregate) or [] when no usable DB exists.
58
- */
59
- function collectCursorTokenUsage(home = homedir()) {
60
- const dbPath = vscdbPath(home);
61
- if (!existsSync(dbPath))
62
- return [];
63
- const cutoffMs = Date.now() - TOKEN_USAGE_RECENCY_DAYS * 24 * 60 * 60 * 1000;
64
- const sqlite3 = resolveSqlite3Binary();
65
- if (!sqlite3)
66
- return [];
67
- // Identity: version, email, plan, subscription status, sign-up type, display name.
87
+ /** The `key<FIELD_SEP>value` rows of a query's output, split; rows without a separator are skipped. */
88
+ function splitRows(raw) {
89
+ const rows = [];
90
+ for (const row of (raw ?? '').split(ROW_SEP)) {
91
+ const trimmed = row.trim();
92
+ const sep = trimmed.indexOf(FIELD_SEP);
93
+ if (sep > -1)
94
+ rows.push([trimmed.slice(0, sep), trimmed.slice(sep + 1)]);
95
+ }
96
+ return rows;
97
+ }
98
+ /** ItemTable key → the identity field it fills (the scoped profile is handled on its own). */
99
+ const IDENTITY_KEYS = {
100
+ 'cursor.startupMetrics.lastVersion': 'version',
101
+ 'cursorAuth/cachedEmail': 'email',
102
+ 'cursorAuth/stripeMembershipType': 'plan',
103
+ 'cursorAuth/stripeSubscriptionStatus': 'subscriptionStatus',
104
+ 'cursorAuth/cachedSignUpType': 'signUpType',
105
+ };
106
+ /** The display name in Cursor's cached scoped profile, or '' when it is missing or not JSON. */
107
+ function profileDisplayName(value) {
108
+ try {
109
+ const profile = JSON.parse(value);
110
+ return typeof profile['displayName'] === 'string' ? profile['displayName'] : '';
111
+ }
112
+ catch {
113
+ /* non-JSON or missing — skip */
114
+ return '';
115
+ }
116
+ }
117
+ function setIdentityField(identity, key, value) {
118
+ if (key === 'cursorAuth/cachedScopedProfile')
119
+ identity.displayName = profileDisplayName(value);
120
+ else if (IDENTITY_KEYS[key])
121
+ identity[IDENTITY_KEYS[key]] = value;
122
+ }
123
+ /** Identity: version, email, plan, subscription status, sign-up type, display name. */
124
+ function readCursorIdentity(sqlite3, dbPath) {
68
125
  const identityRaw = querySqlite(sqlite3, dbPath, `SELECT key || char(31) || COALESCE(value, '') || char(30)
69
126
  FROM ItemTable
70
127
  WHERE key IN (
@@ -75,54 +132,24 @@ function collectCursorTokenUsage(home = homedir()) {
75
132
  'cursorAuth/cachedSignUpType',
76
133
  'cursorAuth/cachedScopedProfile'
77
134
  );`);
78
- let version = '';
79
- let email = '';
80
- let plan = '';
81
- let subscriptionStatus = '';
82
- let signUpType = '';
83
- let displayName = '';
84
- if (identityRaw) {
85
- for (const row of identityRaw.split(ROW_SEP)) {
86
- const trimmed = row.trim();
87
- if (!trimmed)
88
- continue;
89
- const sep = trimmed.indexOf(FIELD_SEP);
90
- if (sep === -1)
91
- continue;
92
- const key = trimmed.slice(0, sep);
93
- const value = trimmed.slice(sep + 1);
94
- switch (key) {
95
- case 'cursor.startupMetrics.lastVersion':
96
- version = value;
97
- break;
98
- case 'cursorAuth/cachedEmail':
99
- email = value;
100
- break;
101
- case 'cursorAuth/stripeMembershipType':
102
- plan = value;
103
- break;
104
- case 'cursorAuth/stripeSubscriptionStatus':
105
- subscriptionStatus = value;
106
- break;
107
- case 'cursorAuth/cachedSignUpType':
108
- signUpType = value;
109
- break;
110
- case 'cursorAuth/cachedScopedProfile':
111
- try {
112
- const profile = JSON.parse(value);
113
- if (typeof profile['displayName'] === 'string')
114
- displayName = profile['displayName'];
115
- }
116
- catch {
117
- /* non-JSON or missing — skip */
118
- }
119
- break;
120
- }
121
- }
122
- }
123
- // Usage: aggregate session counts and model names from composerData entries in the last 30
124
- // days. composerData:<conversationId> entries carry a Unix ms timestamp in $.createdAt and
125
- // the model in $.modelConfig.modelName. Token counts are not written by Cursor 3.8+.
135
+ const identity = {
136
+ version: '',
137
+ email: '',
138
+ plan: '',
139
+ subscriptionStatus: '',
140
+ signUpType: '',
141
+ displayName: '',
142
+ };
143
+ for (const [key, value] of splitRows(identityRaw))
144
+ setIdentityField(identity, key, value);
145
+ return identity;
146
+ }
147
+ /**
148
+ * Usage: aggregate session counts and model names from composerData entries since `cutoffMs`.
149
+ * composerData:<conversationId> entries carry a Unix ms timestamp in $.createdAt and
150
+ * the model in $.modelConfig.modelName. Token counts are not written by Cursor 3.8+.
151
+ */
152
+ function readCursorUsage(sqlite3, dbPath, cutoffMs) {
126
153
  // cutoffMs is a safe integer — interpolating directly avoids an extra round-trip.
127
154
  // key range (not LIKE 'composerData:%') so the query hits the primary-key index on `key`
128
155
  // instead of a full-table scan. SQLite's LIKE optimizer only rewrites a prefix match to an
@@ -140,53 +167,62 @@ function collectCursorTokenUsage(home = homedir()) {
140
167
  WHERE key >= 'composerData:' AND key < 'composerData;'
141
168
  AND CAST(json_extract(value, '$.createdAt') AS INTEGER) >= ${cutoffMs}
142
169
  GROUP BY json_extract(value, '$.modelConfig.modelName');`);
143
- const models = new Set();
144
- let sessionCount = 0;
145
170
  // Cursor 3.8+ doesn't write token counts, so there's no real per-model token weighting — the
146
171
  // per-model composer COUNT from the query above is the only usage-weight signal available.
147
172
  // Mirrors Cowork's session-count convention (see cowork_extract.py): "Most used model" is
148
173
  // derived from whichever model has the most composers, not the most tokens.
149
- const modelComposerCounts = {};
150
- if (usageRaw) {
151
- for (const row of usageRaw.split(ROW_SEP)) {
152
- const trimmed = row.trim();
153
- if (!trimmed)
154
- continue;
155
- const sep = trimmed.indexOf(FIELD_SEP);
156
- if (sep === -1)
157
- continue;
158
- const model = trimmed.slice(0, sep).trim();
159
- const cnt = num(trimmed.slice(sep + 1));
160
- sessionCount += cnt;
161
- if (model) {
162
- models.add(model);
163
- modelComposerCounts[model] = (modelComposerCounts[model] ?? 0) + cnt;
164
- }
165
- }
174
+ const usage = { models: new Set(), sessionCount: 0, modelComposerCounts: {} };
175
+ for (const [rawModel, rawCount] of splitRows(usageRaw)) {
176
+ const model = rawModel.trim();
177
+ const cnt = num(rawCount);
178
+ usage.sessionCount += cnt;
179
+ if (!model)
180
+ continue;
181
+ usage.models.add(model);
182
+ usage.modelComposerCounts[model] = (usage.modelComposerCounts[model] ?? 0) + cnt;
166
183
  }
167
- if (sessionCount === 0 && !email && !version)
184
+ return usage;
185
+ }
186
+ /**
187
+ * Aggregate Cursor token usage / sessions / version / identity from state.vscdb.
188
+ * Returns a single-element array (one per-machine aggregate) or [] when no usable DB exists.
189
+ */
190
+ function collectCursorTokenUsage(home = homedir()) {
191
+ const dbPath = vscdbPath(home);
192
+ if (!existsSync(dbPath))
193
+ return [];
194
+ const cutoffMs = Date.now() - TOKEN_USAGE_RECENCY_DAYS * 24 * 60 * 60 * 1000;
195
+ const sqlite3 = resolveSqlite3Binary();
196
+ if (!sqlite3)
197
+ return [];
198
+ const identity = readCursorIdentity(sqlite3, dbPath);
199
+ const usage = readCursorUsage(sqlite3, dbPath, cutoffMs);
200
+ if (usage.sessionCount === 0 && !identity.email && !identity.version)
168
201
  return [];
202
+ const teamIds = readCursorTeamIds(sqlite3, dbPath);
169
203
  return [
170
204
  {
171
205
  file_type: TOKEN_USAGE_FILE_TYPE,
172
206
  file_path: dbPath,
173
207
  raw_content: {
174
- version,
175
- session_count: sessionCount,
176
- models: [...models].sort(),
177
- model_token_split: modelComposerCounts,
208
+ version: identity.version,
209
+ session_count: usage.sessionCount,
210
+ models: [...usage.models].sort(),
211
+ model_token_split: usage.modelComposerCounts,
178
212
  window_days: TOKEN_USAGE_RECENCY_DAYS,
179
213
  totals: {
180
214
  input_tokens: 0,
181
215
  output_tokens: 0,
182
216
  },
183
- email,
184
- display_name: displayName,
185
- plan,
186
- subscription_status: subscriptionStatus,
187
- auth_method: signUpType ? 'cursor_account' : '',
217
+ email: identity.email,
218
+ display_name: identity.displayName,
219
+ plan: identity.plan,
220
+ subscription_status: identity.subscriptionStatus,
221
+ auth_method: identity.signUpType ? 'cursor_account' : '',
222
+ // Omitted when unreadable: the backend reads a missing list as unknown, [] as personal.
223
+ ...(teamIds !== null ? { team_ids: teamIds } : {}),
188
224
  },
189
225
  },
190
226
  ];
191
227
  }
192
- export { collectCursorTokenUsage, TOKEN_USAGE_RECENCY_DAYS, TOKEN_USAGE_FILE_TYPE, vscdbPath };
228
+ export { collectCursorTokenUsage, readCursorEmail, readCursorTeamIds, TOKEN_USAGE_RECENCY_DAYS, TOKEN_USAGE_FILE_TYPE, vscdbPath, };