@vibe-cafe/vibe-usage 0.10.28 → 0.10.29
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 +2 -1
- package/package.json +1 -1
- package/src/cola-roots.js +16 -0
- package/src/daemon-service.js +2 -0
- package/src/hermes-roots.js +59 -0
- package/src/parsers/cola.js +17 -0
- package/src/parsers/hermes.js +14 -48
- package/src/parsers/index.js +2 -0
- package/src/parsers/pi-session-jsonl.js +44 -8
- package/src/tools.js +10 -1
package/README.md
CHANGED
|
@@ -64,6 +64,7 @@ npx @vibe-cafe/vibe-usage help --all # Full help (plain `help` shows the short
|
|
|
64
64
|
| Claude Code + Claude Desktop Code/Cowork | Claude Code data in `~/.claude/projects/` (tokens + sessions) and `~/.claude/transcripts/` (sessions only), plus Claude Desktop Cowork's per-session `.claude/projects/` directories. Also scans `$CLAUDE_CONFIG_DIR` and data-bearing `~/.claude-*` profiles. All variants use the existing `claude-code` source; the parser selects the most complete copy of each session so shared/copied transcripts are not counted twice. Logs are streamed and cache creation tokens are included in input usage. |
|
|
65
65
|
| Cindy | Per-owner SQLite ledgers in the two regional Electron user-data roots: macOS `~/Library/Application Support/{CindyGlobal,Cindy}/cindy-*.db`, Windows `%APPDATA%\{CindyGlobal,Cindy}\cindy-*.db`, Linux `${XDG_CONFIG_HOME:-~/.config}/{CindyGlobal,Cindy}/cindy-*.db` (fixture/relocation override: `VIBE_USAGE_CINDY_DIRS`). Cindy-launched Claude Code already writes ordinary `~/.claude` transcripts, so it remains attributed to **Claude Code** and is not read again. Cindy's otherwise-private Codex and Pi daily/model ledger rows augment the existing **Codex** and **pi** sources. Currency rows are summed and cache creation joins input; chat messages, credentials, costs, and owner ids are never selected. The ledger adds token buckets only, without project or session timing. |
|
|
66
66
|
| Codex CLI | `$CODEX_HOME/sessions/` and `$CODEX_HOME/archived_sessions/` (default `~/.codex`), plus an optional temporary `--extra-codex-home`, legacy `codexExtraHome`, or explicitly added Codex/Multica roots; a versioned local index avoids re-reading unchanged rollouts and reads only safe append tails for ordinary sessions, while fork/sub-agent replay matching, duplicate suppression, and live/archive/cross-root deduplication retain their existing semantics |
|
|
67
|
+
| Cola | `~/.cola/sessions/<scope>/*.jsonl` (or `$COLA_DATA_DIR/sessions/`), verified with Cola 1.4.4. Reads assistant token usage and session timing through the shared Pi reader; cache writes join input, cache reads remain separate, and reasoning is split from output. Copied transcripts with new session headers are deduplicated using the original record metadata and attributed to the earliest available session copy. Project names come from `cwd`, never channel/scope names. |
|
|
67
68
|
| Grok | `$GROK_HOME/sessions/<encoded-cwd>/<session-id>/` (default `~/.grok`) plus explicitly added Grok Homes; token usage from `updates.jsonl` `turn_completed.usage` (per-model `modelUsage`, cache reads, reasoning); project from `summary.json` cwd; copied sessions keep the more complete local record |
|
|
68
69
|
| GitHub Copilot CLI | `~/.copilot/session-state/*/events.jsonl` |
|
|
69
70
|
| CraftAgent | `~/.craft-agent/workspaces/*/sessions/*/.pi-sessions/*.jsonl`; honors `$CRAFT_AGENT_DIR` / `$CRAFTAGENT_DIR`; cache writes are included in input usage |
|
|
@@ -81,7 +82,7 @@ npx @vibe-cafe/vibe-usage help --all # Full help (plain `help` shows the short
|
|
|
81
82
|
| Amp | `~/.local/share/amp/threads/`; cache-creation tokens are included in input usage |
|
|
82
83
|
| Droid | `~/.factory/sessions/` |
|
|
83
84
|
| DeepSeek Harness | `$DSH_HOME/sessions/` (default `~/.dsh`, fixture/relocation override: `VIBE_USAGE_DSH_SESSIONS`). Reads V0–V3 logs, including `session.v3.jsonl.zstd` from DSH `0.1.5-alpha.2`, with multi-frame Zstandard support (Node ≥ 22.15 built-in, `zstd` CLI fallback) and plain JSONL support. Each session uses its highest `session[.vN].jsonl[.zstd]` generation once, so frozen pre-migration logs are not double-counted. Usage comes from `assistant/message`: cache writes join uncached input, cache reads remain separate, and reasoning is split out of inclusive output. Fork history uses V0/V1 `seedLength` or V2/V3's last `session/end-seed` tagged `inherited: true`, and is skipped only when the parent copy confirms it; missing parents retain the sole local history. Unknown versions warn and protect sync state. |
|
|
84
|
-
| Hermes
|
|
85
|
+
| Hermes (CLI / Desktop) | `<home>/state.db` + `<home>/profiles/<name>/state.db` (SQLite, multi-profile). Home: `$HERMES_HOME`, otherwise `~/.hermes` on macOS/Linux or `%LOCALAPPDATA%\hermes` on Windows (falls back to an existing `~/.hermes` only when the Windows native root is absent). Cache writes join input; reasoning is separated from inclusive output. Usage is currently a cumulative session total attributed to session start: a session spanning several days does **not** yet provide an accurate daily breakdown. |
|
|
85
86
|
| Kiro | Kiro CLI native event streams `~/.kiro/sessions/cli/*.jsonl` (estimated tokens from message text: input = prompt + tool results, output = reply + tool calls, reasoning = thinking, cacheRead = re-sent context; thinking-block signatures excluded). Falls back to `~/Library/Application Support/kiro-cli/data.sqlite3` / `~/.local/share/kiro-cli/data.sqlite3` + optional `~/.kiro_sessions/*.json` archives, then IDE `q-client.log` whole-credit deltas as `kiro-credits` (floored cumulative diff — the server stores token counts as bigint); legacy IDE `dev_data/devdata.sqlite` token telemetry is opt-in with `VIBE_USAGE_KIRO_LEGACY_TOKENS=1` |
|
|
86
87
|
| Cline | Standalone `~/.cline/` plus `<host>/User/globalStorage/saoudrizwan.claude-dev/` across VSCode-fork hosts; migrated copies are deduplicated and empty leftover extension stores no longer count as installed |
|
|
87
88
|
| Roo Code | `<host>/User/globalStorage/rooveterinaryinc.roo-cline/{tasks/_index.json,tasks/<id>/{history_item,ui_messages}.json}` (walks all VSCode-fork hosts) |
|
package/package.json
CHANGED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { statSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
|
|
5
|
+
export function getColaSessionsDir() {
|
|
6
|
+
return join(process.env.COLA_DATA_DIR ?? join(homedir(), '.cola'), 'sessions');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function findColaDataDirs() {
|
|
10
|
+
const dir = getColaSessionsDir();
|
|
11
|
+
try {
|
|
12
|
+
return statSync(dir).isDirectory() ? [dir] : [];
|
|
13
|
+
} catch {
|
|
14
|
+
return [];
|
|
15
|
+
}
|
|
16
|
+
}
|
package/src/daemon-service.js
CHANGED
|
@@ -115,6 +115,8 @@ function escapeXml(value) {
|
|
|
115
115
|
// launchd/systemd unit that inherits nothing, so anything the parsers read for
|
|
116
116
|
// discovery has to be captured into the unit at install time.
|
|
117
117
|
const PRESERVED_SERVICE_ENV = [
|
|
118
|
+
'COLA_DATA_DIR',
|
|
119
|
+
'HERMES_HOME',
|
|
118
120
|
'MCODE_HOME',
|
|
119
121
|
'MIMOCODE_HOME',
|
|
120
122
|
'MIMOCODE_DB',
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { readdirSync, statSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
|
|
5
|
+
// Match the Hermes CLI/Desktop layout. Windows installers use LOCALAPPDATA;
|
|
6
|
+
// Desktop falls back to an existing ~/.hermes only before that native root exists.
|
|
7
|
+
export function getHermesHome({ onError = () => {} } = {}) {
|
|
8
|
+
const explicit = process.env.HERMES_HOME?.trim();
|
|
9
|
+
if (explicit) return explicit;
|
|
10
|
+
|
|
11
|
+
const legacy = join(homedir(), '.hermes');
|
|
12
|
+
if (process.platform !== 'win32') return legacy;
|
|
13
|
+
|
|
14
|
+
const localAppData = process.env.LOCALAPPDATA?.trim() || join(homedir(), 'AppData', 'Local');
|
|
15
|
+
const native = join(localAppData, 'hermes');
|
|
16
|
+
return !statIfPresent(native, onError)?.isDirectory()
|
|
17
|
+
&& statIfPresent(legacy, onError)?.isDirectory() ? legacy : native;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function statIfPresent(path, onError) {
|
|
21
|
+
try {
|
|
22
|
+
return statSync(path);
|
|
23
|
+
} catch (err) {
|
|
24
|
+
if (err.code !== 'ENOENT') onError(err);
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Parsing must fail on unreadable stores: a partial result would allow sync
|
|
30
|
+
// to prune that profile's previous state. Detection alone is best-effort.
|
|
31
|
+
export function discoverHermesDatabases({ onError = err => { throw err; } } = {}) {
|
|
32
|
+
const home = getHermesHome({ onError });
|
|
33
|
+
const dbs = [];
|
|
34
|
+
|
|
35
|
+
const defaultDb = join(home, 'state.db');
|
|
36
|
+
if (statIfPresent(defaultDb, onError)) dbs.push({ path: defaultDb, profile: 'default' });
|
|
37
|
+
|
|
38
|
+
const profilesDir = join(home, 'profiles');
|
|
39
|
+
let entries;
|
|
40
|
+
try {
|
|
41
|
+
entries = readdirSync(profilesDir, { withFileTypes: true });
|
|
42
|
+
} catch (err) {
|
|
43
|
+
if (err.code !== 'ENOENT') onError(err);
|
|
44
|
+
return dbs;
|
|
45
|
+
}
|
|
46
|
+
for (const entry of entries) {
|
|
47
|
+
if (!entry.isDirectory()) continue;
|
|
48
|
+
const profileDb = join(profilesDir, entry.name, 'state.db');
|
|
49
|
+
if (statIfPresent(profileDb, onError)?.isFile()) {
|
|
50
|
+
dbs.push({ path: profileDb, profile: entry.name });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return dbs;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function findHermesDataDirs() {
|
|
58
|
+
return discoverHermesDatabases({ onError: () => {} }).map(db => db.path);
|
|
59
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { getColaSessionsDir } from '../cola-roots.js';
|
|
2
|
+
import { parsePiSessionJsonl } from './pi-session-jsonl.js';
|
|
3
|
+
|
|
4
|
+
/** Cola 1.4.4 writes Pi-compatible transcripts under sessions/<scope>/. */
|
|
5
|
+
export async function parse() {
|
|
6
|
+
const result = await parsePiSessionJsonl({
|
|
7
|
+
source: 'cola',
|
|
8
|
+
sessionsDirs: [getColaSessionsDir()],
|
|
9
|
+
// Scope slugs may identify channels or people, not projects. The session
|
|
10
|
+
// header's cwd supplies a project when present; otherwise keep unknown.
|
|
11
|
+
projectFromPath: () => 'unknown',
|
|
12
|
+
deduplicateCopiedSessions: true,
|
|
13
|
+
});
|
|
14
|
+
// A missing part of the store must not overwrite a complete uploaded bucket
|
|
15
|
+
// with a partial sum, even when several projects are hidden behind unknown.
|
|
16
|
+
return result.skipped ? { ...result, buckets: [], sessions: [] } : result;
|
|
17
|
+
}
|
package/src/parsers/hermes.js
CHANGED
|
@@ -1,24 +1,22 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { join } from 'node:path';
|
|
3
|
-
import { homedir } from 'node:os';
|
|
1
|
+
import { discoverHermesDatabases } from '../hermes-roots.js';
|
|
4
2
|
import { aggregateToBuckets, extractSessions } from './aggregate.js';
|
|
5
3
|
import { toCount } from './fs-utils.js';
|
|
6
4
|
import { queryDbJson, sqliteUnavailableError, isSqliteUnavailableError } from './sqlite.js';
|
|
7
5
|
|
|
8
|
-
const HERMES_HOME = process.env.HERMES_HOME || join(homedir(), '.hermes');
|
|
9
|
-
|
|
10
6
|
/**
|
|
11
7
|
* Parse Hermes Agent usage data from its SQLite databases.
|
|
12
8
|
*
|
|
13
9
|
* Hermes supports multiple profiles — the default profile lives at
|
|
14
|
-
*
|
|
10
|
+
* <home>/state.db, while named profiles live at <home>/profiles/<name>/state.db.
|
|
11
|
+
* The home is shared by CLI/Desktop: ~/.hermes on macOS/Linux, LOCALAPPDATA/hermes
|
|
12
|
+
* on Windows, or an explicit HERMES_HOME.
|
|
15
13
|
* Each profile is an independent HERMES_HOME with its own state.db, so we scan all of them.
|
|
16
14
|
*
|
|
17
15
|
* Token buckets come from the sessions table (cumulative per-session totals).
|
|
18
16
|
* Session timing comes from the messages table (per-message role + timestamp).
|
|
19
17
|
*/
|
|
20
18
|
export async function parse() {
|
|
21
|
-
const dbs =
|
|
19
|
+
const dbs = discoverHermesDatabases();
|
|
22
20
|
if (dbs.length === 0) return { buckets: [], sessions: [] };
|
|
23
21
|
|
|
24
22
|
const entries = [];
|
|
@@ -68,19 +66,15 @@ export async function parse() {
|
|
|
68
66
|
});
|
|
69
67
|
}
|
|
70
68
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
} catch {
|
|
81
|
-
// Messages query failed for this profile — skip its session events
|
|
82
|
-
continue;
|
|
83
|
-
}
|
|
69
|
+
// A failed query is not an empty session history. Let sync protect this
|
|
70
|
+
// source's previous state instead of uploading/pruning a partial result.
|
|
71
|
+
const messageRows = queryDb(dbPath, `SELECT
|
|
72
|
+
session_id as sessionId,
|
|
73
|
+
role,
|
|
74
|
+
timestamp
|
|
75
|
+
FROM messages
|
|
76
|
+
WHERE role IN ('user', 'assistant')
|
|
77
|
+
ORDER BY timestamp`);
|
|
84
78
|
|
|
85
79
|
for (const row of messageRows) {
|
|
86
80
|
const timestamp = new Date(row.timestamp * 1000);
|
|
@@ -99,34 +93,6 @@ export async function parse() {
|
|
|
99
93
|
return { buckets: aggregateToBuckets(entries), sessions: extractSessions(sessionEvents) };
|
|
100
94
|
}
|
|
101
95
|
|
|
102
|
-
function discoverDbPaths(home) {
|
|
103
|
-
const dbs = [];
|
|
104
|
-
|
|
105
|
-
const defaultDb = join(home, 'state.db');
|
|
106
|
-
if (existsSync(defaultDb)) dbs.push({ path: defaultDb, profile: 'default' });
|
|
107
|
-
|
|
108
|
-
const profilesDir = join(home, 'profiles');
|
|
109
|
-
if (existsSync(profilesDir)) {
|
|
110
|
-
let entries;
|
|
111
|
-
try {
|
|
112
|
-
entries = readdirSync(profilesDir, { withFileTypes: true });
|
|
113
|
-
} catch {
|
|
114
|
-
return dbs;
|
|
115
|
-
}
|
|
116
|
-
for (const entry of entries) {
|
|
117
|
-
if (!entry.isDirectory()) continue;
|
|
118
|
-
const profileDb = join(profilesDir, entry.name, 'state.db');
|
|
119
|
-
try {
|
|
120
|
-
if (statSync(profileDb).isFile()) dbs.push({ path: profileDb, profile: entry.name });
|
|
121
|
-
} catch {
|
|
122
|
-
// missing or unreadable — skip
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
return dbs;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
96
|
function queryDb(dbPath, sql) {
|
|
131
97
|
return queryDbJson(dbPath, sql);
|
|
132
98
|
}
|
package/src/parsers/index.js
CHANGED
|
@@ -23,6 +23,7 @@ import { parse as parseKiro } from './kiro.js';
|
|
|
23
23
|
import { parse as parseMcode } from './mcode.js';
|
|
24
24
|
import { parse as parseMimocode } from './mimocode.js';
|
|
25
25
|
import { parse as parsePiCodingAgent } from './pi-coding-agent.js';
|
|
26
|
+
import { parse as parseCola } from './cola.js';
|
|
26
27
|
import { parse as parseZcode } from './zcode.js';
|
|
27
28
|
import { parse as parseTraeCli } from './trae-cli.js';
|
|
28
29
|
import { parse as parseWorkbuddy } from './workbuddy.js';
|
|
@@ -31,6 +32,7 @@ import { parseQoder, parseQoderCn } from './qoder.js';
|
|
|
31
32
|
export const parsers = {
|
|
32
33
|
'claude-code': parseClaudeCode,
|
|
33
34
|
'codex': parseCodex,
|
|
35
|
+
'cola': parseCola,
|
|
34
36
|
'grok': parseGrok,
|
|
35
37
|
'copilot-cli': parseCopilotCli,
|
|
36
38
|
'craft-agent': parseCraftAgent,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readdirSync, readFileSync, realpathSync } from 'node:fs';
|
|
2
2
|
import { basename, join, relative } from 'node:path';
|
|
3
3
|
import { aggregateToBuckets, extractSessions } from './aggregate.js';
|
|
4
4
|
import { projectFromCwd, toCount } from './fs-utils.js';
|
|
@@ -11,12 +11,13 @@ function warn(ctx, message) {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
function findJsonlFiles(dir, includeFile, ctx) {
|
|
14
|
-
if (!existsSync(dir)) return [];
|
|
15
14
|
let children;
|
|
16
15
|
try {
|
|
17
16
|
children = readdirSync(dir, { withFileTypes: true });
|
|
18
17
|
} catch (err) {
|
|
19
|
-
|
|
18
|
+
if (err.code !== 'ENOENT') {
|
|
19
|
+
warn(ctx, `${ctx.source}: cannot read directory ${dir}: ${err.message}`);
|
|
20
|
+
}
|
|
20
21
|
return [];
|
|
21
22
|
}
|
|
22
23
|
|
|
@@ -56,6 +57,7 @@ export async function parsePiSessionJsonl({
|
|
|
56
57
|
sessionsDirs,
|
|
57
58
|
includeFile = () => true,
|
|
58
59
|
projectFromPath = projectFromFirstDir,
|
|
60
|
+
deduplicateCopiedSessions = false,
|
|
59
61
|
}) {
|
|
60
62
|
const ctx = { source, warnings: [], incomplete: false };
|
|
61
63
|
const entriesById = new Map();
|
|
@@ -63,6 +65,7 @@ export async function parsePiSessionJsonl({
|
|
|
63
65
|
const eventsById = new Map();
|
|
64
66
|
const anonymousEvents = [];
|
|
65
67
|
const seenFiles = new Set();
|
|
68
|
+
const recordOwners = new Map();
|
|
66
69
|
|
|
67
70
|
for (const sessionsDir of sessionsDirs) {
|
|
68
71
|
for (const filePath of findJsonlFiles(sessionsDir, includeFile, ctx)) {
|
|
@@ -80,6 +83,8 @@ export async function parsePiSessionJsonl({
|
|
|
80
83
|
|
|
81
84
|
let sessionId = basename(filePath, '.jsonl');
|
|
82
85
|
let project = projectFromPath(filePath, sessionsDir) || 'unknown';
|
|
86
|
+
let sessionStartedAt = Infinity;
|
|
87
|
+
let seenHeader = false;
|
|
83
88
|
|
|
84
89
|
for (const line of content.split('\n')) {
|
|
85
90
|
if (!line.trim()) continue;
|
|
@@ -89,18 +94,40 @@ export async function parsePiSessionJsonl({
|
|
|
89
94
|
} catch {
|
|
90
95
|
continue;
|
|
91
96
|
}
|
|
97
|
+
if (!obj || typeof obj !== 'object') continue;
|
|
92
98
|
|
|
93
99
|
if (obj.type === 'session') {
|
|
100
|
+
if (deduplicateCopiedSessions && seenHeader) continue;
|
|
101
|
+
seenHeader = true;
|
|
94
102
|
if (obj.id) sessionId = String(obj.id);
|
|
95
103
|
if (obj.cwd) project = projectFromCwd(obj.cwd);
|
|
104
|
+
const startedAt = new Date(obj.timestamp).getTime();
|
|
105
|
+
sessionStartedAt = Number.isFinite(startedAt) ? startedAt : Infinity;
|
|
96
106
|
continue;
|
|
97
107
|
}
|
|
98
108
|
if (obj.type !== 'message' || !obj.message) continue;
|
|
99
109
|
|
|
100
110
|
const message = obj.message;
|
|
101
|
-
const
|
|
111
|
+
const rawTimestamp = obj.timestamp || message.timestamp;
|
|
112
|
+
const timestamp = new Date(rawTimestamp || 0);
|
|
102
113
|
if (Number.isNaN(timestamp.getTime())) continue;
|
|
103
|
-
const
|
|
114
|
+
const model = message.model || message.modelId || obj.model || obj.modelId || 'unknown';
|
|
115
|
+
// Cola copies a transcript with a new session header but unchanged
|
|
116
|
+
// records. Its short message ids are only unique within a session, so
|
|
117
|
+
// cross-session dedup also requires time, parent, role, and model.
|
|
118
|
+
// All existing Pi-family callers retain sessionId:id identities.
|
|
119
|
+
const recordId = !obj.id ? null : deduplicateCopiedSessions && rawTimestamp
|
|
120
|
+
? JSON.stringify([obj.id, timestamp.getTime(), obj.parentId ?? null, message.role, model])
|
|
121
|
+
: `${sessionId}:${obj.id}`;
|
|
122
|
+
|
|
123
|
+
if (deduplicateCopiedSessions && recordId) {
|
|
124
|
+
const owner = recordOwners.get(recordId);
|
|
125
|
+
if (!owner || sessionStartedAt < owner.startedAt
|
|
126
|
+
|| (sessionStartedAt === owner.startedAt && sessionId < owner.sessionId)
|
|
127
|
+
|| (sessionStartedAt === owner.startedAt && sessionId === owner.sessionId && canonical < owner.filePath)) {
|
|
128
|
+
recordOwners.set(recordId, { sessionId, project, startedAt: sessionStartedAt, filePath: canonical });
|
|
129
|
+
}
|
|
130
|
+
}
|
|
104
131
|
|
|
105
132
|
if (message.role === 'user' || message.role === 'assistant' || message.role === 'toolResult') {
|
|
106
133
|
const event = {
|
|
@@ -130,7 +157,7 @@ export async function parsePiSessionJsonl({
|
|
|
130
157
|
|
|
131
158
|
const entry = {
|
|
132
159
|
source,
|
|
133
|
-
model
|
|
160
|
+
model,
|
|
134
161
|
project,
|
|
135
162
|
timestamp,
|
|
136
163
|
inputTokens,
|
|
@@ -150,9 +177,18 @@ export async function parsePiSessionJsonl({
|
|
|
150
177
|
|
|
151
178
|
const entries = [
|
|
152
179
|
...anonymousEntries,
|
|
153
|
-
...[...entriesById
|
|
180
|
+
...[...entriesById].map(([id, { entry }]) => {
|
|
181
|
+
const owner = recordOwners.get(id);
|
|
182
|
+
return owner ? { ...entry, project: owner.project } : entry;
|
|
183
|
+
}),
|
|
184
|
+
];
|
|
185
|
+
const events = [
|
|
186
|
+
...anonymousEvents,
|
|
187
|
+
...[...eventsById].map(([id, event]) => {
|
|
188
|
+
const owner = recordOwners.get(id);
|
|
189
|
+
return owner ? { ...event, sessionId: owner.sessionId, project: owner.project } : event;
|
|
190
|
+
}),
|
|
154
191
|
];
|
|
155
|
-
const events = [...anonymousEvents, ...eventsById.values()];
|
|
156
192
|
return {
|
|
157
193
|
buckets: aggregateToBuckets(entries),
|
|
158
194
|
sessions: extractSessions(events),
|
package/src/tools.js
CHANGED
|
@@ -11,7 +11,9 @@ import {
|
|
|
11
11
|
grokSessionsDir,
|
|
12
12
|
} from './extra-roots.js';
|
|
13
13
|
import { findClineDataDirs } from './cline-roots.js';
|
|
14
|
+
import { findColaDataDirs, getColaSessionsDir } from './cola-roots.js';
|
|
14
15
|
import { findCraftDataDirs } from './craft-roots.js';
|
|
16
|
+
import { findHermesDataDirs, getHermesHome } from './hermes-roots.js';
|
|
15
17
|
import { findOmpDataDirs, findPiDataDirs } from './pi-roots.js';
|
|
16
18
|
import { findQoderDataDirs, getQoderProjectsDir } from './qoder-roots.js';
|
|
17
19
|
import { findWorkbuddyDataDirs } from './workbuddy-roots.js';
|
|
@@ -266,6 +268,12 @@ export const TOOLS = [
|
|
|
266
268
|
findCodexDataDirs(codexExtraHome, extraRootList(extraRoots?.codex))
|
|
267
269
|
),
|
|
268
270
|
},
|
|
271
|
+
{
|
|
272
|
+
name: 'Cola',
|
|
273
|
+
id: 'cola',
|
|
274
|
+
dataDir: getColaSessionsDir(),
|
|
275
|
+
detectDataDirs: findColaDataDirs,
|
|
276
|
+
},
|
|
269
277
|
{
|
|
270
278
|
name: 'Grok',
|
|
271
279
|
id: 'grok',
|
|
@@ -396,7 +404,8 @@ export const TOOLS = [
|
|
|
396
404
|
{
|
|
397
405
|
name: 'Hermes',
|
|
398
406
|
id: 'hermes',
|
|
399
|
-
dataDir: join(
|
|
407
|
+
dataDir: join(getHermesHome(), 'state.db'),
|
|
408
|
+
detectDataDirs: findHermesDataDirs,
|
|
400
409
|
},
|
|
401
410
|
{
|
|
402
411
|
name: 'Kiro',
|