agentlytics 0.1.14 → 0.1.16
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 +5 -2
- package/editors/antigravity.js +507 -0
- package/editors/claude.js +100 -1
- package/editors/codex.js +61 -0
- package/editors/copilot.js +68 -1
- package/editors/cursor.js +73 -1
- package/editors/index.js +23 -2
- package/editors/kiro.js +296 -0
- package/editors/opencode.js +146 -67
- package/editors/vscode.js +70 -1
- package/editors/windsurf.js +289 -61
- package/editors/zed.js +42 -19
- package/package.json +1 -1
- package/server.js +10 -0
- package/ui/src/App.jsx +4 -1
- package/ui/src/components/EditorIcon.jsx +2 -0
- package/ui/src/lib/api.js +5 -0
- package/ui/src/lib/constants.js +2 -0
- package/ui/src/pages/Subscriptions.jsx +413 -0
package/editors/codex.js
CHANGED
|
@@ -435,6 +435,66 @@ function safeParseJson(value) {
|
|
|
435
435
|
}
|
|
436
436
|
}
|
|
437
437
|
|
|
438
|
+
// ============================================================
|
|
439
|
+
// Usage / quota data from Codex auth.json JWT
|
|
440
|
+
// ============================================================
|
|
441
|
+
|
|
442
|
+
function getCodexAuth() {
|
|
443
|
+
const authPath = path.join(
|
|
444
|
+
process.env.CODEX_HOME && process.env.CODEX_HOME.trim()
|
|
445
|
+
? path.resolve(process.env.CODEX_HOME.trim())
|
|
446
|
+
: DEFAULT_CODEX_HOME,
|
|
447
|
+
'auth.json'
|
|
448
|
+
);
|
|
449
|
+
try {
|
|
450
|
+
return JSON.parse(fs.readFileSync(authPath, 'utf-8'));
|
|
451
|
+
} catch { return null; }
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function decodeJwtPayload(token) {
|
|
455
|
+
if (!token) return null;
|
|
456
|
+
try {
|
|
457
|
+
const parts = token.split('.');
|
|
458
|
+
if (parts.length < 2) return null;
|
|
459
|
+
let payload = parts[1];
|
|
460
|
+
// Fix base64url padding
|
|
461
|
+
payload += '='.repeat((4 - payload.length % 4) % 4);
|
|
462
|
+
const decoded = Buffer.from(payload, 'base64').toString('utf-8');
|
|
463
|
+
return JSON.parse(decoded);
|
|
464
|
+
} catch { return null; }
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
async function getUsage() {
|
|
468
|
+
const auth = getCodexAuth();
|
|
469
|
+
if (!auth || !auth.tokens) return null;
|
|
470
|
+
|
|
471
|
+
const idPayload = decodeJwtPayload(auth.tokens.id_token);
|
|
472
|
+
const accessPayload = decodeJwtPayload(auth.tokens.access_token);
|
|
473
|
+
|
|
474
|
+
const authClaims = idPayload?.['https://api.openai.com/auth'] || accessPayload?.['https://api.openai.com/auth'] || {};
|
|
475
|
+
const profileClaims = idPayload?.['https://api.openai.com/profile'] || accessPayload?.['https://api.openai.com/profile'] || {};
|
|
476
|
+
|
|
477
|
+
const planType = authClaims.chatgpt_plan_type || null;
|
|
478
|
+
const email = profileClaims.email || null;
|
|
479
|
+
const subscriptionStart = authClaims.chatgpt_subscription_active_start || null;
|
|
480
|
+
const subscriptionEnd = authClaims.chatgpt_subscription_active_until || null;
|
|
481
|
+
|
|
482
|
+
if (!planType && !email) return null;
|
|
483
|
+
|
|
484
|
+
return {
|
|
485
|
+
source: 'codex',
|
|
486
|
+
plan: {
|
|
487
|
+
name: planType,
|
|
488
|
+
subscriptionStart,
|
|
489
|
+
subscriptionEnd,
|
|
490
|
+
},
|
|
491
|
+
user: {
|
|
492
|
+
email,
|
|
493
|
+
},
|
|
494
|
+
authMode: auth.auth_mode || null,
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
|
|
438
498
|
const labels = { 'codex': 'Codex' };
|
|
439
499
|
|
|
440
500
|
module.exports = {
|
|
@@ -442,4 +502,5 @@ module.exports = {
|
|
|
442
502
|
labels,
|
|
443
503
|
getChats,
|
|
444
504
|
getMessages,
|
|
505
|
+
getUsage,
|
|
445
506
|
};
|
package/editors/copilot.js
CHANGED
|
@@ -171,6 +171,73 @@ function safeParse(str) {
|
|
|
171
171
|
try { return JSON.parse(str); } catch { return {}; }
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
+
// ============================================================
|
|
175
|
+
// Usage / quota data from GitHub Copilot internal API
|
|
176
|
+
// ============================================================
|
|
177
|
+
|
|
178
|
+
function getCopilotToken() {
|
|
179
|
+
// GitHub Copilot stores its OAuth token in ~/.config/github-copilot/apps.json
|
|
180
|
+
const appsPath = path.join(os.homedir(), '.config', 'github-copilot', 'apps.json');
|
|
181
|
+
try {
|
|
182
|
+
if (!fs.existsSync(appsPath)) return null;
|
|
183
|
+
const data = JSON.parse(fs.readFileSync(appsPath, 'utf-8'));
|
|
184
|
+
for (const entry of Object.values(data)) {
|
|
185
|
+
if (entry.oauth_token) return { token: entry.oauth_token, user: entry.user || null };
|
|
186
|
+
}
|
|
187
|
+
} catch {}
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function fetchCopilotStatus(token) {
|
|
192
|
+
return new Promise((resolve) => {
|
|
193
|
+
const https = require('https');
|
|
194
|
+
const req = https.get('https://api.github.com/copilot_internal/v2/token', {
|
|
195
|
+
headers: {
|
|
196
|
+
'Authorization': `token ${token}`,
|
|
197
|
+
'Accept': 'application/json',
|
|
198
|
+
'User-Agent': 'agentlytics/1.0',
|
|
199
|
+
},
|
|
200
|
+
timeout: 10000,
|
|
201
|
+
}, (res) => {
|
|
202
|
+
let data = '';
|
|
203
|
+
res.on('data', (chunk) => { data += chunk; });
|
|
204
|
+
res.on('end', () => {
|
|
205
|
+
try { resolve(JSON.parse(data)); } catch { resolve(null); }
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
req.on('error', () => resolve(null));
|
|
209
|
+
req.on('timeout', () => { req.destroy(); resolve(null); });
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async function getUsage() {
|
|
214
|
+
const creds = getCopilotToken();
|
|
215
|
+
if (!creds) return null;
|
|
216
|
+
|
|
217
|
+
const status = await fetchCopilotStatus(creds.token);
|
|
218
|
+
if (!status || status.message) return null;
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
source: 'copilot-cli',
|
|
222
|
+
plan: {
|
|
223
|
+
name: status.sku || null,
|
|
224
|
+
individual: status.individual || false,
|
|
225
|
+
},
|
|
226
|
+
features: {
|
|
227
|
+
chat: status.chat_enabled || false,
|
|
228
|
+
codeReview: status.code_review_enabled || false,
|
|
229
|
+
agentMode: status.agent_mode_auto_approval || false,
|
|
230
|
+
},
|
|
231
|
+
limits: {
|
|
232
|
+
quotas: status.limited_user_quotas || null,
|
|
233
|
+
resetDate: status.limited_user_reset_date || null,
|
|
234
|
+
},
|
|
235
|
+
user: {
|
|
236
|
+
login: creds.user || null,
|
|
237
|
+
},
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
174
241
|
const labels = { 'copilot-cli': 'Copilot CLI' };
|
|
175
242
|
|
|
176
|
-
module.exports = { name, labels, getChats, getMessages };
|
|
243
|
+
module.exports = { name, labels, getChats, getMessages, getUsage };
|
package/editors/cursor.js
CHANGED
|
@@ -341,6 +341,78 @@ function getMessages(chat) {
|
|
|
341
341
|
return msgs;
|
|
342
342
|
}
|
|
343
343
|
|
|
344
|
+
// ============================================================
|
|
345
|
+
// Usage / quota data from Cursor REST API
|
|
346
|
+
// ============================================================
|
|
347
|
+
|
|
348
|
+
function getCursorAccessToken() {
|
|
349
|
+
try {
|
|
350
|
+
const db = new Database(GLOBAL_STORAGE_DB, { readonly: true });
|
|
351
|
+
const row = db.prepare("SELECT value FROM ItemTable WHERE key = 'cursorAuth/accessToken'").get();
|
|
352
|
+
db.close();
|
|
353
|
+
return row ? row.value : null;
|
|
354
|
+
} catch { return null; }
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function cursorApiFetch(endpoint, token) {
|
|
358
|
+
return new Promise((resolve) => {
|
|
359
|
+
const https = require('https');
|
|
360
|
+
const url = `https://api2.cursor.sh/auth/${endpoint}`;
|
|
361
|
+
const req = https.get(url, {
|
|
362
|
+
headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
|
|
363
|
+
timeout: 10000,
|
|
364
|
+
}, (res) => {
|
|
365
|
+
let data = '';
|
|
366
|
+
res.on('data', (chunk) => { data += chunk; });
|
|
367
|
+
res.on('end', () => {
|
|
368
|
+
try { resolve(JSON.parse(data)); } catch { resolve(null); }
|
|
369
|
+
});
|
|
370
|
+
});
|
|
371
|
+
req.on('error', () => resolve(null));
|
|
372
|
+
req.on('timeout', () => { req.destroy(); resolve(null); });
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
async function getUsage() {
|
|
377
|
+
const token = getCursorAccessToken();
|
|
378
|
+
if (!token) return null;
|
|
379
|
+
|
|
380
|
+
const [profile, usage] = await Promise.all([
|
|
381
|
+
cursorApiFetch('full_stripe_profile', token),
|
|
382
|
+
cursorApiFetch('usage', token),
|
|
383
|
+
]);
|
|
384
|
+
|
|
385
|
+
if (!profile && !usage) return null;
|
|
386
|
+
|
|
387
|
+
const result = {
|
|
388
|
+
source: 'cursor',
|
|
389
|
+
plan: {
|
|
390
|
+
name: profile?.individualMembershipType || profile?.membershipType || null,
|
|
391
|
+
status: profile?.subscriptionStatus || null,
|
|
392
|
+
isTeamMember: profile?.isTeamMember || false,
|
|
393
|
+
isYearlyPlan: profile?.isYearlyPlan || false,
|
|
394
|
+
},
|
|
395
|
+
usage: {},
|
|
396
|
+
startOfMonth: usage?.startOfMonth || null,
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
// Parse per-model usage from the usage endpoint
|
|
400
|
+
if (usage) {
|
|
401
|
+
for (const [model, data] of Object.entries(usage)) {
|
|
402
|
+
if (model === 'startOfMonth') continue;
|
|
403
|
+
result.usage[model] = {
|
|
404
|
+
numRequests: data.numRequests || 0,
|
|
405
|
+
numRequestsTotal: data.numRequestsTotal || 0,
|
|
406
|
+
numTokens: data.numTokens || 0,
|
|
407
|
+
maxRequestUsage: data.maxRequestUsage || null,
|
|
408
|
+
maxTokenUsage: data.maxTokenUsage || null,
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
return result;
|
|
414
|
+
}
|
|
415
|
+
|
|
344
416
|
const labels = { 'cursor': 'Cursor' };
|
|
345
417
|
|
|
346
|
-
module.exports = { name, labels, getChats, getMessages };
|
|
418
|
+
module.exports = { name, labels, getChats, getMessages, getUsage };
|
package/editors/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const cursor = require('./cursor');
|
|
2
2
|
const windsurf = require('./windsurf');
|
|
3
|
+
const antigravity = require('./antigravity');
|
|
3
4
|
const claude = require('./claude');
|
|
4
5
|
const vscode = require('./vscode');
|
|
5
6
|
const zed = require('./zed');
|
|
@@ -10,8 +11,9 @@ const copilot = require('./copilot');
|
|
|
10
11
|
const cursorAgent = require('./cursor-agent');
|
|
11
12
|
const commandcode = require('./commandcode');
|
|
12
13
|
const goose = require('./goose');
|
|
14
|
+
const kiro = require('./kiro');
|
|
13
15
|
|
|
14
|
-
const editors = [cursor, windsurf, claude, vscode, zed, opencode, codex, gemini, copilot, cursorAgent, commandcode, goose];
|
|
16
|
+
const editors = [cursor, windsurf, antigravity, claude, vscode, zed, opencode, codex, gemini, copilot, cursorAgent, commandcode, goose, kiro];
|
|
15
17
|
|
|
16
18
|
// Build a unified source → display-label map from all editor modules
|
|
17
19
|
const editorLabels = {};
|
|
@@ -59,4 +61,23 @@ function resetCaches() {
|
|
|
59
61
|
}
|
|
60
62
|
}
|
|
61
63
|
|
|
62
|
-
|
|
64
|
+
/**
|
|
65
|
+
* Get usage / quota data from all editors that support it.
|
|
66
|
+
* Returns an array of usage objects, one per editor/variant.
|
|
67
|
+
*/
|
|
68
|
+
async function getAllUsage() {
|
|
69
|
+
const results = [];
|
|
70
|
+
for (const editor of editors) {
|
|
71
|
+
if (typeof editor.getUsage !== 'function') continue;
|
|
72
|
+
try {
|
|
73
|
+
const usage = await editor.getUsage();
|
|
74
|
+
if (!usage) continue;
|
|
75
|
+
// Windsurf returns an array (one per variant), Cursor returns a single object
|
|
76
|
+
if (Array.isArray(usage)) results.push(...usage);
|
|
77
|
+
else results.push(usage);
|
|
78
|
+
} catch { /* skip broken adapters */ }
|
|
79
|
+
}
|
|
80
|
+
return results;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
module.exports = { getAllChats, getMessages, editors, editorLabels, resetCaches, getAllUsage };
|
package/editors/kiro.js
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const { getAppDataPath } = require('./base');
|
|
5
|
+
|
|
6
|
+
// ============================================================
|
|
7
|
+
// Kiro editor adapter
|
|
8
|
+
// ============================================================
|
|
9
|
+
|
|
10
|
+
const name = 'kiro';
|
|
11
|
+
|
|
12
|
+
const KIRO_AGENT_DIR = path.join(
|
|
13
|
+
getAppDataPath('Kiro'), 'User', 'globalStorage', 'kiro.kiroagent'
|
|
14
|
+
);
|
|
15
|
+
const WORKSPACE_SESSIONS_DIR = path.join(KIRO_AGENT_DIR, 'workspace-sessions');
|
|
16
|
+
|
|
17
|
+
function getChats() {
|
|
18
|
+
const chats = [];
|
|
19
|
+
if (!fs.existsSync(KIRO_AGENT_DIR)) return chats;
|
|
20
|
+
|
|
21
|
+
// Strategy 1: workspace-sessions (structured, has workspace info)
|
|
22
|
+
if (fs.existsSync(WORKSPACE_SESSIONS_DIR)) {
|
|
23
|
+
try {
|
|
24
|
+
for (const folder of fs.readdirSync(WORKSPACE_SESSIONS_DIR)) {
|
|
25
|
+
const wsDir = path.join(WORKSPACE_SESSIONS_DIR, folder);
|
|
26
|
+
if (!fs.statSync(wsDir).isDirectory()) continue;
|
|
27
|
+
|
|
28
|
+
// Decode base64 folder name to get workspace path
|
|
29
|
+
let workspacePath = null;
|
|
30
|
+
try {
|
|
31
|
+
workspacePath = Buffer.from(folder, 'base64').toString('utf-8');
|
|
32
|
+
} catch {}
|
|
33
|
+
|
|
34
|
+
const indexPath = path.join(wsDir, 'sessions.json');
|
|
35
|
+
let sessions = [];
|
|
36
|
+
try {
|
|
37
|
+
sessions = JSON.parse(fs.readFileSync(indexPath, 'utf-8'));
|
|
38
|
+
} catch { continue; }
|
|
39
|
+
|
|
40
|
+
for (const session of sessions) {
|
|
41
|
+
const sessionFile = path.join(wsDir, `${session.sessionId}.json`);
|
|
42
|
+
const exists = fs.existsSync(sessionFile);
|
|
43
|
+
|
|
44
|
+
chats.push({
|
|
45
|
+
source: 'kiro',
|
|
46
|
+
composerId: session.sessionId,
|
|
47
|
+
name: cleanTitle(session.title),
|
|
48
|
+
createdAt: parseInt(session.dateCreated) || null,
|
|
49
|
+
lastUpdatedAt: exists ? getFileMtime(sessionFile) : parseInt(session.dateCreated) || null,
|
|
50
|
+
mode: 'kiro',
|
|
51
|
+
folder: session.workspaceDirectory || workspacePath || null,
|
|
52
|
+
encrypted: false,
|
|
53
|
+
bubbleCount: 0,
|
|
54
|
+
_fullPath: exists ? sessionFile : null,
|
|
55
|
+
_type: 'workspace-session',
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
} catch {}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Strategy 2: .chat files in hash directories (individual agent executions)
|
|
63
|
+
// Kiro saves a snapshot of the conversation after each API call, so multiple
|
|
64
|
+
// .chat files can share the same executionId. We group by executionId and
|
|
65
|
+
// keep only the latest snapshot (highest message count) per conversation.
|
|
66
|
+
const seenIds = new Set(chats.map(c => c.composerId));
|
|
67
|
+
const executionMap = new Map(); // executionId -> best candidate
|
|
68
|
+
try {
|
|
69
|
+
for (const dir of fs.readdirSync(KIRO_AGENT_DIR)) {
|
|
70
|
+
// Skip known non-workspace directories
|
|
71
|
+
if (['default', 'dev_data', 'index', 'sessions', 'workspace-sessions'].includes(dir)) continue;
|
|
72
|
+
const fullDir = path.join(KIRO_AGENT_DIR, dir);
|
|
73
|
+
if (!fs.statSync(fullDir).isDirectory()) continue;
|
|
74
|
+
|
|
75
|
+
let files;
|
|
76
|
+
try { files = fs.readdirSync(fullDir).filter(f => f.endsWith('.chat')); } catch { continue; }
|
|
77
|
+
|
|
78
|
+
for (const file of files) {
|
|
79
|
+
const fullPath = path.join(fullDir, file);
|
|
80
|
+
try {
|
|
81
|
+
const stat = fs.statSync(fullPath);
|
|
82
|
+
const meta = peekChatMeta(fullPath);
|
|
83
|
+
const chatId = meta.executionId || `${dir}/${file.replace('.chat', '')}`;
|
|
84
|
+
if (seenIds.has(chatId)) continue;
|
|
85
|
+
|
|
86
|
+
const candidate = {
|
|
87
|
+
source: 'kiro',
|
|
88
|
+
composerId: chatId,
|
|
89
|
+
name: meta.title || null,
|
|
90
|
+
createdAt: meta.startTime || stat.birthtime.getTime(),
|
|
91
|
+
lastUpdatedAt: meta.endTime || stat.mtime.getTime(),
|
|
92
|
+
mode: meta.workflow || 'kiro',
|
|
93
|
+
folder: meta.folder || null,
|
|
94
|
+
encrypted: false,
|
|
95
|
+
bubbleCount: meta.messageCount || 0,
|
|
96
|
+
_fullPath: fullPath,
|
|
97
|
+
_type: 'chat-file',
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// Keep the snapshot with the most messages per executionId
|
|
101
|
+
if (meta.executionId) {
|
|
102
|
+
const existing = executionMap.get(meta.executionId);
|
|
103
|
+
if (!existing || meta.messageCount > existing.bubbleCount) {
|
|
104
|
+
// Update createdAt to the earliest startTime seen
|
|
105
|
+
if (existing && existing.createdAt < candidate.createdAt) {
|
|
106
|
+
candidate.createdAt = existing.createdAt;
|
|
107
|
+
}
|
|
108
|
+
executionMap.set(meta.executionId, candidate);
|
|
109
|
+
} else if (existing && meta.startTime && meta.startTime < existing.createdAt) {
|
|
110
|
+
existing.createdAt = meta.startTime;
|
|
111
|
+
}
|
|
112
|
+
} else {
|
|
113
|
+
chats.push(candidate);
|
|
114
|
+
}
|
|
115
|
+
} catch {}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
} catch {}
|
|
119
|
+
|
|
120
|
+
// Add the deduplicated execution sessions
|
|
121
|
+
for (const chat of executionMap.values()) {
|
|
122
|
+
chats.push(chat);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return chats;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function peekChatMeta(filePath) {
|
|
129
|
+
const meta = { title: null, folder: null, startTime: null, endTime: null, workflow: null, messageCount: 0, executionId: null };
|
|
130
|
+
try {
|
|
131
|
+
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
132
|
+
const data = JSON.parse(raw);
|
|
133
|
+
|
|
134
|
+
meta.executionId = data.executionId || null;
|
|
135
|
+
|
|
136
|
+
if (data.metadata) {
|
|
137
|
+
meta.startTime = data.metadata.startTime || null;
|
|
138
|
+
meta.endTime = data.metadata.endTime || null;
|
|
139
|
+
meta.workflow = data.metadata.workflow || null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const chat = data.chat || [];
|
|
143
|
+
for (const msg of chat) {
|
|
144
|
+
if (msg.role === 'human') {
|
|
145
|
+
// Try to extract user request from rules block
|
|
146
|
+
const userReq = extractUserRequest(msg.content);
|
|
147
|
+
if (userReq && !meta.title) {
|
|
148
|
+
meta.title = cleanTitle(userReq);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (msg.role === 'bot' || msg.role === 'human') meta.messageCount++;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Try to extract folder from context
|
|
155
|
+
for (const ctx of data.context || []) {
|
|
156
|
+
if (ctx.type === 'steering' && ctx.id) {
|
|
157
|
+
// Extract workspace from steering file path
|
|
158
|
+
const match = ctx.id.match(/file:\/\/(.*?)\/.kiro\//);
|
|
159
|
+
if (match) meta.folder = match[1];
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
} catch {}
|
|
163
|
+
return meta;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function isSystemPrompt(content) {
|
|
167
|
+
if (typeof content !== 'string') return false;
|
|
168
|
+
return content.startsWith('<identity>') || content.startsWith('# ');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function extractUserRequest(content) {
|
|
172
|
+
if (typeof content !== 'string') return null;
|
|
173
|
+
// "## Included Rules" messages contain the actual user request after </user-rule>
|
|
174
|
+
const ruleEnd = content.lastIndexOf('</user-rule>');
|
|
175
|
+
if (ruleEnd >= 0) {
|
|
176
|
+
let userPart = content.substring(ruleEnd + '</user-rule>'.length).trim();
|
|
177
|
+
// Strip trailing EnvironmentContext block
|
|
178
|
+
const envIdx = userPart.indexOf('<EnvironmentContext>');
|
|
179
|
+
if (envIdx >= 0) userPart = userPart.substring(0, envIdx).trim();
|
|
180
|
+
// Strip steering-reminder blocks
|
|
181
|
+
const steerIdx = userPart.indexOf('<steering-reminder>');
|
|
182
|
+
if (steerIdx >= 0) userPart = userPart.substring(0, steerIdx).trim();
|
|
183
|
+
if (userPart) return userPart;
|
|
184
|
+
}
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function getMessages(chat) {
|
|
189
|
+
if (!chat._fullPath || !fs.existsSync(chat._fullPath)) return [];
|
|
190
|
+
|
|
191
|
+
if (chat._type === 'workspace-session') {
|
|
192
|
+
return getWorkspaceSessionMessages(chat._fullPath);
|
|
193
|
+
}
|
|
194
|
+
return getChatFileMessages(chat._fullPath);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function getWorkspaceSessionMessages(filePath) {
|
|
198
|
+
const messages = [];
|
|
199
|
+
try {
|
|
200
|
+
const data = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
201
|
+
const history = data.history || [];
|
|
202
|
+
|
|
203
|
+
for (const entry of history) {
|
|
204
|
+
const msg = entry.message;
|
|
205
|
+
if (!msg) continue;
|
|
206
|
+
|
|
207
|
+
const role = msg.role === 'user' ? 'user' : msg.role === 'assistant' ? 'assistant' : null;
|
|
208
|
+
if (!role) continue;
|
|
209
|
+
|
|
210
|
+
const content = extractContentFromMessage(msg.content);
|
|
211
|
+
if (!content) continue;
|
|
212
|
+
|
|
213
|
+
const result = { role, content };
|
|
214
|
+
|
|
215
|
+
// Extract model info from promptLogs
|
|
216
|
+
if (role === 'assistant' && entry.promptLogs && entry.promptLogs.length > 0) {
|
|
217
|
+
const log = entry.promptLogs[0];
|
|
218
|
+
result._model = log.modelTitle || null;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
messages.push(result);
|
|
222
|
+
}
|
|
223
|
+
} catch {}
|
|
224
|
+
return messages;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function getChatFileMessages(filePath) {
|
|
228
|
+
const messages = [];
|
|
229
|
+
try {
|
|
230
|
+
const data = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
231
|
+
const chat = data.chat || [];
|
|
232
|
+
const model = data.metadata?.modelId || null;
|
|
233
|
+
|
|
234
|
+
for (const msg of chat) {
|
|
235
|
+
if (msg.role === 'human') {
|
|
236
|
+
if (isSystemPrompt(msg.content)) continue;
|
|
237
|
+
// Try extracting user request from rules block first
|
|
238
|
+
const userReq = extractUserRequest(msg.content);
|
|
239
|
+
const content = userReq || extractUserText(msg.content);
|
|
240
|
+
if (content) messages.push({ role: 'user', content });
|
|
241
|
+
} else if (msg.role === 'bot') {
|
|
242
|
+
const content = typeof msg.content === 'string' ? msg.content : '';
|
|
243
|
+
if (content) messages.push({ role: 'assistant', content, _model: model });
|
|
244
|
+
} else if (msg.role === 'tool') {
|
|
245
|
+
const content = typeof msg.content === 'string' ? msg.content : '';
|
|
246
|
+
if (content) messages.push({ role: 'tool', content: content.substring(0, 2000) });
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
} catch {}
|
|
250
|
+
return messages;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function extractContentFromMessage(content) {
|
|
254
|
+
if (typeof content === 'string') return content;
|
|
255
|
+
if (!Array.isArray(content)) return '';
|
|
256
|
+
return content
|
|
257
|
+
.filter(c => c.type === 'text' || c.type === 'mention')
|
|
258
|
+
.map(c => c.text)
|
|
259
|
+
.join('\n') || '';
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function extractUserText(content) {
|
|
263
|
+
if (typeof content === 'string') {
|
|
264
|
+
// Skip system prompt content
|
|
265
|
+
if (isSystemPrompt(content)) return null;
|
|
266
|
+
// Strip XML tags and rules blocks
|
|
267
|
+
return cleanTitle(content);
|
|
268
|
+
}
|
|
269
|
+
if (Array.isArray(content)) {
|
|
270
|
+
return content
|
|
271
|
+
.filter(c => c.type === 'text' || c.type === 'mention')
|
|
272
|
+
.map(c => c.text)
|
|
273
|
+
.join('\n') || '';
|
|
274
|
+
}
|
|
275
|
+
return '';
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function cleanTitle(title) {
|
|
279
|
+
if (!title) return null;
|
|
280
|
+
let clean = title
|
|
281
|
+
.replace(/<[^>]+>[\s\S]*?<\/[^>]+>/g, '')
|
|
282
|
+
.replace(/<[^>]+>/g, '')
|
|
283
|
+
.replace(/## Included Rules[\s\S]*$/m, '')
|
|
284
|
+
.replace(/\s+/g, ' ')
|
|
285
|
+
.trim()
|
|
286
|
+
.substring(0, 120);
|
|
287
|
+
return clean || null;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function getFileMtime(filePath) {
|
|
291
|
+
try { return fs.statSync(filePath).mtime.getTime(); } catch { return null; }
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const labels = { 'kiro': 'Kiro' };
|
|
295
|
+
|
|
296
|
+
module.exports = { name, labels, getChats, getMessages };
|