@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.
Files changed (45) hide show
  1. package/README.md +36 -8
  2. package/dist/auth/credentials.d.ts +7 -1
  3. package/dist/auth/credentials.js +71 -14
  4. package/dist/auth/exchange.d.ts +1 -1
  5. package/dist/auth/exchange.js +1 -1
  6. package/dist/auth/flow.d.ts +8 -1
  7. package/dist/auth/flow.js +27 -5
  8. package/dist/auth/keycloak.d.ts +1 -1
  9. package/dist/auth/keycloak.js +20 -1
  10. package/dist/cli.d.ts +5 -3
  11. package/dist/cli.js +69 -21
  12. package/dist/collect-cursor-payloads.d.ts +4 -3
  13. package/dist/collect-cursor-payloads.js +8 -5
  14. package/dist/cursor-checkpoints.d.ts +2 -2
  15. package/dist/cursor-payload-contract.d.ts +5 -5
  16. package/dist/cursor-payload-contract.js +6 -0
  17. package/dist/cursor-settings.d.ts +9 -4
  18. package/dist/cursor-settings.js +80 -10
  19. package/dist/health.d.ts +3 -1
  20. package/dist/health.js +13 -1
  21. package/dist/hooks/cursor-hooks-mapper.d.ts +3 -3
  22. package/dist/hooks/cursor-hooks-mapper.js +1 -1
  23. package/dist/hooks/cursor-hooks-reader.d.ts +2 -0
  24. package/dist/hooks/cursor-hooks-reader.js +2 -2
  25. package/dist/install/cursor.d.ts +34 -0
  26. package/dist/install/cursor.js +193 -0
  27. package/dist/install/index.d.ts +6 -4
  28. package/dist/install/index.js +6 -1
  29. package/dist/lib/client.d.ts +7 -0
  30. package/dist/lib/client.js +17 -0
  31. package/dist/lib/config.js +7 -2
  32. package/dist/lib/project-resolver.d.ts +5 -4
  33. package/dist/lib/project-resolver.js +20 -8
  34. package/dist/lib/transport-security.d.ts +1 -0
  35. package/dist/lib/transport-security.js +1 -1
  36. package/dist/readers/claude.d.ts +54 -6
  37. package/dist/readers/claude.js +154 -2
  38. package/dist/readers/cursor.d.ts +10 -7
  39. package/dist/readers/cursor.js +101 -15
  40. package/dist/server.d.ts +20 -3
  41. package/dist/server.js +101 -67
  42. package/dist/state.js +7 -2
  43. package/dist/sync.d.ts +4 -2
  44. package/dist/sync.js +61 -46
  45. package/package.json +2 -2
@@ -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 a db90 ingest payload. */
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
- return payload;
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
  }
@@ -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 Db90CursorPayloadMetadata = {
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 CursorDb90Payload extends IngestPayload {
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: Db90CursorPayloadMetadata;
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): CursorDb90Payload[];
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): CursorDb90Payload | null;
132
- export declare function mapEvent(row: CursorRow, workspacePath: string, projectId?: string, pricing?: PricingConfig): CursorDb90Payload | null;
133
- export declare function mapTranscriptTurn(turn: CursorTranscriptTurn, projectId?: string, pricing?: PricingConfig, model?: string): CursorDb90Payload;
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 {};
@@ -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 occurredAt = new Date().toISOString();
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
- occurredAt = stat.mtime.toISOString();
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
- if (header?.lastUpdatedAt)
584
- occurredAt = header.lastUpdatedAt;
585
- const turns = [];
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
- turns.push({
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
- occurredAt,
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
- for (const t of turns)
663
- t.contentHash = contentHash;
664
- return turns;
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", composerAccepted, pricing),
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
- /** Structured status for `db90_status` — tolerates missing/malformed credentials and state. */
11
- export declare function buildDb90StatusPayload(): Promise<Record<string, unknown>>;
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 createDb90McpServer(): McpServer;
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>;