@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/README.md +284 -0
- package/dist/atomic-write.js +72 -0
- package/dist/claude-creds.js +161 -0
- package/dist/claude-limits.js +217 -0
- package/dist/collector.js +243 -0
- package/dist/config.js +74 -0
- package/dist/guard.js +59 -0
- package/dist/hook.js +103 -0
- package/dist/index.js +298 -0
- package/dist/notifier.js +112 -0
- package/dist/notify.js +88 -0
- package/dist/os.js +16 -0
- package/dist/parser.js +126 -0
- package/dist/paths.js +48 -0
- package/dist/release.js +8 -0
- package/dist/scanner.js +24 -0
- package/dist/service.js +524 -0
- package/dist/store.js +104 -0
- package/dist/tailer.js +65 -0
- package/dist/types.js +1 -0
- package/dist/ui.js +69 -0
- package/dist/update.js +100 -0
- package/dist/uploader.js +93 -0
- package/package.json +39 -0
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
|
+
}
|
package/dist/release.js
ADDED
|
@@ -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';
|
package/dist/scanner.js
ADDED
|
@@ -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
|
+
}
|