@usagefleet/cli 1.2.151 → 1.2.152

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/dist/collector.js CHANGED
@@ -75,7 +75,7 @@ export async function runOnce(cfg, log = () => {
75
75
  if (!tail || tail.consumedBytes === 0) {
76
76
  continue;
77
77
  }
78
- if (tail.records.length === 0) {
78
+ if (tail.segments.length === 0) {
79
79
  // Consumed only non-usage lines — safe to advance immediately.
80
80
  state.files[fp] = tail.nextState;
81
81
  advanced = true;
@@ -83,8 +83,8 @@ export async function runOnce(cfg, log = () => {
83
83
  }
84
84
  // sendChunk absorbs "invalid" by bisecting, so only auth/plan/transient escape.
85
85
  let outcome = 'ok';
86
- for (let i = 0; i < tail.records.length; i += step) {
87
- outcome = await sendChunk(tail.records.slice(i, i + step), cfg, result, log);
86
+ for (const chunk of chunks(tail.segments, step)) {
87
+ outcome = await sendChunk(chunk.records, chunk.accountExtId, cfg, result, log);
88
88
  if (outcome !== 'ok') {
89
89
  break;
90
90
  }
@@ -151,9 +151,9 @@ export async function runOnce(cfg, log = () => {
151
151
  * anything that survives that many splits is not record-shaped.
152
152
  */
153
153
  const MAX_DROPPED_PER_CHUNK = 2;
154
- async function sendChunk(records, cfg, result, log, dropCeiling = result.dropped + MAX_DROPPED_PER_CHUNK) {
154
+ async function sendChunk(records, accountExtId, cfg, result, log, dropCeiling = result.dropped + MAX_DROPPED_PER_CHUNK) {
155
155
  const res = await uploadBatch({
156
- accountExtId: detectClaudeAccount()?.extId,
156
+ accountExtId: accountExtId ?? detectClaudeAccount()?.extId,
157
157
  collectorVersion: RELEASE_VERSION,
158
158
  hostname: hostname(),
159
159
  os: detectOs(),
@@ -185,8 +185,18 @@ async function sendChunk(records, cfg, result, log, dropCeiling = result.dropped
185
185
  return 'ok';
186
186
  }
187
187
  const mid = Math.ceil(records.length / 2);
188
- const head = await sendChunk(records.slice(0, mid), cfg, result, log, dropCeiling);
189
- return head === 'ok' ? sendChunk(records.slice(mid), cfg, result, log, dropCeiling) : head;
188
+ const head = await sendChunk(records.slice(0, mid), accountExtId, cfg, result, log, dropCeiling);
189
+ return head === 'ok' ? sendChunk(records.slice(mid), accountExtId, cfg, result, log, dropCeiling) : head;
190
+ }
191
+ /** Upload-sized slices of each segment; a chunk never spans two accounts. */
192
+ function chunks(segments, step) {
193
+ const out = [];
194
+ for (const { accountExtId, records } of segments) {
195
+ for (let i = 0; i < records.length; i += step) {
196
+ out.push({ accountExtId, records: records.slice(i, i + step) });
197
+ }
198
+ }
199
+ return out;
190
200
  }
191
201
  /**
192
202
  * Drop offsets for logs that no longer exist on disk, so a long-lived install
package/dist/release.js CHANGED
@@ -1,2 +1,2 @@
1
1
  // Generated by .github/workflows/release.yml.
2
- export const RELEASE_VERSION = "1.2.151";
2
+ export const RELEASE_VERSION = "1.2.152";
package/dist/tailer.js CHANGED
@@ -5,6 +5,8 @@ import { dim, line, tilde, yellow } from './ui.js';
5
5
  const MAX_READ = 16 * 1024 * 1024;
6
6
  // Both parsers need a `usage` object, and about 3/4 of log bytes are lines without one.
7
7
  const USAGE_KEY = Buffer.from('"usage"');
8
+ // Unescaped quotes: the same text quoted inside a message is `\"customType\"` and never matches.
9
+ const ACCOUNT_KEY = Buffer.from('"customType":"claude-account"');
8
10
  /**
9
11
  * The working directory of a pi session, read from the file's first line
10
12
  * (`{"type":"session",…,"cwd":"/path"}`). pi's message lines carry no cwd, and
@@ -26,6 +28,18 @@ function piSessionCwd(fd) {
26
28
  return null;
27
29
  }
28
30
  }
31
+ /** The account a pi session pinned itself to, from the dotfiles claude-accounts extension's
32
+ * `{"type":"custom","customType":"claude-account","data":{"email","uuid"}}` line. Every
33
+ * later line in the file ran on it, until the next such line. */
34
+ function piSessionAccount(line) {
35
+ try {
36
+ const uuid = JSON.parse(line)?.data?.uuid;
37
+ return typeof uuid === 'string' && uuid !== '' ? uuid.slice(0, 100) : undefined;
38
+ }
39
+ catch {
40
+ return undefined;
41
+ }
42
+ }
29
43
  /**
30
44
  * Read new, newline-terminated lines from `filePath` starting at the previously
31
45
  * stored offset. Handles rotation/truncation (inode change or size < offset →
@@ -41,9 +55,10 @@ export function tailFile(filePath, prev, source = 'cli') {
41
55
  }
42
56
  const rotated = prev !== undefined && (st.ino !== prev.inode || st.size < prev.offset);
43
57
  const start = rotated || prev === undefined ? 0 : prev.offset;
44
- const base = { inode: Number(st.ino), offset: start };
58
+ const startAccount = rotated ? undefined : prev?.accountExtId;
59
+ const base = { accountExtId: startAccount, inode: Number(st.ino), offset: start };
45
60
  if (st.size <= start) {
46
- return { consumedBytes: 0, nextState: base, records: [] };
61
+ return { consumedBytes: 0, nextState: base, segments: [] };
47
62
  }
48
63
  // Cap the per-cycle read so a huge backlog can't OOM; the next cycle resumes
49
64
  // from the committed offset.
@@ -70,29 +85,53 @@ export function tailFile(filePath, prev, source = 'cli') {
70
85
  return {
71
86
  consumedBytes: length,
72
87
  nextState: { ...base, offset: start + length },
73
- records: [],
88
+ segments: [],
74
89
  };
75
90
  }
76
- return { consumedBytes: 0, nextState: base, records: [] };
91
+ return { consumedBytes: 0, nextState: base, segments: [] };
77
92
  }
78
93
  const consumed = buf.subarray(0, lastNl + 1);
79
- // Decode only the lines that can hold usage: decoding and splitting the whole
80
- // 16 MB read peaked a first-run backlog at ~490 MB RSS.
81
- const records = [];
82
- let at = consumed.indexOf(USAGE_KEY);
83
- while (at !== -1) {
94
+ // Decode only the lines that can hold usage or a pi account switch: decoding and
95
+ // splitting the whole 16 MB read peaked a first-run backlog at ~490 MB RSS.
96
+ const segments = [];
97
+ let account = startAccount;
98
+ let usageAt = consumed.indexOf(USAGE_KEY);
99
+ let accountAt = source === 'pi' ? consumed.indexOf(ACCOUNT_KEY) : -1;
100
+ while (usageAt !== -1 || accountAt !== -1) {
101
+ const isAccountLine = accountAt !== -1 && (usageAt === -1 || accountAt < usageAt);
102
+ const at = isAccountLine ? accountAt : usageAt;
84
103
  const lineStart = consumed.lastIndexOf(0x0a, at) + 1;
85
104
  const lineEnd = consumed.indexOf(0x0a, at);
86
- const rec = parseLine(consumed.toString('utf-8', lineStart, lineEnd), source, sessionCwd);
87
- if (rec) {
88
- records.push(rec);
105
+ const text = consumed.toString('utf-8', lineStart, lineEnd);
106
+ if (isAccountLine) {
107
+ account = piSessionAccount(text);
108
+ }
109
+ else {
110
+ const rec = parseLine(text, source, sessionCwd);
111
+ if (rec) {
112
+ addRecord(segments, account, rec);
113
+ }
114
+ }
115
+ if (usageAt !== -1 && usageAt < lineEnd) {
116
+ usageAt = consumed.indexOf(USAGE_KEY, lineEnd);
117
+ }
118
+ if (accountAt !== -1 && accountAt < lineEnd) {
119
+ accountAt = consumed.indexOf(ACCOUNT_KEY, lineEnd);
89
120
  }
90
- at = consumed.indexOf(USAGE_KEY, lineEnd);
91
121
  }
92
122
  const consumedBytes = consumed.length;
93
123
  return {
94
124
  consumedBytes,
95
- nextState: { ...base, offset: start + consumedBytes },
96
- records,
125
+ nextState: { accountExtId: account, inode: base.inode, offset: start + consumedBytes },
126
+ segments,
97
127
  };
98
128
  }
129
+ function addRecord(segments, accountExtId, record) {
130
+ const last = segments.at(-1);
131
+ if (last && last.accountExtId === accountExtId) {
132
+ last.records.push(record);
133
+ }
134
+ else {
135
+ segments.push({ accountExtId, records: [record] });
136
+ }
137
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usagefleet/cli",
3
- "version": "1.2.151",
3
+ "version": "1.2.152",
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",