@aixle/insights 0.2.0 → 0.2.1-staging
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 +36 -8
- package/dist/auth/credentials.d.ts +7 -1
- package/dist/auth/credentials.js +71 -14
- package/dist/auth/exchange.d.ts +1 -1
- package/dist/auth/exchange.js +1 -1
- package/dist/auth/flow.d.ts +8 -1
- package/dist/auth/flow.js +27 -5
- package/dist/auth/keycloak.d.ts +1 -1
- package/dist/auth/keycloak.js +20 -1
- package/dist/cli.d.ts +5 -3
- package/dist/cli.js +69 -21
- package/dist/collect-cursor-payloads.d.ts +4 -3
- package/dist/collect-cursor-payloads.js +8 -5
- package/dist/cursor-checkpoints.d.ts +2 -2
- package/dist/cursor-payload-contract.d.ts +5 -5
- package/dist/cursor-payload-contract.js +6 -0
- package/dist/cursor-settings.d.ts +9 -4
- package/dist/cursor-settings.js +80 -10
- package/dist/health.d.ts +3 -1
- package/dist/health.js +13 -1
- package/dist/hooks/cursor-hooks-mapper.d.ts +3 -3
- package/dist/hooks/cursor-hooks-mapper.js +1 -1
- package/dist/hooks/cursor-hooks-reader.d.ts +2 -0
- package/dist/hooks/cursor-hooks-reader.js +2 -2
- package/dist/install/cursor.d.ts +34 -0
- package/dist/install/cursor.js +193 -0
- package/dist/install/index.d.ts +6 -4
- package/dist/install/index.js +6 -1
- package/dist/lib/client.d.ts +7 -0
- package/dist/lib/client.js +17 -0
- package/dist/lib/config.js +7 -2
- package/dist/lib/project-resolver.d.ts +5 -4
- package/dist/lib/project-resolver.js +20 -8
- package/dist/lib/transport-security.d.ts +1 -0
- package/dist/lib/transport-security.js +1 -1
- package/dist/readers/claude.d.ts +54 -6
- package/dist/readers/claude.js +154 -2
- package/dist/readers/cursor.d.ts +10 -7
- package/dist/readers/cursor.js +101 -15
- package/dist/server.d.ts +20 -3
- package/dist/server.js +101 -67
- package/dist/state.js +7 -2
- package/dist/sync.d.ts +4 -2
- package/dist/sync.js +61 -46
- package/package.json +2 -2
package/dist/readers/claude.js
CHANGED
|
@@ -6,6 +6,83 @@ import { homedir } from "node:os";
|
|
|
6
6
|
import { glob } from "glob";
|
|
7
7
|
import { calculateCost } from "../pricing.js";
|
|
8
8
|
import { scanText } from "../risk-scanner.js";
|
|
9
|
+
const NAV_TOOLS = new Set(["Read", "Grep", "Glob", "LS"]);
|
|
10
|
+
const EDIT_TOOLS = new Set(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
|
|
11
|
+
function bashCommand(block) {
|
|
12
|
+
const c = block.input?.command;
|
|
13
|
+
return typeof c === "string" ? c : "";
|
|
14
|
+
}
|
|
15
|
+
export function classifyToolUse(block) {
|
|
16
|
+
if (NAV_TOOLS.has(block.name))
|
|
17
|
+
return null;
|
|
18
|
+
if (EDIT_TOOLS.has(block.name))
|
|
19
|
+
return "edit";
|
|
20
|
+
if (block.name === "Bash") {
|
|
21
|
+
const cmd = bashCommand(block);
|
|
22
|
+
// Require a space or end-of-string after "commit" so "git commit-tree" is not a commit.
|
|
23
|
+
if (/\bgit\s+commit(\s|$)/.test(cmd))
|
|
24
|
+
return "commit";
|
|
25
|
+
if (/\b(rspec|jest|vitest|pytest|phpunit)\b/i.test(cmd))
|
|
26
|
+
return "test";
|
|
27
|
+
}
|
|
28
|
+
return "tool_use";
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Redacts credential-bearing substrings from a Bash command string before egress.
|
|
32
|
+
*
|
|
33
|
+
* Claude events carry `scannable: true`, which causes the server's ClassificationActivity
|
|
34
|
+
* to take Path 2 (classification_activity.rb:30-44) — trusting the CLI verbatim and
|
|
35
|
+
* skipping server-side sanitization. This function is the single point of trust for
|
|
36
|
+
* command strings derived from tool_input.command. Any new pattern that reads from
|
|
37
|
+
* tool_input.command MUST pass through this function before being emitted. See DATA-CURRENT.md §12.
|
|
38
|
+
*/
|
|
39
|
+
/**
|
|
40
|
+
* True when an env-var name looks credential-bearing.
|
|
41
|
+
* Long tokens (SECRET/TOKEN/PASSWORD/…) may appear as substrings; short ones
|
|
42
|
+
* (KEY/PASS/PWD) must be underscore-delimited segments so "monkey" / "keyboard"
|
|
43
|
+
* are not redacted.
|
|
44
|
+
*/
|
|
45
|
+
function isSecretEnvName(name) {
|
|
46
|
+
const n = name.toUpperCase();
|
|
47
|
+
if (/(?:SECRET|TOKEN|PASSWORD|APIKEY|API_KEY)/.test(n))
|
|
48
|
+
return true;
|
|
49
|
+
return /(?:^|_)(?:KEY|PASS|PWD)(?:_|$)/.test(n);
|
|
50
|
+
}
|
|
51
|
+
export function scrubBashCommand(cmd) {
|
|
52
|
+
return (cmd
|
|
53
|
+
// Authorization / Bearer / Basic headers (curl -H, fetch headers, etc.)
|
|
54
|
+
.replace(/\b(Authorization\s*:\s*)(Bearer|Basic|Token)\s+\S+/gi, "$1$2 [REDACTED]")
|
|
55
|
+
// Inline env-var assignments carrying secrets (AWS_SECRET_ACCESS_KEY=…, db_password=…)
|
|
56
|
+
.replace(/\b([A-Za-z_][A-Za-z0-9_]*)\s*=\s*\S+/g, (match, name) => isSecretEnvName(name) ? `${name}=[REDACTED]` : match)
|
|
57
|
+
// --password=, --token=, --secret=, --api-key= flags
|
|
58
|
+
.replace(/(--(?:password|token|secret|api[-_]?key|access[-_]?key|auth[-_]?key)=)\S+/gi, "$1[REDACTED]")
|
|
59
|
+
// -p / --password <value> flag-value pairs
|
|
60
|
+
.replace(/((?:^|\s)-p\s+)\S+/g, "$1[REDACTED]")
|
|
61
|
+
// AWS CLI --profile (may embed customer identifiers)
|
|
62
|
+
.replace(/(--profile\s+)\S+/g, "$1[REDACTED]")
|
|
63
|
+
// Cloud CLI credential flags: --access-key-id, --secret-access-key, --service-account-key
|
|
64
|
+
.replace(/(--(?:access-key-id|secret-access-key|service-account-key|client-secret)\s+)\S+/gi, "$1[REDACTED]")
|
|
65
|
+
// curl | sh / curl | bash patterns (remote code execution)
|
|
66
|
+
.replace(/\|\s*(sh|bash|zsh|dash)\b/g, "| [SHELL REDACTED]"));
|
|
67
|
+
}
|
|
68
|
+
export function summarizeToolUse(block) {
|
|
69
|
+
const input = block.input ?? {};
|
|
70
|
+
let raw;
|
|
71
|
+
if (typeof input.file_path === "string" && input.file_path.trim()) {
|
|
72
|
+
raw = `${block.name}: ${input.file_path}`;
|
|
73
|
+
}
|
|
74
|
+
else if (typeof input.command === "string" && input.command.trim()) {
|
|
75
|
+
const scrubbed = scrubBashCommand(input.command.trim().replace(/\s+/g, " "));
|
|
76
|
+
raw = `${block.name}: ${scrubbed}`;
|
|
77
|
+
}
|
|
78
|
+
else if (typeof input.pattern === "string") {
|
|
79
|
+
raw = `${block.name}: ${scrubBashCommand(input.pattern)}`;
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
raw = block.name;
|
|
83
|
+
}
|
|
84
|
+
return raw.length <= 256 ? raw : `${raw.slice(0, 253)}...`;
|
|
85
|
+
}
|
|
9
86
|
/** Prompt substrings emitted for local IDE commands — not real user prompts. */
|
|
10
87
|
const LOCAL_COMMAND_NOISE_PROMPT_PATTERNS = [
|
|
11
88
|
/<local-command-caveat\b/i,
|
|
@@ -125,6 +202,10 @@ function newTurn(sessionId, turnIndex, filePath, fileSize, occurredAt, promptId)
|
|
|
125
202
|
riskLevel: "low",
|
|
126
203
|
riskScore: 0,
|
|
127
204
|
riskCategories: [],
|
|
205
|
+
toolUses: [],
|
|
206
|
+
navToolCalls: 0,
|
|
207
|
+
totalToolCalls: 0,
|
|
208
|
+
messageIds: [],
|
|
128
209
|
persisted: false,
|
|
129
210
|
};
|
|
130
211
|
}
|
|
@@ -141,6 +222,41 @@ function enrichTurnRisk(turn) {
|
|
|
141
222
|
turn.riskScore = result.risk_score;
|
|
142
223
|
turn.riskCategories = result.risk_categories;
|
|
143
224
|
}
|
|
225
|
+
function collectToolUsesFromContent(content, turn) {
|
|
226
|
+
if (!Array.isArray(content))
|
|
227
|
+
return;
|
|
228
|
+
let nav = 0;
|
|
229
|
+
let total = 0;
|
|
230
|
+
for (const raw of content) {
|
|
231
|
+
if (typeof raw !== "object" || raw === null)
|
|
232
|
+
continue;
|
|
233
|
+
const block = raw;
|
|
234
|
+
if (block.type !== "tool_use" || typeof block.name !== "string")
|
|
235
|
+
continue;
|
|
236
|
+
total += 1;
|
|
237
|
+
const toolBlock = {
|
|
238
|
+
id: typeof block.id === "string" ? block.id : undefined,
|
|
239
|
+
name: block.name,
|
|
240
|
+
input: typeof block.input === "object" && block.input !== null
|
|
241
|
+
? block.input
|
|
242
|
+
: undefined,
|
|
243
|
+
};
|
|
244
|
+
const eventType = classifyToolUse(toolBlock);
|
|
245
|
+
if (eventType === null) {
|
|
246
|
+
nav += 1;
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
const id = toolBlock.id ?? `idx-${turn.toolUses.length}`;
|
|
250
|
+
turn.toolUses.push({
|
|
251
|
+
id,
|
|
252
|
+
name: toolBlock.name,
|
|
253
|
+
eventType,
|
|
254
|
+
summary: summarizeToolUse(toolBlock),
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
turn.navToolCalls += nav;
|
|
258
|
+
turn.totalToolCalls += total;
|
|
259
|
+
}
|
|
144
260
|
/** Streams a JSONL file and splits Claude transcripts into individual turns. */
|
|
145
261
|
export async function parseTranscriptFile(filePath, verbose = false) {
|
|
146
262
|
const turns = [];
|
|
@@ -254,9 +370,13 @@ export async function parseTranscriptFile(filePath, verbose = false) {
|
|
|
254
370
|
}
|
|
255
371
|
if (entry.message.model)
|
|
256
372
|
currentTurn.model = entry.message.model;
|
|
373
|
+
if (entry.message.id && !currentTurn.messageIds.includes(entry.message.id)) {
|
|
374
|
+
currentTurn.messageIds.push(entry.message.id);
|
|
375
|
+
}
|
|
257
376
|
currentTurn.occurredAt = timestamp > currentTurn.occurredAt ? timestamp : currentTurn.occurredAt;
|
|
258
377
|
const text = extractContentText(entry.message.content).join("\n\n").trim();
|
|
259
378
|
currentTurn.assistantText = appendText(currentTurn.assistantText, text);
|
|
379
|
+
collectToolUsesFromContent(entry.message.content, currentTurn);
|
|
260
380
|
}
|
|
261
381
|
}
|
|
262
382
|
}
|
|
@@ -273,7 +393,7 @@ export async function parseTranscriptFile(filePath, verbose = false) {
|
|
|
273
393
|
flushCurrentTurn();
|
|
274
394
|
return finalizedTurns.map(({ persisted: _persisted, ...turn }) => turn);
|
|
275
395
|
}
|
|
276
|
-
/** Converts a Claude transcript turn to
|
|
396
|
+
/** Converts a Claude transcript turn to parent chat and derivative tool-use payloads. */
|
|
277
397
|
export function mapTranscriptTurn(turn, options) {
|
|
278
398
|
const { projectId, pricing } = options ?? {};
|
|
279
399
|
const baseInputTokens = Math.max(0, turn.tokensIn - turn.cacheWriteTokens - turn.cacheReadTokens);
|
|
@@ -300,6 +420,11 @@ export function mapTranscriptTurn(turn, options) {
|
|
|
300
420
|
prompt_text: turn.promptText || undefined,
|
|
301
421
|
assistant_text: turn.assistantText || undefined,
|
|
302
422
|
scannable: true,
|
|
423
|
+
cost_model: "token_count",
|
|
424
|
+
// Zero is intentional: confirms no tool activity on this turn (not an omission).
|
|
425
|
+
nav_tool_calls: turn.navToolCalls,
|
|
426
|
+
total_tool_calls: turn.totalToolCalls,
|
|
427
|
+
message_ids: turn.messageIds.length > 0 ? turn.messageIds : undefined,
|
|
303
428
|
},
|
|
304
429
|
};
|
|
305
430
|
if (turn.model)
|
|
@@ -313,5 +438,32 @@ export function mapTranscriptTurn(turn, options) {
|
|
|
313
438
|
}
|
|
314
439
|
if (projectId)
|
|
315
440
|
payload.project_id = projectId;
|
|
316
|
-
|
|
441
|
+
const derivatives = turn.toolUses.map((toolUse) => {
|
|
442
|
+
const derivative = {
|
|
443
|
+
tool_name: "claude_code",
|
|
444
|
+
event_type: toolUse.eventType,
|
|
445
|
+
cost_usd: 0,
|
|
446
|
+
occurred_at: turn.occurredAt,
|
|
447
|
+
metadata: {
|
|
448
|
+
session_id: `${turn.turnId}:tool:${toolUse.id}`,
|
|
449
|
+
claude_session_id: turn.sessionId,
|
|
450
|
+
transcript_source: "claude_jsonl",
|
|
451
|
+
cost_model: "derivative",
|
|
452
|
+
parent_session_id: turn.turnId,
|
|
453
|
+
tool_name_inner: toolUse.name,
|
|
454
|
+
tool_use_id: toolUse.id,
|
|
455
|
+
summary: toolUse.summary,
|
|
456
|
+
scannable: false,
|
|
457
|
+
risk_level: "none",
|
|
458
|
+
risk_categories: [],
|
|
459
|
+
risk_score: 0,
|
|
460
|
+
},
|
|
461
|
+
};
|
|
462
|
+
if (turn.model)
|
|
463
|
+
derivative.model = turn.model;
|
|
464
|
+
if (projectId)
|
|
465
|
+
derivative.project_id = projectId;
|
|
466
|
+
return derivative;
|
|
467
|
+
});
|
|
468
|
+
return [payload, ...derivatives];
|
|
317
469
|
}
|
package/dist/readers/cursor.d.ts
CHANGED
|
@@ -46,6 +46,8 @@ interface CursorComposerHeader {
|
|
|
46
46
|
composerId: string;
|
|
47
47
|
name: string | null;
|
|
48
48
|
workspacePath: string | null;
|
|
49
|
+
/** Session start (composer.createdAt). Used to spread turns when JSONL lacks per-message times. */
|
|
50
|
+
createdAt: string | null;
|
|
49
51
|
lastUpdatedAt: string | null;
|
|
50
52
|
}
|
|
51
53
|
export interface CursorTranscriptTurn {
|
|
@@ -83,7 +85,7 @@ export interface PricingConfig {
|
|
|
83
85
|
chat_output_per_mtok: number;
|
|
84
86
|
}
|
|
85
87
|
export declare const DEFAULT_CURSOR_PRICING: PricingConfig;
|
|
86
|
-
export type
|
|
88
|
+
export type CursorPayloadMetadata = {
|
|
87
89
|
session_id?: string;
|
|
88
90
|
cursor_session_id: string | null;
|
|
89
91
|
workspace: string;
|
|
@@ -109,8 +111,9 @@ export type Db90CursorPayloadMetadata = {
|
|
|
109
111
|
generation_id?: string;
|
|
110
112
|
hook_tool_name?: string;
|
|
111
113
|
duration_ms?: number;
|
|
114
|
+
model_resolution?: "settings_json" | "state_vscdb" | "unresolved";
|
|
112
115
|
};
|
|
113
|
-
export interface
|
|
116
|
+
export interface CursorPayload extends IngestPayload {
|
|
114
117
|
tool_name: "cursor";
|
|
115
118
|
event_type: "completion" | "chat" | "commit";
|
|
116
119
|
model: string;
|
|
@@ -119,16 +122,16 @@ export interface CursorDb90Payload extends IngestPayload {
|
|
|
119
122
|
cost_usd: number;
|
|
120
123
|
occurred_at: string;
|
|
121
124
|
project_id?: string;
|
|
122
|
-
metadata:
|
|
125
|
+
metadata: CursorPayloadMetadata;
|
|
123
126
|
}
|
|
124
127
|
export declare function toEpochMs(timestamp: number | string | null | undefined): number | null;
|
|
125
|
-
export declare function mapDailyStats(entry: DailyStatsEntry, projectId?: string, pricing?: PricingConfig, model?: string):
|
|
128
|
+
export declare function mapDailyStats(entry: DailyStatsEntry, projectId?: string, pricing?: PricingConfig, model?: string, modelResolution?: CursorPayloadMetadata["model_resolution"]): CursorPayload[];
|
|
126
129
|
/**
|
|
127
130
|
* Maps Cursor’s latest-commit snapshot (`aiCodeTracking.recentCommit`) to a single commit-classified event.
|
|
128
131
|
* Cursor only keeps one recent commit row (overwritten on each new commit).
|
|
129
132
|
* Line-cost math still follows the chat-style line proxy (`computeLineCost("chat", …)`); only `event_type` differs.
|
|
130
133
|
*/
|
|
131
|
-
export declare function mapRecentCommit(entry: RecentCommitSnapshot, projectId?: string, pricing?: PricingConfig, model?: string):
|
|
132
|
-
export declare function mapEvent(row: CursorRow, workspacePath: string, projectId?: string, pricing?: PricingConfig):
|
|
133
|
-
export declare function mapTranscriptTurn(turn: CursorTranscriptTurn, projectId?: string, pricing?: PricingConfig, model?: string):
|
|
134
|
+
export declare function mapRecentCommit(entry: RecentCommitSnapshot, projectId?: string, pricing?: PricingConfig, model?: string, modelResolution?: CursorPayloadMetadata["model_resolution"]): CursorPayload | null;
|
|
135
|
+
export declare function mapEvent(row: CursorRow, workspacePath: string, projectId?: string, pricing?: PricingConfig): CursorPayload | null;
|
|
136
|
+
export declare function mapTranscriptTurn(turn: CursorTranscriptTurn, projectId?: string, pricing?: PricingConfig, model?: string, modelResolution?: CursorPayloadMetadata["model_resolution"]): CursorPayload;
|
|
134
137
|
export {};
|
package/dist/readers/cursor.js
CHANGED
|
@@ -447,6 +447,62 @@ function toIsoFromMs(value) {
|
|
|
447
447
|
const date = new Date(value);
|
|
448
448
|
return Number.isNaN(date.getTime()) ? null : date.toISOString();
|
|
449
449
|
}
|
|
450
|
+
/** Coerce epoch ms/seconds, numeric strings, or ISO-8601 into an ISO timestamp. */
|
|
451
|
+
function coerceTranscriptTimestamp(value) {
|
|
452
|
+
if (value == null)
|
|
453
|
+
return null;
|
|
454
|
+
if (typeof value === "number")
|
|
455
|
+
return toIsoFromMs(value);
|
|
456
|
+
if (typeof value !== "string")
|
|
457
|
+
return null;
|
|
458
|
+
const trimmed = value.trim();
|
|
459
|
+
if (!trimmed)
|
|
460
|
+
return null;
|
|
461
|
+
const fromEpoch = toIsoString(trimmed);
|
|
462
|
+
if (fromEpoch)
|
|
463
|
+
return fromEpoch;
|
|
464
|
+
const ms = Date.parse(trimmed);
|
|
465
|
+
if (Number.isNaN(ms))
|
|
466
|
+
return null;
|
|
467
|
+
return new Date(ms).toISOString();
|
|
468
|
+
}
|
|
469
|
+
/** Prefer explicit per-line / per-message times when Cursor includes them. */
|
|
470
|
+
function extractLineOccurredAt(entry) {
|
|
471
|
+
const candidates = [
|
|
472
|
+
entry.timestamp,
|
|
473
|
+
entry.createdAt,
|
|
474
|
+
entry.unixMs,
|
|
475
|
+
entry.message?.timestamp,
|
|
476
|
+
entry.message?.createdAt,
|
|
477
|
+
];
|
|
478
|
+
for (const candidate of candidates) {
|
|
479
|
+
const iso = coerceTranscriptTimestamp(candidate);
|
|
480
|
+
if (iso)
|
|
481
|
+
return iso;
|
|
482
|
+
}
|
|
483
|
+
return null;
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* Spread turns across [start, end] when the JSONL has no per-message times.
|
|
487
|
+
* Prevents first-sync backfill from collapsing every turn onto lastUpdatedAt/mtime
|
|
488
|
+
* (DB90DV-605 weekly chart spike).
|
|
489
|
+
*/
|
|
490
|
+
function interpolateTurnOccurredAt(startIso, endIso, index, total, fallbackIso) {
|
|
491
|
+
if (total <= 0)
|
|
492
|
+
return fallbackIso;
|
|
493
|
+
// Single turn: prefer session end (lastUpdatedAt) — matches prior composer-header behavior.
|
|
494
|
+
if (total === 1)
|
|
495
|
+
return endIso ?? startIso ?? fallbackIso;
|
|
496
|
+
const startMs = startIso ? Date.parse(startIso) : NaN;
|
|
497
|
+
const endMs = endIso ? Date.parse(endIso) : NaN;
|
|
498
|
+
if (Number.isNaN(startMs) || Number.isNaN(endMs)) {
|
|
499
|
+
return startIso ?? endIso ?? fallbackIso;
|
|
500
|
+
}
|
|
501
|
+
if (endMs <= startMs)
|
|
502
|
+
return startIso ?? fallbackIso;
|
|
503
|
+
const ms = startMs + ((endMs - startMs) * index) / (total - 1);
|
|
504
|
+
return new Date(ms).toISOString();
|
|
505
|
+
}
|
|
450
506
|
function readComposerHeaders(baseDir) {
|
|
451
507
|
const userDir = baseDir ?? cursorUserDir();
|
|
452
508
|
const dbPath = join(userDir, "globalStorage", "state.vscdb");
|
|
@@ -491,6 +547,7 @@ function readComposerHeaders(baseDir) {
|
|
|
491
547
|
composerId,
|
|
492
548
|
name: typeof composer.name === "string" ? composer.name : null,
|
|
493
549
|
workspacePath: typeof uri?.fsPath === "string" ? uri.fsPath : null,
|
|
550
|
+
createdAt: toIsoFromMs(composer.createdAt),
|
|
494
551
|
lastUpdatedAt: toIsoFromMs(composer.lastUpdatedAt),
|
|
495
552
|
});
|
|
496
553
|
}
|
|
@@ -563,11 +620,15 @@ function workspaceFromTranscriptFile(filePath) {
|
|
|
563
620
|
const MAX_TRANSCRIPT_BYTES = 50 * 1024 * 1024; // 50 MB
|
|
564
621
|
export async function parseCursorTranscriptFile(filePath, composerHeaders, verbose = false) {
|
|
565
622
|
let fileSize = 0;
|
|
566
|
-
let
|
|
623
|
+
let fileMtimeIso = new Date().toISOString();
|
|
624
|
+
let fileBirthIso = null;
|
|
567
625
|
try {
|
|
568
626
|
const stat = statSync(filePath);
|
|
569
627
|
fileSize = stat.size;
|
|
570
|
-
|
|
628
|
+
fileMtimeIso = stat.mtime.toISOString();
|
|
629
|
+
if (stat.birthtime && !Number.isNaN(stat.birthtime.getTime()) && stat.birthtime.getTime() > 0) {
|
|
630
|
+
fileBirthIso = stat.birthtime.toISOString();
|
|
631
|
+
}
|
|
571
632
|
}
|
|
572
633
|
catch {
|
|
573
634
|
return [];
|
|
@@ -580,15 +641,25 @@ export async function parseCursorTranscriptFile(filePath, composerHeaders, verbo
|
|
|
580
641
|
}
|
|
581
642
|
const sessionId = basename(filePath, ".jsonl");
|
|
582
643
|
const header = composerHeaders.get(sessionId);
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
const
|
|
644
|
+
const sessionFallback = header?.lastUpdatedAt ?? fileMtimeIso;
|
|
645
|
+
const sessionStart = header?.createdAt ?? fileBirthIso ?? sessionFallback;
|
|
646
|
+
const sessionEnd = header?.lastUpdatedAt ?? fileMtimeIso;
|
|
647
|
+
const drafts = [];
|
|
586
648
|
let currentPromptParts = [];
|
|
587
649
|
let currentAssistantParts = [];
|
|
650
|
+
let currentTurnOccurredAt = null;
|
|
588
651
|
let turnIndex = 0;
|
|
589
652
|
const hasher = createHash("sha256");
|
|
590
653
|
const stream = createReadStream(filePath, { encoding: "utf-8" });
|
|
591
654
|
const rl = createInterface({ input: stream, crlfDelay: Infinity });
|
|
655
|
+
const noteLineTime = (entry) => {
|
|
656
|
+
const lineAt = extractLineOccurredAt(entry);
|
|
657
|
+
if (!lineAt)
|
|
658
|
+
return;
|
|
659
|
+
// Prefer the first timestamp in the turn (usually the user message).
|
|
660
|
+
if (!currentTurnOccurredAt)
|
|
661
|
+
currentTurnOccurredAt = lineAt;
|
|
662
|
+
};
|
|
592
663
|
const finalizeTurn = () => {
|
|
593
664
|
const promptText = currentPromptParts.join("\n\n").trim();
|
|
594
665
|
const assistantText = currentAssistantParts.join("\n\n").trim();
|
|
@@ -596,14 +667,14 @@ export async function parseCursorTranscriptFile(filePath, composerHeaders, verbo
|
|
|
596
667
|
return;
|
|
597
668
|
const risk = scanText(promptText);
|
|
598
669
|
turnIndex += 1;
|
|
599
|
-
|
|
670
|
+
drafts.push({
|
|
600
671
|
turnId: `${sessionId}:${turnIndex}`,
|
|
601
672
|
sessionId,
|
|
602
673
|
filePath,
|
|
603
674
|
fileSize,
|
|
604
675
|
workspacePath: header?.workspacePath ?? workspaceFromTranscriptFile(filePath),
|
|
605
676
|
composerName: header?.name ?? null,
|
|
606
|
-
|
|
677
|
+
turnOccurredAt: currentTurnOccurredAt,
|
|
607
678
|
promptText,
|
|
608
679
|
assistantText,
|
|
609
680
|
tokensIn: estimateTokens(promptText),
|
|
@@ -612,6 +683,7 @@ export async function parseCursorTranscriptFile(filePath, composerHeaders, verbo
|
|
|
612
683
|
riskScore: risk.risk_score,
|
|
613
684
|
riskCategories: risk.risk_categories,
|
|
614
685
|
});
|
|
686
|
+
currentTurnOccurredAt = null;
|
|
615
687
|
};
|
|
616
688
|
let lineNumber = 0;
|
|
617
689
|
try {
|
|
@@ -640,9 +712,11 @@ export async function parseCursorTranscriptFile(filePath, composerHeaders, verbo
|
|
|
640
712
|
currentPromptParts = [];
|
|
641
713
|
currentAssistantParts = [];
|
|
642
714
|
}
|
|
715
|
+
noteLineTime(entry);
|
|
643
716
|
currentPromptParts.push(...texts.map(stripUserQueryWrapper).filter((text) => text.length > 0));
|
|
644
717
|
}
|
|
645
718
|
else if (entry.role === "assistant") {
|
|
719
|
+
noteLineTime(entry);
|
|
646
720
|
currentAssistantParts.push(...texts.map((text) => text.trim()).filter((text) => text.length > 0));
|
|
647
721
|
}
|
|
648
722
|
}
|
|
@@ -659,9 +733,16 @@ export async function parseCursorTranscriptFile(filePath, composerHeaders, verbo
|
|
|
659
733
|
}
|
|
660
734
|
finalizeTurn();
|
|
661
735
|
const contentHash = hasher.digest("hex").slice(0, 32);
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
736
|
+
const total = drafts.length;
|
|
737
|
+
return drafts.map((draft, index) => {
|
|
738
|
+
const { turnOccurredAt, ...rest } = draft;
|
|
739
|
+
return {
|
|
740
|
+
...rest,
|
|
741
|
+
contentHash,
|
|
742
|
+
occurredAt: turnOccurredAt ??
|
|
743
|
+
interpolateTurnOccurredAt(sessionStart, sessionEnd, index, total, sessionFallback),
|
|
744
|
+
};
|
|
745
|
+
});
|
|
665
746
|
}
|
|
666
747
|
export async function readCursorTranscriptSessions(cursorUserBaseDir, transcriptProjectDirs, verbose = false) {
|
|
667
748
|
const composerHeaders = readComposerHeaders(cursorUserBaseDir);
|
|
@@ -730,7 +811,7 @@ function dailyStatsSessionId(date, eventType, modelKey) {
|
|
|
730
811
|
return `cursor:daily_stats:${date}:${suffix}`;
|
|
731
812
|
}
|
|
732
813
|
function buildPayload(opts) {
|
|
733
|
-
const { eventType, tokensIn, tokensOut, costUsd, occurredAt, dbPath, date, model = "unknown", modelKey, projectId, costModel = LINE_COST_MODEL, } = opts;
|
|
814
|
+
const { eventType, tokensIn, tokensOut, costUsd, occurredAt, dbPath, date, model = "unknown", modelResolution, modelKey, projectId, costModel = LINE_COST_MODEL, } = opts;
|
|
734
815
|
const payload = {
|
|
735
816
|
tool_name: "cursor",
|
|
736
817
|
event_type: eventType,
|
|
@@ -746,13 +827,14 @@ function buildPayload(opts) {
|
|
|
746
827
|
cost_model: costModel,
|
|
747
828
|
scannable: false,
|
|
748
829
|
risk_level: "none",
|
|
830
|
+
...(modelResolution !== undefined ? { model_resolution: modelResolution } : {}),
|
|
749
831
|
},
|
|
750
832
|
};
|
|
751
833
|
if (projectId)
|
|
752
834
|
payload.project_id = projectId;
|
|
753
835
|
return payload;
|
|
754
836
|
}
|
|
755
|
-
export function mapDailyStats(entry, projectId, pricing = DEFAULT_CURSOR_PRICING, model) {
|
|
837
|
+
export function mapDailyStats(entry, projectId, pricing = DEFAULT_CURSOR_PRICING, model, modelResolution) {
|
|
756
838
|
const { date, value, dbPath } = entry;
|
|
757
839
|
const occurredAt = `${date}T00:00:00.000Z`;
|
|
758
840
|
const results = [];
|
|
@@ -773,6 +855,7 @@ export function mapDailyStats(entry, projectId, pricing = DEFAULT_CURSOR_PRICING
|
|
|
773
855
|
dbPath,
|
|
774
856
|
date,
|
|
775
857
|
model,
|
|
858
|
+
modelResolution,
|
|
776
859
|
projectId,
|
|
777
860
|
}));
|
|
778
861
|
}
|
|
@@ -781,11 +864,12 @@ export function mapDailyStats(entry, projectId, pricing = DEFAULT_CURSOR_PRICING
|
|
|
781
864
|
eventType: "chat",
|
|
782
865
|
tokensIn: composerSuggested,
|
|
783
866
|
tokensOut: composerAccepted,
|
|
784
|
-
costUsd: computeLineCost("chat",
|
|
867
|
+
costUsd: computeLineCost("chat", composerSuggested, pricing),
|
|
785
868
|
occurredAt,
|
|
786
869
|
dbPath,
|
|
787
870
|
date,
|
|
788
871
|
model,
|
|
872
|
+
modelResolution,
|
|
789
873
|
projectId,
|
|
790
874
|
}));
|
|
791
875
|
}
|
|
@@ -820,7 +904,7 @@ export function mapDailyStats(entry, projectId, pricing = DEFAULT_CURSOR_PRICING
|
|
|
820
904
|
* Cursor only keeps one recent commit row (overwritten on each new commit).
|
|
821
905
|
* Line-cost math still follows the chat-style line proxy (`computeLineCost("chat", …)`); only `event_type` differs.
|
|
822
906
|
*/
|
|
823
|
-
export function mapRecentCommit(entry, projectId, pricing = DEFAULT_CURSOR_PRICING, model) {
|
|
907
|
+
export function mapRecentCommit(entry, projectId, pricing = DEFAULT_CURSOR_PRICING, model, modelResolution) {
|
|
824
908
|
const { value: obj, dbPath } = entry;
|
|
825
909
|
const occurredAt = toIsoString(obj.timestamp);
|
|
826
910
|
if (!occurredAt)
|
|
@@ -866,6 +950,7 @@ export function mapRecentCommit(entry, projectId, pricing = DEFAULT_CURSOR_PRICI
|
|
|
866
950
|
: undefined,
|
|
867
951
|
scannable: false,
|
|
868
952
|
risk_level: "none",
|
|
953
|
+
...(modelResolution !== undefined ? { model_resolution: modelResolution } : {}),
|
|
869
954
|
},
|
|
870
955
|
};
|
|
871
956
|
if (projectId)
|
|
@@ -902,7 +987,7 @@ export function mapEvent(row, workspacePath, projectId, pricing = DEFAULT_CURSOR
|
|
|
902
987
|
payload.project_id = projectId;
|
|
903
988
|
return payload;
|
|
904
989
|
}
|
|
905
|
-
export function mapTranscriptTurn(turn, projectId, pricing = DEFAULT_CURSOR_PRICING, model) {
|
|
990
|
+
export function mapTranscriptTurn(turn, projectId, pricing = DEFAULT_CURSOR_PRICING, model, modelResolution) {
|
|
906
991
|
const payload = {
|
|
907
992
|
tool_name: "cursor",
|
|
908
993
|
event_type: "chat",
|
|
@@ -924,6 +1009,7 @@ export function mapTranscriptTurn(turn, projectId, pricing = DEFAULT_CURSOR_PRIC
|
|
|
924
1009
|
composer_name: turn.composerName ?? undefined,
|
|
925
1010
|
prompt_text: turn.promptText || undefined,
|
|
926
1011
|
assistant_text: turn.assistantText || undefined,
|
|
1012
|
+
...(modelResolution !== undefined ? { model_resolution: modelResolution } : {}),
|
|
927
1013
|
},
|
|
928
1014
|
};
|
|
929
1015
|
if (projectId)
|
package/dist/server.d.ts
CHANGED
|
@@ -7,8 +7,25 @@ export declare const SYNC_NOW_INPUT_SCHEMA: z.ZodObject<{
|
|
|
7
7
|
cursor: "cursor";
|
|
8
8
|
}>>>;
|
|
9
9
|
}, z.core.$strict>;
|
|
10
|
-
/**
|
|
11
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Whether a `credential_validation_failed` warning should mirror to stderr.
|
|
12
|
+
* Mirrors for one-shot/startup sources so an installed-but-uninitialized MCP is
|
|
13
|
+
* visible, but NOT for the recurring background `"interval"` tick — that fires
|
|
14
|
+
* every SYNC_INTERVAL_MS for the whole process lifetime and would spam the logs.
|
|
15
|
+
* The event is always written to mcp.log regardless of this return value.
|
|
16
|
+
*/
|
|
17
|
+
export declare function shouldMirrorMissingCredentials(source: string): boolean;
|
|
18
|
+
/** Structured status for `aixle_insights_status` — tolerates missing/malformed credentials and state. */
|
|
19
|
+
export declare function buildAixleInsightsStatusPayload(): Promise<Record<string, unknown>>;
|
|
12
20
|
/** In-process MCP server instance (stdio not attached). */
|
|
13
|
-
export declare function
|
|
21
|
+
export declare function createAixleInsightsMcpServer(): McpServer;
|
|
22
|
+
/**
|
|
23
|
+
* Subscribes `shutdown` to the given stream's "end" and "close" events.
|
|
24
|
+
* Closing stdin is how the OS signals a stdio-transport MCP server that its
|
|
25
|
+
* parent process is gone — this fires even when the parent can't deliver a
|
|
26
|
+
* signal (e.g. it was itself SIGKILL'd, or the OS reparented this process
|
|
27
|
+
* without ever sending one). Exported standalone so it's testable with a
|
|
28
|
+
* plain EventEmitter instead of the real process.stdin.
|
|
29
|
+
*/
|
|
30
|
+
export declare function wireParentExitShutdown(stdin: Pick<NodeJS.ReadStream, "on">, shutdown: () => void): void;
|
|
14
31
|
export declare function startServer(): Promise<void>;
|