@usagefleet/cli 1.2.163 → 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.
package/README.md CHANGED
@@ -78,8 +78,8 @@ shell once. `uninstall` removes both again, and self-update keeps them current.
78
78
  Desktop agent-mode (Cowork) sessions, and the
79
79
  [pi](https://github.com/badlogic/pi-mono) agent (`~/.pi/agent/sessions`,
80
80
  `anthropic` and `claude-bridge` provider records only; other providers don't
81
- touch Claude limits), and the jcode agent (`~/.jcode/sessions`, Claude
82
- subscription turns only).
81
+ touch Claude limits), and the jcode agent (`~/.jcode/sessions`) and its pj fork
82
+ (`~/.pj/sessions`), Claude subscription turns only.
83
83
  A per-file byte offset in the config file means each line is sent once;
84
84
  rotation and truncation are handled and partial lines are never sent. Delivery
85
85
  is at-least-once — the server dedups on `uuid`.
@@ -159,6 +159,7 @@ it (`usagefleet config` prints this same table):
159
159
  | `desktopDir` | `USAGEFLEET_DESKTOP` | override the Claude Desktop sessions dir; `off` to skip it |
160
160
  | `piDir` | `USAGEFLEET_PI` | pi sessions dirs — string or array in the file, comma-separated in env; `off` to skip |
161
161
  | `jcodeDir` | `USAGEFLEET_JCODE` | override the jcode sessions dir (`~/.jcode/sessions`); `off` to skip it |
162
+ | `pjDir` | `USAGEFLEET_PJ` | override the pj sessions dir (`~/.pj/sessions`); `off` to skip it |
162
163
  | `interval` | `USAGEFLEET_INTERVAL` | watch poll seconds (default 15) |
163
164
  | `limitsInterval` | `USAGEFLEET_LIMITS_INTERVAL` | seconds between limit reports (default 60; 300 on API keys, where each report costs a 1-token ping) |
164
165
  | `notifications` | `USAGEFLEET_NOTIFY` | desktop notifications, on by default (`false`/`0` disables) |
@@ -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,12 +52,15 @@ export async function runOnce(cfg, log = () => {
52
52
  files.push({ fp, source: root.source });
53
53
  }
54
54
  }
55
- const jcodeSessions = cfg.jcodeDir ? listJcodeSessions(cfg.jcodeDir) : [];
55
+ const jcodeSessions = await listJcodeSessionsWithAccount(cfg, tokenOwners);
56
56
  // Each source yields its new records and how to commit having sent them, so both
57
- // share one upload path: offsets for appended JSONL, a watermark for jcode.
57
+ // share one upload path: offsets for appended JSONL, a watermark for jcode and pj.
58
58
  const reads = [
59
59
  ...files.map(({ fp, source }) => ({ fp, read: () => tailPending(fp, source, state.files) })),
60
- ...jcodeSessions.map(fp => ({ fp, read: () => jcodePending(fp, state.jcode) })),
60
+ ...jcodeSessions.map(({ accountExtId, fp, source }) => ({
61
+ fp,
62
+ read: () => jcodePending(fp, source, accountExtId, state.jcode),
63
+ })),
61
64
  ];
62
65
  const result = {
63
66
  accepted: 0,
@@ -140,7 +143,7 @@ export async function runOnce(cfg, log = () => {
140
143
  // dedups. Only our own section is replaced, so a token written by a
141
144
  // concurrent `usagefleet login` survives.
142
145
  const pruned = pruneMissingFiles(state, files);
143
- const prunedJcode = pruneGoneSessions(state.jcode, jcodeSessions);
146
+ const prunedJcode = pruneGoneSessions(state.jcode, jcodeSessions.map(s => s.fp));
144
147
  if (pruned || prunedJcode || advanced) {
145
148
  updateStore(cfg.storePath, store => {
146
149
  store.state.files = state.files;
@@ -224,8 +227,8 @@ function tailPending(fp, source, files) {
224
227
  segments: tail.segments,
225
228
  };
226
229
  }
227
- function jcodePending(fp, sessions) {
228
- const session = readJcodeSession(fp, sessions[fp]);
230
+ function jcodePending(fp, source, accountExtId, sessions) {
231
+ const session = readJcodeSession(fp, source, sessions[fp]);
229
232
  if (!session) {
230
233
  return null;
231
234
  }
@@ -233,11 +236,50 @@ function jcodePending(fp, sessions) {
233
236
  commit: () => {
234
237
  sessions[fp] = session.nextState;
235
238
  },
236
- // shortcut: jcode keeps its own Anthropic logins and stamps none on a session, so its
237
- // turns book on the device's Claude Code account; read jcode's active login if they differ.
238
- segments: session.records.length > 0 ? [{ accountExtId: undefined, records: session.records }] : [],
239
+ segments: session.records.length > 0 ? [{ accountExtId, records: session.records }] : [],
239
240
  };
240
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
+ }
241
283
  /** Drop the progress of jcode sessions no longer on disk; returns whether anything went. */
242
284
  function pruneGoneSessions(sessions, listed) {
243
285
  const kept = new Set(listed);
package/dist/config.js CHANGED
@@ -1,4 +1,4 @@
1
- import { defaultDesktopSessionsDir, defaultJcodeSessionsDir, defaultPiSessionsDirs, defaultProjectsDir, } from './paths.js';
1
+ import { defaultDesktopSessionsDir, defaultJcodeSessionsDir, defaultPiSessionsDirs, defaultPjSessionsDir, defaultProjectsDir, } from './paths.js';
2
2
  import { readStore, storePath } from './store.js';
3
3
  /** Matches the server's BatchSchema `.max(1000)`. */
4
4
  const MAX_BATCH = 1000;
@@ -28,6 +28,7 @@ export function loadConfig() {
28
28
  desktopDir: resolveOptionalDir(process.env.USAGEFLEET_DESKTOP, file.desktopDir, defaultDesktopSessionsDir()),
29
29
  jcodeDir: resolveOptionalDir(process.env.USAGEFLEET_JCODE, file.jcodeDir, defaultJcodeSessionsDir()),
30
30
  piDirs: resolvePiDirs(process.env.USAGEFLEET_PI, file.piDir),
31
+ pjDir: resolveOptionalDir(process.env.USAGEFLEET_PJ, file.pjDir, defaultPjSessionsDir()),
31
32
  projectsDir: process.env.USAGEFLEET_PROJECTS || file.projectsDir || defaultProjectsDir(),
32
33
  storePath: storePath(),
33
34
  token,
package/dist/index.js CHANGED
@@ -89,7 +89,7 @@ async function cmdWatch() {
89
89
  const rawUpdate = positiveNumber(process.env.USAGEFLEET_UPDATE_INTERVAL, settings.updateInterval) ?? 6 * 60 * 60;
90
90
  const updateInterval = Math.max(60, rawUpdate) * 1000;
91
91
  let lastUpdateAt = 0;
92
- const watching = [cfg.projectsDir, cfg.desktopDir, ...cfg.piDirs, cfg.jcodeDir].filter((d) => !!d);
92
+ const watching = [cfg.projectsDir, cfg.desktopDir, ...cfg.piDirs, cfg.jcodeDir, cfg.pjDir].filter((d) => !!d);
93
93
  console.log(header(`watching every ${interval / 1000}s`));
94
94
  console.log(hint(`${watching.map(tilde).join(' · ')} → ${host(ENDPOINT)}`));
95
95
  console.log('');
@@ -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) {
@@ -192,7 +193,7 @@ async function cmdStatus() {
192
193
  console.log('');
193
194
  console.log(row('endpoint', host(ENDPOINT)));
194
195
  console.log(row('device', `${state.deviceId} · token ${tokenFor(cfg, detectClaudeAccount()?.extId).slice(0, 8)}…`));
195
- const watching = [cfg.projectsDir, cfg.desktopDir, ...cfg.piDirs, cfg.jcodeDir].filter((d) => !!d);
196
+ const watching = [cfg.projectsDir, cfg.desktopDir, ...cfg.piDirs, cfg.jcodeDir, cfg.pjDir].filter((d) => !!d);
196
197
  for (const [i, dir] of watching.entries()) {
197
198
  console.log(row(i === 0 ? 'watching' : '', tilde(dir)));
198
199
  }
@@ -283,6 +284,7 @@ function cmdConfig() {
283
284
  ['desktopDir', 'USAGEFLEET_DESKTOP', 'override the Claude Desktop dir ("off" disables)'],
284
285
  ['piDir', 'USAGEFLEET_PI', 'pi session dirs · string or array; env comma-separated'],
285
286
  ['jcodeDir', 'USAGEFLEET_JCODE', 'override ~/.jcode/sessions ("off" disables)'],
287
+ ['pjDir', 'USAGEFLEET_PJ', 'override ~/.pj/sessions ("off" disables)'],
286
288
  ['interval', 'USAGEFLEET_INTERVAL', 'watch interval seconds (default 15)'],
287
289
  [
288
290
  'limitsInterval',
package/dist/jcode.js CHANGED
@@ -20,7 +20,7 @@ export function listJcodeSessions(dir) {
20
20
  * (an interrupted one leaves a turn in both). So both are read and turns key on their
21
21
  * id, and anything at or before `prev.uploadedThrough` was already sent.
22
22
  */
23
- export function readJcodeSession(snapshotPath, prev) {
23
+ export function readJcodeSession(snapshotPath, source, prev) {
24
24
  const journalPath = snapshotPath.replace(/\.json$/, '.journal.jsonl');
25
25
  const mtimeMs = Math.max(statSync(snapshotPath).mtimeMs, existsSync(journalPath) ? statSync(journalPath).mtimeMs : 0);
26
26
  if (prev?.mtimeMs === mtimeMs) {
@@ -40,7 +40,7 @@ export function readJcodeSession(snapshotPath, prev) {
40
40
  const seen = new Set();
41
41
  const records = [];
42
42
  for (const [message, meta] of turns) {
43
- const record = toRecord(message, meta, str(snapshot.id));
43
+ const record = toRecord(message, meta, str(snapshot.id), source);
44
44
  if (!record || seen.has(record.uuid) || Date.parse(record.timestamp) <= uploadedThrough) {
45
45
  continue;
46
46
  }
@@ -69,7 +69,7 @@ function readJournal(path) {
69
69
  return lines;
70
70
  }
71
71
  /** jcode's `token_usage` is Anthropic's usage block (`prompt_tokens` is their sum, left out). */
72
- function toRecord(m, meta, sessionId) {
72
+ function toRecord(m, meta, sessionId, source) {
73
73
  const u = m.token_usage;
74
74
  const id = str(m.id);
75
75
  const timestamp = str(m.timestamp);
@@ -96,9 +96,9 @@ function toRecord(m, meta, sessionId) {
96
96
  requestId: null,
97
97
  serviceTier: null,
98
98
  sessionId,
99
- source: 'jcode',
99
+ source,
100
100
  timestamp,
101
- uuid: `jcode:${id}`,
101
+ uuid: `${source}:${id}`,
102
102
  version: null,
103
103
  };
104
104
  }
package/dist/paths.js CHANGED
@@ -68,6 +68,10 @@ function realDir(dir) {
68
68
  export function defaultJcodeSessionsDir() {
69
69
  return join(homedir(), '.jcode', 'sessions');
70
70
  }
71
+ /** pj is a jcode fork that keeps its sessions in `~/.pj/sessions`; JCODE_HOME moves it too. */
72
+ export function defaultPjSessionsDir() {
73
+ return join(homedir(), '.pj', 'sessions');
74
+ }
71
75
  /** pi's agent dir, the root for its sessions and extensions.
72
76
  * PI_CODING_AGENT_DIR is pi's own relocation knob; unlike the service-side
73
77
  * scan above, the guard installer runs from the user's shell where that env
package/dist/release.js CHANGED
@@ -1,2 +1,2 @@
1
1
  // Generated by .github/workflows/release.yml.
2
- export const RELEASE_VERSION = "1.2.163";
2
+ export const RELEASE_VERSION = "1.2.165";
package/dist/store.js CHANGED
@@ -62,6 +62,7 @@ function normalize(raw) {
62
62
  },
63
63
  notifyThresholds: raw.notifyThresholds,
64
64
  piDir: raw.piDir,
65
+ pjDir: raw.pjDir,
65
66
  projectsDir: raw.projectsDir,
66
67
  state: {
67
68
  deviceId: raw.state?.deviceId || randomUUID(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usagefleet/cli",
3
- "version": "1.2.163",
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",