clawsage 1.0.6 → 1.0.8
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/package.json +2 -2
- package/src/cli.js +4 -4
- package/src/parser.js +48 -14
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clawsage",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.8",
|
|
4
4
|
"description": "CLI tool for analyzing OpenClaw token usage and costs from local session logs",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -30,6 +30,6 @@
|
|
|
30
30
|
},
|
|
31
31
|
"homepage": "https://github.com/its-clawdia/clawsage#readme",
|
|
32
32
|
"engines": {
|
|
33
|
-
"node": ">=
|
|
33
|
+
"node": ">=20"
|
|
34
34
|
}
|
|
35
35
|
}
|
package/src/cli.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* cli.js — ocusage CLI entry point
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import { resolveSessionDirs, parseAllSessions, getISOWeek, getMonth } from './parser.js';
|
|
7
7
|
import { aggregateByPeriod, aggregateBySessions } from './aggregate.js';
|
|
8
8
|
import { printPeriodReport, printSessionReport } from './format.js';
|
|
9
9
|
|
|
@@ -117,7 +117,7 @@ async function main() {
|
|
|
117
117
|
process.exit(0);
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
-
const
|
|
120
|
+
const sessionDirs = resolveSessionDirs(opts.path);
|
|
121
121
|
|
|
122
122
|
const streamOpts = {
|
|
123
123
|
since: opts.since,
|
|
@@ -129,7 +129,7 @@ async function main() {
|
|
|
129
129
|
let data;
|
|
130
130
|
|
|
131
131
|
if (opts.command === 'session') {
|
|
132
|
-
const sessionStream = parseAllSessions(
|
|
132
|
+
const sessionStream = parseAllSessions(sessionDirs, streamOpts);
|
|
133
133
|
data = await aggregateBySessions(sessionStream, {
|
|
134
134
|
breakdown: opts.breakdown,
|
|
135
135
|
timezone: opts.timezone,
|
|
@@ -142,7 +142,7 @@ async function main() {
|
|
|
142
142
|
monthly: (date) => date.slice(0, 7),
|
|
143
143
|
}[opts.command] || ((date) => date);
|
|
144
144
|
|
|
145
|
-
const sessionStream = parseAllSessions(
|
|
145
|
+
const sessionStream = parseAllSessions(sessionDirs, streamOpts);
|
|
146
146
|
data = await aggregateByPeriod(sessionStream, keyFn, {
|
|
147
147
|
breakdown: opts.breakdown,
|
|
148
148
|
timezone: opts.timezone,
|
package/src/parser.js
CHANGED
|
@@ -7,26 +7,56 @@ import path from 'path';
|
|
|
7
7
|
import readline from 'readline';
|
|
8
8
|
import os from 'os';
|
|
9
9
|
|
|
10
|
-
const
|
|
10
|
+
const DEFAULT_AGENTS_DIR = path.join(os.homedir(), '.openclaw', 'agents');
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
* Resolve session
|
|
13
|
+
* Resolve session directories.
|
|
14
|
+
* If a custom path is given, use it as a single directory.
|
|
15
|
+
* Otherwise, scan all agent dirs under ~/.openclaw/agents/{agent}/sessions/
|
|
14
16
|
*/
|
|
17
|
+
export function resolveSessionDirs(customPath) {
|
|
18
|
+
if (customPath) return [path.resolve(customPath)];
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const agents = fs.readdirSync(DEFAULT_AGENTS_DIR);
|
|
22
|
+
const dirs = [];
|
|
23
|
+
for (const agent of agents) {
|
|
24
|
+
const sessDir = path.join(DEFAULT_AGENTS_DIR, agent, 'sessions');
|
|
25
|
+
if (fs.existsSync(sessDir) && fs.statSync(sessDir).isDirectory()) {
|
|
26
|
+
dirs.push(sessDir);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return dirs.length > 0 ? dirs : [path.join(DEFAULT_AGENTS_DIR, 'main', 'sessions')];
|
|
30
|
+
} catch {
|
|
31
|
+
return [path.join(DEFAULT_AGENTS_DIR, 'main', 'sessions')];
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Back-compat alias
|
|
15
36
|
export function resolveSessionDir(customPath) {
|
|
16
|
-
return
|
|
37
|
+
return resolveSessionDirs(customPath)[0];
|
|
17
38
|
}
|
|
18
39
|
|
|
19
40
|
/**
|
|
20
|
-
* List all .jsonl session files
|
|
41
|
+
* List all .jsonl session files across one or more directories
|
|
21
42
|
*/
|
|
22
|
-
export function listSessionFiles(
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
43
|
+
export function listSessionFiles(dirs) {
|
|
44
|
+
if (!Array.isArray(dirs)) dirs = [dirs];
|
|
45
|
+
const files = [];
|
|
46
|
+
for (const dir of dirs) {
|
|
47
|
+
try {
|
|
48
|
+
const entries = fs.readdirSync(dir)
|
|
49
|
+
.filter(f => f.includes('.jsonl') && !f.endsWith('.lock'))
|
|
50
|
+
.map(f => path.join(dir, f));
|
|
51
|
+
files.push(...entries);
|
|
52
|
+
} catch {
|
|
53
|
+
// skip unreadable dirs
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (files.length === 0) {
|
|
57
|
+
throw new Error(`No session files found in: ${dirs.join(', ')}`);
|
|
29
58
|
}
|
|
59
|
+
return files;
|
|
30
60
|
}
|
|
31
61
|
|
|
32
62
|
/**
|
|
@@ -34,8 +64,12 @@ export function listSessionFiles(dir) {
|
|
|
34
64
|
* Returns: { id, timestamp, date, models, messages: [{timestamp, model, usage}] }
|
|
35
65
|
*/
|
|
36
66
|
export async function parseSessionFile(filePath) {
|
|
67
|
+
// Handle both regular (.jsonl) and reset (.jsonl.reset.<timestamp>) files
|
|
68
|
+
const basename = path.basename(filePath);
|
|
69
|
+
const id = basename.replace(/\.jsonl(?:\.(?:reset|deleted)\..+)?$/, '');
|
|
70
|
+
|
|
37
71
|
const session = {
|
|
38
|
-
id
|
|
72
|
+
id,
|
|
39
73
|
filePath,
|
|
40
74
|
timestamp: null,
|
|
41
75
|
date: null,
|
|
@@ -127,8 +161,8 @@ export async function parseSessionFile(filePath) {
|
|
|
127
161
|
* Parse all session files in parallel (with concurrency limit)
|
|
128
162
|
* Yields results as they complete
|
|
129
163
|
*/
|
|
130
|
-
export async function* parseAllSessions(
|
|
131
|
-
const files = listSessionFiles(
|
|
164
|
+
export async function* parseAllSessions(sessionDirs, { since, until, timezone } = {}) {
|
|
165
|
+
const files = listSessionFiles(sessionDirs);
|
|
132
166
|
|
|
133
167
|
// Process in batches of 10 for parallelism
|
|
134
168
|
const BATCH = 10;
|