@usagefleet/cli 1.2.59

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/parser.js ADDED
@@ -0,0 +1,126 @@
1
+ /** Only accept genuine strings; ignores numbers/objects that would later break. */
2
+ function str(v) {
3
+ return typeof v === 'string' && v.length > 0 ? v : null;
4
+ }
5
+ /** A valid ISO-ish timestamp string, else the current time (never an invalid one). */
6
+ function validTimestamp(v) {
7
+ if (typeof v === 'string' && !Number.isNaN(new Date(v).getTime())) {
8
+ return v;
9
+ }
10
+ return new Date().toISOString();
11
+ }
12
+ function cacheCreation(u) {
13
+ if (typeof u.cache_creation_input_tokens === 'number') {
14
+ return u.cache_creation_input_tokens;
15
+ }
16
+ const c = u.cache_creation;
17
+ if (c) {
18
+ return (c.ephemeral_5m_input_tokens ?? 0) + (c.ephemeral_1h_input_tokens ?? 0);
19
+ }
20
+ return 0;
21
+ }
22
+ /** pi agent session line: `{type:"message", id, timestamp, message:{role:"assistant",
23
+ * provider, model, responseId, usage:{input, output, cacheRead, cacheWrite}}}`.
24
+ * Only provider "anthropic" hits the user's Claude account — other providers
25
+ * (openai-codex, openrouter, …) are skipped. `output` already includes reasoning
26
+ * tokens (totalTokens = input + output + cacheRead + cacheWrite). */
27
+ function parsePiLine(o) {
28
+ if (o.type !== 'message') {
29
+ return null;
30
+ }
31
+ const m = o.message;
32
+ if (!m || m.role !== 'assistant' || m.provider !== 'anthropic') {
33
+ return null;
34
+ }
35
+ const u = m.usage;
36
+ if (typeof u !== 'object' || u === null) {
37
+ return null;
38
+ }
39
+ // Idempotency key: the Anthropic response id is globally unique; the line's own
40
+ // short id needs the timestamp to be collision-safe across sessions.
41
+ const rid = str(m.responseId);
42
+ const lid = str(o.id);
43
+ const uuid = rid ? `pi:${rid}` : lid ? `pi:${lid}:${validTimestamp(o.timestamp)}` : null;
44
+ if (!uuid) {
45
+ return null;
46
+ }
47
+ return {
48
+ cacheCreationTokens: u.cacheWrite ?? 0,
49
+ cacheReadTokens: u.cacheRead ?? 0,
50
+ cwd: null,
51
+ gitBranch: null,
52
+ inputTokens: u.input ?? 0,
53
+ messageId: rid,
54
+ model: str(m.model),
55
+ outputTokens: u.output ?? 0,
56
+ requestId: null,
57
+ serviceTier: null,
58
+ sessionId: null,
59
+ source: 'pi',
60
+ timestamp: validTimestamp(o.timestamp),
61
+ uuid,
62
+ version: null,
63
+ };
64
+ }
65
+ /**
66
+ * Parse a single JSONL line. Returns a UsageRecord only for assistant messages
67
+ * that carry a usage object; everything else (user/system/summary/tool/…) → null.
68
+ * `uuid` is the per-line idempotency key the server dedups on. `source` tags which
69
+ * app the file came from (the line itself carries no app identifier) and selects
70
+ * the format: `pi` files use pi's own schema, everything else Claude Code's.
71
+ */
72
+ export function parseLine(line, source = 'cli') {
73
+ const trimmed = line.trim();
74
+ if (!trimmed) {
75
+ return null;
76
+ }
77
+ // JSON.parse is typed `any`, so a valid-JSON line that is not an object sails
78
+ // past the try/catch. A bare `null` line then throws on the first property
79
+ // access below, and that throw escapes tailFile and leaves the file's offset
80
+ // unadvanced forever, silently dropping every later record in it. Numbers,
81
+ // strings and arrays never threw (`(123).type` is just undefined); they are in
82
+ // the guard so the `as Record<string, unknown>` cast below is not a lie.
83
+ let parsed;
84
+ try {
85
+ parsed = JSON.parse(trimmed);
86
+ }
87
+ catch {
88
+ return null;
89
+ }
90
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
91
+ return null;
92
+ }
93
+ const o = parsed;
94
+ if (source === 'pi') {
95
+ return parsePiLine(o);
96
+ }
97
+ if (o.type !== 'assistant') {
98
+ return null;
99
+ }
100
+ const message = o.message;
101
+ if (!message || typeof message.usage !== 'object' || message.usage === null) {
102
+ return null;
103
+ }
104
+ const { uuid } = o;
105
+ if (typeof uuid !== 'string' || !uuid) {
106
+ return null;
107
+ }
108
+ const u = message.usage;
109
+ return {
110
+ cacheCreationTokens: cacheCreation(u),
111
+ cacheReadTokens: u.cache_read_input_tokens ?? 0,
112
+ cwd: str(o.cwd),
113
+ gitBranch: str(o.gitBranch),
114
+ inputTokens: u.input_tokens ?? 0,
115
+ messageId: str(message.id),
116
+ model: str(message.model),
117
+ outputTokens: u.output_tokens ?? 0,
118
+ requestId: str(o.requestId),
119
+ serviceTier: u.service_tier ?? null,
120
+ sessionId: str(o.sessionId),
121
+ source,
122
+ timestamp: validTimestamp(o.timestamp),
123
+ uuid,
124
+ version: str(o.version),
125
+ };
126
+ }
package/dist/paths.js ADDED
@@ -0,0 +1,48 @@
1
+ import { homedir } from 'node:os';
2
+ import { join } from 'node:path';
3
+ export function defaultProjectsDir() {
4
+ return process.env.USAGEFLEET_PROJECTS ?? join(homedir(), '.claude', 'projects');
5
+ }
6
+ /** Claude Desktop's Electron userData dir, per-OS. Mirrors the app's own
7
+ * `app.getPath("userData")` (= platform appData + the "Claude" product name),
8
+ * verified against the installed app's main bundle. */
9
+ function claudeDesktopUserData() {
10
+ if (process.platform === 'win32') {
11
+ const appData = process.env.APPDATA || join(homedir(), 'AppData', 'Roaming');
12
+ return join(appData, 'Claude');
13
+ }
14
+ if (process.platform === 'darwin') {
15
+ return join(homedir(), 'Library', 'Application Support', 'Claude');
16
+ }
17
+ // linux + other unix: respect XDG_CONFIG_HOME, else ~/.config
18
+ const xdg = process.env.XDG_CONFIG_HOME;
19
+ return join(xdg && xdg.length > 0 ? xdg : join(homedir(), '.config'), 'Claude');
20
+ }
21
+ /** Root under which Claude Desktop agent-mode (Cowork) sessions write their
22
+ * Claude-Code-format JSONL usage logs. The actual logs live deeper, under
23
+ * `<session>/.claude/projects/`; the collector filters to that subtree. */
24
+ export function defaultDesktopSessionsDir() {
25
+ return join(claudeDesktopUserData(), 'local-agent-mode-sessions');
26
+ }
27
+ /** Roots under which the pi coding agent writes its per-project session JSONLs
28
+ * (`<root>/<project>/<timestamp>_<uuid>.jsonl`). pi relocates them via
29
+ * PI_CODING_AGENT_SESSION_DIR / PI_CODING_AGENT_DIR — but those live in the
30
+ * user's shell, which a launchd/systemd service never inherits, so the default
31
+ * is every plausible root at once (missing ones scan to nothing). */
32
+ export function defaultPiSessionsDirs() {
33
+ const dirs = [join(homedir(), '.pi', 'agent', 'sessions')];
34
+ const session = process.env.PI_CODING_AGENT_SESSION_DIR;
35
+ const agent = process.env.PI_CODING_AGENT_DIR;
36
+ if (session) {
37
+ dirs.push(session);
38
+ }
39
+ if (agent) {
40
+ dirs.push(join(agent, 'sessions'));
41
+ }
42
+ return [...new Set(dirs)];
43
+ }
44
+ /** Claude Code's user settings file, where the prompt guard hook is registered.
45
+ * CLAUDE_CONFIG_DIR is Claude Code's own relocation knob. */
46
+ export function claudeSettingsPath() {
47
+ return join(process.env.CLAUDE_CONFIG_DIR ?? join(homedir(), '.claude'), 'settings.json');
48
+ }
@@ -0,0 +1,8 @@
1
+ // Overwritten by .github/workflows/release.yml with the version being published
2
+ // to npm. "dev" means "built locally", which disables self-update — a dev build
3
+ // must never be replaced by a published one behind your back.
4
+ // The annotation is load-bearing: without it the literal type would be 'dev'
5
+ // here and '1.2.3' in CI, so every `=== '1.2.59'` check compiles locally and
6
+ // fails the release build as a comparison with no overlap.
7
+ // oxlint-disable-next-line typescript/no-inferrable-types -- see above
8
+ export const RELEASE_VERSION = '1.2.59';
@@ -0,0 +1,24 @@
1
+ import { readdirSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ /** Recursively list all *.jsonl files under a directory. Returns [] if the
4
+ * directory is missing. */
5
+ export function listJsonlFiles(dir) {
6
+ const out = [];
7
+ let entries;
8
+ try {
9
+ entries = readdirSync(dir, { withFileTypes: true });
10
+ }
11
+ catch {
12
+ return out;
13
+ }
14
+ for (const e of entries) {
15
+ const full = join(dir, e.name);
16
+ if (e.isDirectory()) {
17
+ out.push(...listJsonlFiles(full));
18
+ }
19
+ else if (e.isFile() && e.name.endsWith('.jsonl')) {
20
+ out.push(full);
21
+ }
22
+ }
23
+ return out;
24
+ }