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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawsage",
3
- "version": "1.0.6",
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": ">=18"
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 { resolveSessionDir, parseAllSessions, getISOWeek, getMonth } from './parser.js';
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 sessionDir = resolveSessionDir(opts.path);
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(sessionDir, streamOpts);
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(sessionDir, streamOpts);
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 DEFAULT_SESSION_DIR = path.join(os.homedir(), '.openclaw', 'agents', 'main', 'sessions');
10
+ const DEFAULT_AGENTS_DIR = path.join(os.homedir(), '.openclaw', 'agents');
11
11
 
12
12
  /**
13
- * Resolve session directory path
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 customPath ? path.resolve(customPath) : DEFAULT_SESSION_DIR;
37
+ return resolveSessionDirs(customPath)[0];
17
38
  }
18
39
 
19
40
  /**
20
- * List all .jsonl session files in the directory (exclude .reset. files)
41
+ * List all .jsonl session files across one or more directories
21
42
  */
22
- export function listSessionFiles(dir) {
23
- try {
24
- return fs.readdirSync(dir)
25
- .filter(f => f.endsWith('.jsonl') && !f.includes('.reset.'))
26
- .map(f => path.join(dir, f));
27
- } catch (err) {
28
- throw new Error(`Cannot read session directory: ${dir}\n${err.message}`);
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: path.basename(filePath, '.jsonl'),
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(sessionDir, { since, until, timezone } = {}) {
131
- const files = listSessionFiles(sessionDir);
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;