@usagefleet/cli 1.2.164 → 1.2.165

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.
@@ -33,6 +33,19 @@ export function detectClaudeAccount(path = claudeStatePath()) {
33
33
  }
34
34
  }
35
35
  const OAUTH_PROFILE_URL = 'https://api.anthropic.com/api/oauth/profile';
36
+ /** The OAuth access token of the login jcode (or pj) has active in its `auth.json`,
37
+ * falling back to the first saved login the way jcode does. Null when unreadable. */
38
+ export function activeJcodeToken(authPath) {
39
+ try {
40
+ const auth = JSON.parse(readFileSync(authPath, 'utf-8'));
41
+ const accounts = auth.anthropic_accounts ?? [];
42
+ const active = accounts.find(a => a.label === auth.active_anthropic_account) ?? accounts[0];
43
+ return typeof active?.access === 'string' && active.access !== '' ? active.access : null;
44
+ }
45
+ catch {
46
+ return null;
47
+ }
48
+ }
36
49
  /** The accountUuid Anthropic says owns this OAuth token, or null when it cannot
37
50
  * say. Null fails open to the file's identity: the endpoint is undocumented, and
38
51
  * losing it must not stop every collector from reporting limits. */
package/dist/collector.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import { hostname } from 'node:os';
3
- import { sep } from 'node:path';
4
- import { detectClaudeAccount, tokenOwner } from './claude-account.js';
3
+ import { dirname, join, sep } from 'node:path';
4
+ import { activeJcodeToken, detectClaudeAccount, tokenOwner } from './claude-account.js';
5
5
  import { detectClaudeCreds, macKeychainDenied } from './claude-creds.js';
6
6
  import { fetchLimits } from './claude-limits.js';
7
7
  import { ENDPOINT } from './config.js';
@@ -29,7 +29,7 @@ const LIMITS_CACHE_REFRESH_MS = 10 * 60_000;
29
29
  */
30
30
  export async function runOnce(cfg, log = () => {
31
31
  /* empty */
32
- }, dirCache = new Map()) {
32
+ }, dirCache = new Map(), tokenOwners = new Map()) {
33
33
  const { state } = readStore(cfg.storePath);
34
34
  // Each scan root is tagged with the app that owns it. Claude Code's projects
35
35
  // dir is scanned whole; the Claude Desktop sessions root is filtered to its
@@ -52,16 +52,15 @@ export async function runOnce(cfg, log = () => {
52
52
  files.push({ fp, source: root.source });
53
53
  }
54
54
  }
55
- const jcodeRoots = [
56
- { dir: cfg.jcodeDir, source: 'jcode' },
57
- { dir: cfg.pjDir, source: 'pj' },
58
- ];
59
- const jcodeSessions = jcodeRoots.flatMap(({ dir, source }) => dir ? listJcodeSessions(dir).map(fp => ({ fp, source })) : []);
55
+ const jcodeSessions = await listJcodeSessionsWithAccount(cfg, tokenOwners);
60
56
  // Each source yields its new records and how to commit having sent them, so both
61
57
  // share one upload path: offsets for appended JSONL, a watermark for jcode and pj.
62
58
  const reads = [
63
59
  ...files.map(({ fp, source }) => ({ fp, read: () => tailPending(fp, source, state.files) })),
64
- ...jcodeSessions.map(({ fp, source }) => ({ fp, read: () => jcodePending(fp, source, state.jcode) })),
60
+ ...jcodeSessions.map(({ accountExtId, fp, source }) => ({
61
+ fp,
62
+ read: () => jcodePending(fp, source, accountExtId, state.jcode),
63
+ })),
65
64
  ];
66
65
  const result = {
67
66
  accepted: 0,
@@ -228,7 +227,7 @@ function tailPending(fp, source, files) {
228
227
  segments: tail.segments,
229
228
  };
230
229
  }
231
- function jcodePending(fp, source, sessions) {
230
+ function jcodePending(fp, source, accountExtId, sessions) {
232
231
  const session = readJcodeSession(fp, source, sessions[fp]);
233
232
  if (!session) {
234
233
  return null;
@@ -237,11 +236,50 @@ function jcodePending(fp, source, sessions) {
237
236
  commit: () => {
238
237
  sessions[fp] = session.nextState;
239
238
  },
240
- // shortcut: jcode keeps its own Anthropic logins and stamps none on a session, so its
241
- // turns book on the device's Claude Code account; read jcode's active login if they differ.
242
- segments: session.records.length > 0 ? [{ accountExtId: undefined, records: session.records }] : [],
239
+ segments: session.records.length > 0 ? [{ accountExtId, records: session.records }] : [],
243
240
  };
244
241
  }
242
+ async function listJcodeSessionsWithAccount(cfg, tokenOwners) {
243
+ const roots = [
244
+ { dir: cfg.jcodeDir, source: 'jcode' },
245
+ { dir: cfg.pjDir, source: 'pj' },
246
+ ];
247
+ const out = [];
248
+ for (const { dir, source } of roots) {
249
+ const sessions = dir ? listJcodeSessions(dir) : [];
250
+ if (!dir || sessions.length === 0) {
251
+ continue;
252
+ }
253
+ const accountExtId = await jcodeAccount(dir, tokenOwners);
254
+ for (const fp of sessions) {
255
+ out.push({ accountExtId, fp, source });
256
+ }
257
+ }
258
+ return out;
259
+ }
260
+ /** The Anthropic account jcode (or pj) is signed into: the owner of the active login in
261
+ * the `auth.json` beside its sessions dir. Undefined (the batch then books on the
262
+ * Claude Code account) when that file or the profile lookup fails. Owners are cached per
263
+ * access token, so the profile call repeats only when jcode refreshes or switches logins.
264
+ * shortcut: books a turn on the login active at upload time, so a switch between a turn
265
+ * and its upload (or a backfill) books it on the new login; stamp the login per turn in
266
+ * jcode if that matters. */
267
+ async function jcodeAccount(sessionsDir, owners) {
268
+ const token = activeJcodeToken(join(dirname(sessionsDir), 'auth.json'));
269
+ if (!token) {
270
+ return undefined;
271
+ }
272
+ const cached = owners.get(token);
273
+ if (cached) {
274
+ return cached;
275
+ }
276
+ const owner = await tokenOwner(token);
277
+ if (!owner) {
278
+ return undefined;
279
+ }
280
+ owners.set(token, owner);
281
+ return owner;
282
+ }
245
283
  /** Drop the progress of jcode sessions no longer on disk; returns whether anything went. */
246
284
  function pruneGoneSessions(sessions, listed) {
247
285
  const kept = new Set(listed);
package/dist/index.js CHANGED
@@ -97,13 +97,14 @@ async function cmdWatch() {
97
97
  let timer = null;
98
98
  let running = false;
99
99
  const dirCache = new Map();
100
+ const tokenOwners = new Map();
100
101
  const tick = async () => {
101
102
  if (stopping) {
102
103
  return;
103
104
  }
104
105
  running = true;
105
106
  try {
106
- const r = await runOnce(cfg, stream, dirCache);
107
+ const r = await runOnce(cfg, stream, dirCache, tokenOwners);
107
108
  // Dropped records are real data loss, so they must show up even in a
108
109
  // cycle that uploaded nothing.
109
110
  if (r.sent > 0 || r.dropped > 0) {
package/dist/release.js CHANGED
@@ -1,2 +1,2 @@
1
1
  // Generated by .github/workflows/release.yml.
2
- export const RELEASE_VERSION = "1.2.164";
2
+ export const RELEASE_VERSION = "1.2.165";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usagefleet/cli",
3
- "version": "1.2.164",
3
+ "version": "1.2.165",
4
4
  "description": "Tails Claude Code, Claude Desktop, and pi agent JSONL logs and reports token usage to a UsageFleet server.",
5
5
  "keywords": [
6
6
  "claude",