@vibe-cafe/vibe-usage 0.7.11 → 0.7.13

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 CHANGED
@@ -59,6 +59,9 @@ npx @vibe-cafe/vibe-usage status # Show config & detected tools
59
59
  | Amp | `~/.local/share/amp/threads/` |
60
60
  | Droid | `~/.factory/sessions/` |
61
61
  | Hermes | `~/.hermes/state.db` + `~/.hermes/profiles/<name>/state.db` (SQLite, multi-profile) |
62
+ | Kiro | `~/Library/Application Support/Kiro/User/globalStorage/kiro.kiroagent/dev_data/devdata.sqlite` (SQLite, JSONL fallback; model name resolved from `.chat` timeline) |
63
+ | Cline | `<host>/User/globalStorage/saoudrizwan.claude-dev/{state/taskHistory.json,tasks/<id>/ui_messages.json}` (walks all VSCode-fork hosts: Code, Cursor, Windsurf, VSCodium, Trae, ...) |
64
+ | Roo Code | `<host>/User/globalStorage/rooveterinaryinc.roo-cline/{tasks/_index.json,tasks/<id>/{history_item,ui_messages}.json}` (walks all VSCode-fork hosts) |
62
65
 
63
66
  ## How It Works
64
67
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibe-cafe/vibe-usage",
3
- "version": "0.7.11",
3
+ "version": "0.7.13",
4
4
  "description": "Track your AI coding tool token usage and sync to vibecafe.ai",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,116 @@
1
+ import { readFileSync, statSync } from 'node:fs';
2
+ import { basename, join } from 'node:path';
3
+ import { homedir } from 'node:os';
4
+ import { aggregateToBuckets, extractSessions } from './index.js';
5
+
6
+ const EXTENSION_ID = 'saoudrizwan.claude-dev';
7
+
8
+ // VSCode-fork application names that may host extensions.
9
+ const HOSTS = ['Code', 'Cursor', 'Windsurf', 'VSCodium', 'Code - Insiders', 'Trae', 'Trae CN'];
10
+
11
+ function getHostRoots() {
12
+ const out = [];
13
+ if (process.platform === 'darwin') {
14
+ const base = join(homedir(), 'Library', 'Application Support');
15
+ for (const h of HOSTS) out.push(join(base, h));
16
+ } else if (process.platform === 'win32') {
17
+ const appData = process.env.APPDATA?.trim() || join(homedir(), 'AppData', 'Roaming');
18
+ for (const h of HOSTS) out.push(join(appData, h));
19
+ } else {
20
+ const xdg = process.env.XDG_CONFIG_HOME?.trim() || join(homedir(), '.config');
21
+ for (const h of HOSTS) out.push(join(xdg, h));
22
+ }
23
+ return out;
24
+ }
25
+
26
+ export function findClineExtensionDirs() {
27
+ const dirs = [];
28
+ for (const root of getHostRoots()) {
29
+ const ext = join(root, 'User', 'globalStorage', EXTENSION_ID);
30
+ try {
31
+ if (statSync(ext).isDirectory()) dirs.push(ext);
32
+ } catch {
33
+ // not installed in this host; skip
34
+ }
35
+ }
36
+ return dirs;
37
+ }
38
+
39
+ function readJsonSafe(path) {
40
+ try { return JSON.parse(readFileSync(path, 'utf-8')); } catch { return null; }
41
+ }
42
+
43
+ function projectFromPath(absPath) {
44
+ if (!absPath || typeof absPath !== 'string') return 'unknown';
45
+ const trimmed = absPath.replace(/[\\/]+$/, '');
46
+ const name = basename(trimmed);
47
+ return name || 'unknown';
48
+ }
49
+
50
+ export async function parse() {
51
+ const extDirs = findClineExtensionDirs();
52
+ if (extDirs.length === 0) return { buckets: [], sessions: [] };
53
+
54
+ const entries = [];
55
+ const events = [];
56
+
57
+ for (const extDir of extDirs) {
58
+ const history = readJsonSafe(join(extDir, 'state', 'taskHistory.json'));
59
+ if (!Array.isArray(history)) continue;
60
+
61
+ for (const item of history) {
62
+ try {
63
+ if (!item || typeof item !== 'object' || !item.id) continue;
64
+ const taskId = String(item.id);
65
+ const project = projectFromPath(item.cwdOnTaskInitialization || item.shadowGitConfigWorkTree);
66
+ const fallbackModel = (item.modelId && String(item.modelId).trim()) || 'cline-unknown';
67
+
68
+ const messages = readJsonSafe(join(extDir, 'tasks', taskId, 'ui_messages.json'));
69
+ if (!Array.isArray(messages)) continue;
70
+
71
+ for (const msg of messages) {
72
+ if (!msg || typeof msg !== 'object') continue;
73
+ const ts = Number(msg.ts);
74
+ if (!Number.isFinite(ts)) continue;
75
+ const timestamp = new Date(ts);
76
+
77
+ if (msg.type === 'say' && msg.say === 'api_req_started') {
78
+ let info = null;
79
+ try { info = JSON.parse(msg.text); } catch { /* skip */ }
80
+ if (!info) continue;
81
+
82
+ const inputTokens = Math.max(0, Number(info.tokensIn) || 0);
83
+ const outputTokens = Math.max(0, Number(info.tokensOut) || 0);
84
+ const cacheWrites = Math.max(0, Number(info.cacheWrites) || 0);
85
+ const cacheReads = Math.max(0, Number(info.cacheReads) || 0);
86
+ if (inputTokens + outputTokens + cacheWrites + cacheReads === 0) continue;
87
+
88
+ // Newer Cline embeds the model id directly on the api_req_started payload.
89
+ const model = (info.model && String(info.model).trim()) || fallbackModel;
90
+
91
+ // Bucket schema (matches Cursor's CSV semantics):
92
+ // inputTokens = non-cache input + cache-write tokens (both billed as input)
93
+ // cachedInputTokens = cache-read tokens (10% input rate)
94
+ entries.push({
95
+ source: 'cline',
96
+ model,
97
+ project,
98
+ timestamp,
99
+ inputTokens: inputTokens + cacheWrites,
100
+ outputTokens,
101
+ cachedInputTokens: cacheReads,
102
+ reasoningOutputTokens: 0,
103
+ });
104
+ events.push({ sessionId: taskId, source: 'cline', project, timestamp, role: 'assistant' });
105
+ } else if (msg.type === 'ask' || (msg.type === 'say' && msg.say === 'user_feedback')) {
106
+ events.push({ sessionId: taskId, source: 'cline', project, timestamp, role: 'user' });
107
+ }
108
+ }
109
+ } catch {
110
+ // Skip this task; keep going for the rest of the history.
111
+ }
112
+ }
113
+ }
114
+
115
+ return { buckets: aggregateToBuckets(entries), sessions: extractSessions(events) };
116
+ }
@@ -1,8 +1,10 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { parse as parseClaudeCode } from './claude-code.js';
3
+ import { parse as parseCline } from './cline.js';
3
4
  import { parse as parseCodex } from './codex.js';
4
5
  import { parse as parseCopilotCli } from './copilot-cli.js';
5
6
  import { parse as parseCursor } from './cursor.js';
7
+ import { parse as parseRooCode } from './roo-code.js';
6
8
  import { parse as parseGeminiCli } from './gemini-cli.js';
7
9
  import { parse as parseOpencode } from './opencode.js';
8
10
  import { parse as parseOpenclaw } from './openclaw.js';
@@ -12,13 +14,16 @@ import { parse as parseAmp } from './amp.js';
12
14
  import { parse as parseDroid } from './droid.js';
13
15
  import { parse as parseAntigravity } from './antigravity.js';
14
16
  import { parse as parseHermes } from './hermes.js';
17
+ import { parse as parseKiro } from './kiro.js';
15
18
  import { parse as parsePiCodingAgent } from './pi-coding-agent.js';
16
19
 
17
20
  export const parsers = {
18
21
  'claude-code': parseClaudeCode,
22
+ 'cline': parseCline,
19
23
  'codex': parseCodex,
20
24
  'copilot-cli': parseCopilotCli,
21
25
  'cursor': parseCursor,
26
+ 'roo-code': parseRooCode,
22
27
  'gemini-cli': parseGeminiCli,
23
28
  'opencode': parseOpencode,
24
29
  'openclaw': parseOpenclaw,
@@ -28,6 +33,7 @@ export const parsers = {
28
33
  'droid': parseDroid,
29
34
  'antigravity': parseAntigravity,
30
35
  'hermes': parseHermes,
36
+ 'kiro': parseKiro,
31
37
  'pi-coding-agent': parsePiCodingAgent,
32
38
  };
33
39
 
@@ -0,0 +1,238 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import {
3
+ copyFileSync,
4
+ existsSync,
5
+ mkdtempSync,
6
+ readFileSync,
7
+ readdirSync,
8
+ rmSync,
9
+ statSync,
10
+ } from 'node:fs';
11
+ import { join, resolve } from 'node:path';
12
+ import { homedir, tmpdir } from 'node:os';
13
+ import { aggregateToBuckets } from './index.js';
14
+
15
+ const KIROAGENT_RELATIVE = join('User', 'globalStorage', 'kiro.kiroagent');
16
+
17
+ function getDefaultBasePath() {
18
+ if (process.platform === 'darwin') {
19
+ return join(homedir(), 'Library', 'Application Support', 'Kiro', KIROAGENT_RELATIVE);
20
+ }
21
+ if (process.platform === 'win32') {
22
+ const appData = process.env.APPDATA?.trim() || join(homedir(), 'AppData', 'Roaming');
23
+ return join(appData, 'Kiro', KIROAGENT_RELATIVE);
24
+ }
25
+ const xdgConfigHome = process.env.XDG_CONFIG_HOME?.trim() || join(homedir(), '.config');
26
+ return join(xdgConfigHome, 'Kiro', KIROAGENT_RELATIVE);
27
+ }
28
+
29
+ export function getKiroBasePath() {
30
+ const explicit = process.env.KIRO_BASE_PATH?.trim();
31
+ if (explicit) {
32
+ const r = resolve(explicit);
33
+ return existsSync(r) ? r : null;
34
+ }
35
+ const def = getDefaultBasePath();
36
+ return existsSync(def) ? def : null;
37
+ }
38
+
39
+ function isLockError(err) {
40
+ return err && typeof err.message === 'string' && /database is locked/i.test(err.message);
41
+ }
42
+
43
+ function queryDb(dbPath, sql) {
44
+ const out = execFileSync('sqlite3', ['-json', dbPath, sql], {
45
+ encoding: 'utf-8',
46
+ maxBuffer: 100 * 1024 * 1024,
47
+ timeout: 30000,
48
+ });
49
+ const trimmed = out.trim();
50
+ if (!trimmed || trimmed === '[]') return [];
51
+ return JSON.parse(trimmed);
52
+ }
53
+
54
+ const TOKENS_SQL =
55
+ 'SELECT id, model, tokens_prompt, tokens_generated, timestamp ' +
56
+ 'FROM tokens_generated ' +
57
+ 'WHERE tokens_prompt > 0 OR tokens_generated > 0 ' +
58
+ 'ORDER BY id ASC';
59
+
60
+ function readDb(dbPath) {
61
+ try {
62
+ return queryDb(dbPath, TOKENS_SQL);
63
+ } catch (err) {
64
+ if (!isLockError(err)) throw err;
65
+ // Kiro app holds a write lock; snapshot WAL set to a temp dir and retry.
66
+ const snapshotDir = mkdtempSync(join(tmpdir(), 'vibe-usage-kiro-'));
67
+ const queryPath = join(snapshotDir, 'devdata.sqlite');
68
+ copyFileSync(dbPath, queryPath);
69
+ for (const suffix of ['-shm', '-wal']) {
70
+ const companion = `${dbPath}${suffix}`;
71
+ if (existsSync(companion)) copyFileSync(companion, `${queryPath}${suffix}`);
72
+ }
73
+ try {
74
+ return queryDb(queryPath, TOKENS_SQL);
75
+ } finally {
76
+ rmSync(snapshotDir, { recursive: true, force: true });
77
+ }
78
+ }
79
+ }
80
+
81
+ // JSONL fallback: tokens_generated.jsonl. Each line:
82
+ // {"model":"agent","provider":"kiro","promptTokens":N,"generatedTokens":N}
83
+ // No per-row timestamp — bucket all rows under the file mtime.
84
+ function readJsonl(jsonlPath) {
85
+ let raw;
86
+ try { raw = readFileSync(jsonlPath, 'utf-8'); } catch { return []; }
87
+ const lines = raw.split('\n').map(l => l.trim()).filter(Boolean);
88
+ if (lines.length === 0) return [];
89
+ let mtime;
90
+ try { mtime = statSync(jsonlPath).mtime; } catch { mtime = new Date(); }
91
+ const ts = mtime.toISOString().replace('T', ' ').replace('Z', '').slice(0, 19);
92
+ const rows = [];
93
+ for (let i = 0; i < lines.length; i++) {
94
+ try {
95
+ const obj = JSON.parse(lines[i]);
96
+ rows.push({
97
+ id: i + 1,
98
+ model: obj.model || 'agent',
99
+ tokens_prompt: obj.promptTokens || 0,
100
+ tokens_generated: obj.generatedTokens || 0,
101
+ timestamp: ts,
102
+ });
103
+ } catch {
104
+ // skip malformed lines
105
+ }
106
+ }
107
+ return rows;
108
+ }
109
+
110
+ // Walk workspace dirs under kiro.kiroagent/ and collect every .chat file's
111
+ // modelId + start/end window. Used to attribute each tokens_generated row to
112
+ // a real model (the SQLite `model` column is usually the literal "agent").
113
+ function buildModelTimeline(base) {
114
+ const timeline = [];
115
+ let entries;
116
+ try { entries = readdirSync(base, { withFileTypes: true }); } catch { return timeline; }
117
+ for (const entry of entries) {
118
+ if (!entry.isDirectory() || entry.name === 'dev_data') continue;
119
+ const dirPath = join(base, entry.name);
120
+ let files;
121
+ try {
122
+ files = readdirSync(dirPath).filter(f => f.endsWith('.chat'));
123
+ } catch {
124
+ continue;
125
+ }
126
+ for (const file of files) {
127
+ try {
128
+ const data = JSON.parse(readFileSync(join(dirPath, file), 'utf-8'));
129
+ const meta = data?.metadata;
130
+ if (!meta?.modelId || !meta?.startTime) continue;
131
+ const startMs = Number(meta.startTime);
132
+ const endMs = Number(meta.endTime || meta.startTime);
133
+ if (!Number.isFinite(startMs) || !Number.isFinite(endMs)) continue;
134
+ timeline.push({ startMs, endMs, model: String(meta.modelId) });
135
+ } catch {
136
+ // skip unreadable / malformed chat files
137
+ }
138
+ }
139
+ }
140
+ timeline.sort((a, b) => a.startMs - b.startMs);
141
+ return timeline;
142
+ }
143
+
144
+ function resolveModel(timeline, ts) {
145
+ if (!timeline.length || !ts) return null;
146
+ const t = ts.getTime();
147
+ if (!Number.isFinite(t)) return null;
148
+ let best = null;
149
+ let bestDist = Infinity;
150
+ for (const e of timeline) {
151
+ if (t >= e.startMs && t <= e.endMs) return e.model;
152
+ const d = Math.min(Math.abs(t - e.startMs), Math.abs(t - e.endMs));
153
+ if (d < bestDist) { bestDist = d; best = e.model; }
154
+ }
155
+ // 10-minute tolerance — beyond that, treat as no match.
156
+ return bestDist < 10 * 60 * 1000 ? best : null;
157
+ }
158
+
159
+ // "CLAUDE_SONNET_4_20250514_V1_0" -> "claude-sonnet-4"
160
+ function normalizeModelName(raw) {
161
+ if (!raw || typeof raw !== 'string') return null;
162
+ const trimmed = raw.trim();
163
+ if (!trimmed) return null;
164
+ if (trimmed === trimmed.toLowerCase() && trimmed.includes('-')) return trimmed;
165
+ const cleaned = trimmed
166
+ .replace(/_\d{8}_V\d+_\d+$/i, '')
167
+ .replace(/_V\d+$/i, '')
168
+ .toLowerCase()
169
+ .replace(/_/g, '-');
170
+ return cleaned || null;
171
+ }
172
+
173
+ function parseDbTimestamp(value) {
174
+ if (!value) return null;
175
+ // SQLite CURRENT_TIMESTAMP: "2026-01-09 15:25:30" (UTC, naive — append Z).
176
+ const d = new Date(String(value).trim().replace(' ', 'T') + 'Z');
177
+ return isNaN(d.getTime()) ? null : d;
178
+ }
179
+
180
+ export async function parse() {
181
+ const base = getKiroBasePath();
182
+ if (!base) return { buckets: [], sessions: [] };
183
+
184
+ const dbPath = join(base, 'dev_data', 'devdata.sqlite');
185
+ const jsonlPath = join(base, 'dev_data', 'tokens_generated.jsonl');
186
+
187
+ let rows;
188
+ try {
189
+ if (existsSync(dbPath)) {
190
+ rows = readDb(dbPath);
191
+ } else if (existsSync(jsonlPath)) {
192
+ rows = readJsonl(jsonlPath);
193
+ } else {
194
+ return { buckets: [], sessions: [] };
195
+ }
196
+ } catch (err) {
197
+ if (err && typeof err.message === 'string' && err.message.includes('ENOENT')) {
198
+ throw new Error('sqlite3 CLI not found. Install sqlite3 to sync Kiro data.');
199
+ }
200
+ throw err;
201
+ }
202
+
203
+ if (!rows.length) return { buckets: [], sessions: [] };
204
+
205
+ const timeline = buildModelTimeline(base);
206
+ const entries = [];
207
+ for (const row of rows) {
208
+ const inputTokens = Math.max(0, Number(row.tokens_prompt) || 0);
209
+ const outputTokens = Math.max(0, Number(row.tokens_generated) || 0);
210
+ if (inputTokens === 0 && outputTokens === 0) continue;
211
+ const timestamp = parseDbTimestamp(row.timestamp);
212
+ if (!timestamp) continue;
213
+
214
+ // Prefer the .chat timeline; fall back to the row's literal model (skip
215
+ // the placeholder "agent"); then "kiro-agent".
216
+ let model = normalizeModelName(resolveModel(timeline, timestamp));
217
+ if (!model) {
218
+ const literal = (row.model || '').trim();
219
+ if (literal && literal.toLowerCase() !== 'agent') {
220
+ model = normalizeModelName(literal);
221
+ }
222
+ }
223
+ if (!model) model = 'kiro-agent';
224
+
225
+ entries.push({
226
+ source: 'kiro',
227
+ model,
228
+ project: 'unknown',
229
+ timestamp,
230
+ inputTokens,
231
+ outputTokens,
232
+ cachedInputTokens: 0,
233
+ reasoningOutputTokens: 0,
234
+ });
235
+ }
236
+
237
+ return { buckets: aggregateToBuckets(entries), sessions: [] };
238
+ }
@@ -0,0 +1,133 @@
1
+ import { readFileSync, readdirSync, statSync } from 'node:fs';
2
+ import { basename, join } from 'node:path';
3
+ import { homedir } from 'node:os';
4
+ import { aggregateToBuckets, extractSessions } from './index.js';
5
+
6
+ const EXTENSION_ID = 'rooveterinaryinc.roo-cline';
7
+
8
+ const HOSTS = ['Code', 'Cursor', 'Windsurf', 'VSCodium', 'Code - Insiders', 'Trae', 'Trae CN'];
9
+
10
+ function getHostRoots() {
11
+ const out = [];
12
+ if (process.platform === 'darwin') {
13
+ const base = join(homedir(), 'Library', 'Application Support');
14
+ for (const h of HOSTS) out.push(join(base, h));
15
+ } else if (process.platform === 'win32') {
16
+ const appData = process.env.APPDATA?.trim() || join(homedir(), 'AppData', 'Roaming');
17
+ for (const h of HOSTS) out.push(join(appData, h));
18
+ } else {
19
+ const xdg = process.env.XDG_CONFIG_HOME?.trim() || join(homedir(), '.config');
20
+ for (const h of HOSTS) out.push(join(xdg, h));
21
+ }
22
+ return out;
23
+ }
24
+
25
+ export function findRooCodeExtensionDirs() {
26
+ const dirs = [];
27
+ for (const root of getHostRoots()) {
28
+ const ext = join(root, 'User', 'globalStorage', EXTENSION_ID);
29
+ try {
30
+ if (statSync(ext).isDirectory()) dirs.push(ext);
31
+ } catch {
32
+ // not installed in this host; skip
33
+ }
34
+ }
35
+ return dirs;
36
+ }
37
+
38
+ function readJsonSafe(path) {
39
+ try { return JSON.parse(readFileSync(path, 'utf-8')); } catch { return null; }
40
+ }
41
+
42
+ function projectFromPath(absPath) {
43
+ if (!absPath || typeof absPath !== 'string') return 'unknown';
44
+ const trimmed = absPath.replace(/[\\/]+$/, '');
45
+ const name = basename(trimmed);
46
+ return name || 'unknown';
47
+ }
48
+
49
+ // Read all HistoryItems from `_index.json` if present, else fall back to
50
+ // scanning per-task `history_item.json` files (Roo migrated to per-task
51
+ // files in 2025; the index is a cache).
52
+ function readHistoryItems(extDir) {
53
+ const tasksDir = join(extDir, 'tasks');
54
+ const indexPath = join(tasksDir, '_index.json');
55
+ const index = readJsonSafe(indexPath);
56
+ if (index && Array.isArray(index.entries)) return index.entries;
57
+
58
+ const items = [];
59
+ let names;
60
+ try { names = readdirSync(tasksDir, { withFileTypes: true }); } catch { return items; }
61
+ for (const entry of names) {
62
+ if (!entry.isDirectory() || entry.name.startsWith('_') || entry.name.startsWith('.')) continue;
63
+ const item = readJsonSafe(join(tasksDir, entry.name, 'history_item.json'));
64
+ if (item && typeof item === 'object') items.push(item);
65
+ }
66
+ return items;
67
+ }
68
+
69
+ export async function parse() {
70
+ const extDirs = findRooCodeExtensionDirs();
71
+ if (extDirs.length === 0) return { buckets: [], sessions: [] };
72
+
73
+ const entries = [];
74
+ const events = [];
75
+
76
+ for (const extDir of extDirs) {
77
+ const items = readHistoryItems(extDir);
78
+ if (!items.length) continue;
79
+
80
+ for (const item of items) {
81
+ try {
82
+ if (!item || typeof item !== 'object' || !item.id) continue;
83
+ const taskId = String(item.id);
84
+ const project = projectFromPath(item.workspace);
85
+ // Roo doesn't store modelId; the profile name (apiConfigName) is the
86
+ // best fallback — users typically name profiles after the model.
87
+ const fallbackModel = (item.apiConfigName && String(item.apiConfigName).trim()) || 'roo-unknown';
88
+
89
+ const messages = readJsonSafe(join(extDir, 'tasks', taskId, 'ui_messages.json'));
90
+ if (!Array.isArray(messages)) continue;
91
+
92
+ for (const msg of messages) {
93
+ if (!msg || typeof msg !== 'object') continue;
94
+ const ts = Number(msg.ts);
95
+ if (!Number.isFinite(ts)) continue;
96
+ const timestamp = new Date(ts);
97
+
98
+ if (msg.type === 'say' && msg.say === 'api_req_started') {
99
+ let info = null;
100
+ try { info = JSON.parse(msg.text); } catch { /* skip */ }
101
+ if (!info) continue;
102
+
103
+ const inputTokens = Math.max(0, Number(info.tokensIn) || 0);
104
+ const outputTokens = Math.max(0, Number(info.tokensOut) || 0);
105
+ const cacheWrites = Math.max(0, Number(info.cacheWrites) || 0);
106
+ const cacheReads = Math.max(0, Number(info.cacheReads) || 0);
107
+ if (inputTokens + outputTokens + cacheWrites + cacheReads === 0) continue;
108
+
109
+ const model = (info.model && String(info.model).trim()) || fallbackModel;
110
+
111
+ entries.push({
112
+ source: 'roo-code',
113
+ model,
114
+ project,
115
+ timestamp,
116
+ inputTokens: inputTokens + cacheWrites,
117
+ outputTokens,
118
+ cachedInputTokens: cacheReads,
119
+ reasoningOutputTokens: 0,
120
+ });
121
+ events.push({ sessionId: taskId, source: 'roo-code', project, timestamp, role: 'assistant' });
122
+ } else if (msg.type === 'ask' || (msg.type === 'say' && msg.say === 'user_feedback')) {
123
+ events.push({ sessionId: taskId, source: 'roo-code', project, timestamp, role: 'user' });
124
+ }
125
+ }
126
+ } catch {
127
+ // Skip this task; keep going for the rest of the history.
128
+ }
129
+ }
130
+ }
131
+
132
+ return { buckets: aggregateToBuckets(entries), sessions: extractSessions(events) };
133
+ }
package/src/sync.js CHANGED
@@ -89,6 +89,8 @@ export async function runSync({ throws = false, quiet = false } = {}) {
89
89
 
90
90
  let totalIngested = 0;
91
91
  let totalSessionsSynced = 0;
92
+ let totalDroppedBuckets = 0;
93
+ const droppedSources = new Set();
92
94
  const bucketBatches = Math.ceil(allBuckets.length / BATCH_SIZE);
93
95
  const sessionBatches = Math.ceil(allSessions.length / SESSION_BATCH_SIZE);
94
96
  const totalBatches = Math.max(bucketBatches, sessionBatches, 1);
@@ -108,6 +110,10 @@ export async function runSync({ throws = false, quiet = false } = {}) {
108
110
  }, batchSessions.length > 0 ? batchSessions : undefined);
109
111
  totalIngested += result.ingested ?? batch.length;
110
112
  totalSessionsSynced += result.sessions ?? 0;
113
+ if (result.dropped) {
114
+ totalDroppedBuckets += Number(result.dropped.buckets) || 0;
115
+ for (const s of result.dropped.unknownSources || []) droppedSources.add(s);
116
+ }
111
117
  }
112
118
 
113
119
  if (totalBatches > 1 || allBuckets.length > 0) {
@@ -117,6 +123,14 @@ export async function runSync({ throws = false, quiet = false } = {}) {
117
123
  if (totalSessionsSynced > 0) syncParts.push(`${totalSessionsSynced} sessions`);
118
124
  console.log(success(`已同步 ${syncParts.join(' · ')}`));
119
125
 
126
+ if (totalDroppedBuckets > 0) {
127
+ // Server doesn't (yet) recognize these source IDs — usually means the
128
+ // CLI is newer than the deployed vibe-cafe. Surface so the user knows
129
+ // the data wasn't lost on their end, just not stored upstream.
130
+ const sourcesList = Array.from(droppedSources).sort().join(', ');
131
+ console.log(dim(` ${totalDroppedBuckets} buckets dropped (服务端未收录的 source: ${sourcesList})`));
132
+ }
133
+
120
134
  if (!quiet && totalSessionsSynced > 0) {
121
135
  const totalActive = allSessions.reduce((s, x) => s + x.activeSeconds, 0);
122
136
  const totalDuration = allSessions.reduce((s, x) => s + x.durationSeconds, 0);
package/src/tools.js CHANGED
@@ -1,4 +1,4 @@
1
- import { existsSync, readdirSync } from 'node:fs';
1
+ import { existsSync, readdirSync, statSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { homedir } from 'node:os';
4
4
 
@@ -15,6 +15,53 @@ function getCursorStateDbPath() {
15
15
  return join(xdgConfigHome, 'Cursor', rel);
16
16
  }
17
17
 
18
+ function getKiroAgentPath() {
19
+ const rel = join('User', 'globalStorage', 'kiro.kiroagent');
20
+ if (process.platform === 'darwin') {
21
+ return join(homedir(), 'Library', 'Application Support', 'Kiro', rel);
22
+ }
23
+ if (process.platform === 'win32') {
24
+ const appData = process.env.APPDATA?.trim() || join(homedir(), 'AppData', 'Roaming');
25
+ return join(appData, 'Kiro', rel);
26
+ }
27
+ const xdgConfigHome = process.env.XDG_CONFIG_HOME?.trim() || join(homedir(), '.config');
28
+ return join(xdgConfigHome, 'Kiro', rel);
29
+ }
30
+
31
+ // VSCode-fork host directories where extensions like Cline / Roo Code live.
32
+ const VSCODE_HOSTS = ['Code', 'Cursor', 'Windsurf', 'VSCodium', 'Code - Insiders', 'Trae', 'Trae CN'];
33
+
34
+ function getVscodeHostRoots() {
35
+ const out = [];
36
+ if (process.platform === 'darwin') {
37
+ const base = join(homedir(), 'Library', 'Application Support');
38
+ for (const h of VSCODE_HOSTS) out.push(join(base, h));
39
+ } else if (process.platform === 'win32') {
40
+ const appData = process.env.APPDATA?.trim() || join(homedir(), 'AppData', 'Roaming');
41
+ for (const h of VSCODE_HOSTS) out.push(join(appData, h));
42
+ } else {
43
+ const xdg = process.env.XDG_CONFIG_HOME?.trim() || join(homedir(), '.config');
44
+ for (const h of VSCODE_HOSTS) out.push(join(xdg, h));
45
+ }
46
+ return out;
47
+ }
48
+
49
+ function findExtensionDirs(extensionId) {
50
+ const dirs = [];
51
+ for (const root of getVscodeHostRoots()) {
52
+ const ext = join(root, 'User', 'globalStorage', extensionId);
53
+ try {
54
+ if (statSync(ext).isDirectory()) dirs.push(ext);
55
+ } catch {
56
+ // not present in this host
57
+ }
58
+ }
59
+ return dirs;
60
+ }
61
+
62
+ const findClineDataDirs = () => findExtensionDirs('saoudrizwan.claude-dev');
63
+ const findRooCodeDataDirs = () => findExtensionDirs('rooveterinaryinc.roo-cline');
64
+
18
65
  /** Find all OpenClaw data roots: ~/.openclaw and ~/.openclaw-<profile> */
19
66
  function findOpenclawDataDirs() {
20
67
  const home = homedir();
@@ -44,6 +91,12 @@ export const TOOLS = [
44
91
  id: 'claude-code',
45
92
  dataDir: join(homedir(), '.claude', 'projects'),
46
93
  },
94
+ {
95
+ name: 'Cline',
96
+ id: 'cline',
97
+ dataDir: join(homedir(), 'Library', 'Application Support', 'Code', 'User', 'globalStorage', 'saoudrizwan.claude-dev'),
98
+ detectDataDirs: findClineDataDirs,
99
+ },
47
100
  {
48
101
  name: 'Codex CLI',
49
102
  id: 'codex',
@@ -105,6 +158,17 @@ export const TOOLS = [
105
158
  id: 'hermes',
106
159
  dataDir: join(homedir(), '.hermes', 'state.db'),
107
160
  },
161
+ {
162
+ name: 'Kiro',
163
+ id: 'kiro',
164
+ dataDir: getKiroAgentPath(),
165
+ },
166
+ {
167
+ name: 'Roo Code',
168
+ id: 'roo-code',
169
+ dataDir: join(homedir(), 'Library', 'Application Support', 'Code', 'User', 'globalStorage', 'rooveterinaryinc.roo-cline'),
170
+ detectDataDirs: findRooCodeDataDirs,
171
+ },
108
172
  ];
109
173
 
110
174
  export function detectInstalledTools() {