@aixle/insights 0.1.1 → 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 (53) hide show
  1. package/README.md +154 -3
  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 +10 -1
  7. package/dist/auth/flow.js +38 -5
  8. package/dist/auth/keycloak.d.ts +1 -1
  9. package/dist/auth/keycloak.js +20 -1
  10. package/dist/cli.d.ts +7 -3
  11. package/dist/cli.js +87 -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 +7 -0
  17. package/dist/cursor-settings.d.ts +9 -4
  18. package/dist/cursor-settings.js +80 -10
  19. package/dist/cursor-store-audit.d.ts +2 -2
  20. package/dist/cursor-store-audit.js +22 -11
  21. package/dist/daily-stats-versions.d.ts +3 -1
  22. package/dist/daily-stats-versions.js +6 -7
  23. package/dist/health.d.ts +3 -1
  24. package/dist/health.js +13 -1
  25. package/dist/hooks/cursor-hooks-mapper.d.ts +3 -3
  26. package/dist/hooks/cursor-hooks-mapper.js +1 -1
  27. package/dist/hooks/cursor-hooks-reader.d.ts +2 -0
  28. package/dist/hooks/cursor-hooks-reader.js +2 -2
  29. package/dist/install/cursor.d.ts +34 -0
  30. package/dist/install/cursor.js +193 -0
  31. package/dist/install/index.d.ts +6 -4
  32. package/dist/install/index.js +6 -1
  33. package/dist/lib/client.d.ts +7 -0
  34. package/dist/lib/client.js +17 -0
  35. package/dist/lib/config.js +7 -2
  36. package/dist/lib/project-resolver.d.ts +5 -4
  37. package/dist/lib/project-resolver.js +20 -8
  38. package/dist/lib/transport-security.d.ts +13 -0
  39. package/dist/lib/transport-security.js +47 -0
  40. package/dist/pricing.d.ts +9 -1
  41. package/dist/pricing.js +39 -8
  42. package/dist/readers/claude.d.ts +54 -6
  43. package/dist/readers/claude.js +158 -6
  44. package/dist/readers/cursor-sqlite.d.ts +23 -0
  45. package/dist/readers/cursor-sqlite.js +68 -0
  46. package/dist/readers/cursor.d.ts +11 -8
  47. package/dist/readers/cursor.js +149 -31
  48. package/dist/server.d.ts +20 -3
  49. package/dist/server.js +101 -67
  50. package/dist/state.js +7 -2
  51. package/dist/sync.d.ts +4 -2
  52. package/dist/sync.js +61 -46
  53. package/package.json +2 -2
package/dist/pricing.js CHANGED
@@ -2,7 +2,7 @@
2
2
  * Model pricing table and cost calculation for @aixle/insights.
3
3
  *
4
4
  * Default rates source: https://platform.claude.com/docs/en/about-claude/pricing
5
- * Rates last verified: 2026-05-04
5
+ * Rates last verified: 2026-06-17
6
6
  */
7
7
  /**
8
8
  * Default pricing table (USD per million tokens).
@@ -14,7 +14,20 @@
14
14
  * breakdown is a future improvement.
15
15
  */
16
16
  export const DEFAULT_PRICING = {
17
- // Opus 4.7/4.6/4.5 — $5 input / $25 output
17
+ // Fable 5 — $10 input / $50 output
18
+ "claude-fable-5": {
19
+ input_per_mtok: 10.0,
20
+ output_per_mtok: 50.0,
21
+ cache_write_per_mtok: 12.5,
22
+ cache_read_per_mtok: 1.0,
23
+ },
24
+ // Opus 4.8/4.7/4.6/4.5 — $5 input / $25 output
25
+ "claude-opus-4-8": {
26
+ input_per_mtok: 5.0,
27
+ output_per_mtok: 25.0,
28
+ cache_write_per_mtok: 6.25,
29
+ cache_read_per_mtok: 0.5,
30
+ },
18
31
  "claude-opus-4-7": {
19
32
  input_per_mtok: 5.0,
20
33
  output_per_mtok: 25.0,
@@ -47,26 +60,26 @@ export const DEFAULT_PRICING = {
47
60
  cache_read_per_mtok: 1.5,
48
61
  },
49
62
  // Sonnet 4.x family — $3 input / $15 output
50
- "claude-sonnet-4": {
63
+ "claude-sonnet-4-6": {
51
64
  input_per_mtok: 3.0,
52
65
  output_per_mtok: 15.0,
53
66
  cache_write_per_mtok: 3.75,
54
67
  cache_read_per_mtok: 0.3,
55
68
  },
56
- "claude-sonnet-4-6": {
69
+ "claude-sonnet-4-5": {
57
70
  input_per_mtok: 3.0,
58
71
  output_per_mtok: 15.0,
59
72
  cache_write_per_mtok: 3.75,
60
73
  cache_read_per_mtok: 0.3,
61
74
  },
62
- "claude-sonnet-4-5-20251001": {
75
+ "claude-sonnet-4": {
63
76
  input_per_mtok: 3.0,
64
77
  output_per_mtok: 15.0,
65
78
  cache_write_per_mtok: 3.75,
66
79
  cache_read_per_mtok: 0.3,
67
80
  },
68
81
  // Haiku 4.5 — $1 input / $5 output
69
- "claude-haiku-4-5-20251001": {
82
+ "claude-haiku-4-5": {
70
83
  input_per_mtok: 1.0,
71
84
  output_per_mtok: 5.0,
72
85
  cache_write_per_mtok: 1.25,
@@ -110,6 +123,22 @@ export function mergePricing(base, overrides) {
110
123
  }
111
124
  return result;
112
125
  }
126
+ /**
127
+ * Strips a trailing -YYYYMMDD date suffix to produce the bare model key used
128
+ * in the pricing table. Returns the original ID when:
129
+ * - it has no date suffix, OR
130
+ * - the bare form is not in the table (i.e. the dated form IS the canonical key,
131
+ * like legacy "claude-3-5-sonnet-20241022").
132
+ */
133
+ export function normalizeModelId(model, pricing) {
134
+ if (pricing[model])
135
+ return model;
136
+ const match = model.match(/^(.+)-(\d{8})$/);
137
+ if (!match)
138
+ return model;
139
+ const bare = match[1];
140
+ return pricing[bare] ? bare : model;
141
+ }
113
142
  /** Returns true only when all four rate fields are finite numbers. */
114
143
  function hasValidRates(rates) {
115
144
  return (Number.isFinite(rates.input_per_mtok) &&
@@ -120,7 +149,8 @@ function hasValidRates(rates) {
120
149
  export function calculateCost(model, baseInputTokens, outputTokens, cacheWriteTokens, cacheReadTokens, pricing) {
121
150
  if (!model)
122
151
  return null;
123
- const rates = pricing[model];
152
+ const resolved = normalizeModelId(model, pricing);
153
+ const rates = pricing[resolved];
124
154
  if (!rates || !hasValidRates(rates))
125
155
  return null;
126
156
  const raw = (baseInputTokens * rates.input_per_mtok +
@@ -138,7 +168,8 @@ export function calculateCost(model, baseInputTokens, outputTokens, cacheWriteTo
138
168
  export function getCostWarning(model, pricing) {
139
169
  if (!model)
140
170
  return null;
141
- const rates = pricing[model];
171
+ const resolved = normalizeModelId(model, pricing);
172
+ const rates = pricing[resolved];
142
173
  if (!rates) {
143
174
  return `Model "${model}" not in pricing table — cost_usd will be null. Extend DEFAULT_PRICING or add future pricing overrides when supported.`;
144
175
  }
@@ -1,6 +1,21 @@
1
1
  import type { IngestPayload } from "../lib/index.js";
2
2
  import { type PricingTable } from "../pricing.js";
3
3
  import { type RiskLevel } from "../risk-scanner.js";
4
+ export type ClaudeDerivativeEventType = "edit" | "commit" | "test" | "tool_use";
5
+ export interface ClaudeToolUseBlock {
6
+ id?: string;
7
+ name: string;
8
+ input?: Record<string, unknown>;
9
+ }
10
+ export interface ClaudeCollectedToolUse {
11
+ id: string;
12
+ name: string;
13
+ eventType: ClaudeDerivativeEventType;
14
+ summary: string;
15
+ }
16
+ export declare function classifyToolUse(block: ClaudeToolUseBlock): ClaudeDerivativeEventType | null;
17
+ export declare function scrubBashCommand(cmd: string): string;
18
+ export declare function summarizeToolUse(block: ClaudeToolUseBlock): string;
4
19
  /** True when prompt text alone matches known local-command injection markers. */
5
20
  export declare function isClaudeLocalCommandNoisePrompt(promptText: string): boolean;
6
21
  /**
@@ -41,9 +56,13 @@ export interface ClaudeTranscriptTurn {
41
56
  riskLevel: RiskLevel;
42
57
  riskScore: number;
43
58
  riskCategories: string[];
59
+ toolUses: ClaudeCollectedToolUse[];
60
+ navToolCalls: number;
61
+ totalToolCalls: number;
62
+ messageIds: string[];
44
63
  }
45
- /** Payload shape expected by the db90 ingest API. */
46
- export interface Db90Payload extends IngestPayload {
64
+ /** Payload shape for the parent chat turn (carries full token cost). */
65
+ export interface ClaudePayload extends IngestPayload {
47
66
  tool_name: "claude_code";
48
67
  event_type: "chat";
49
68
  model?: string;
@@ -57,7 +76,7 @@ export interface Db90Payload extends IngestPayload {
57
76
  session_id: string;
58
77
  claude_session_id: string;
59
78
  transcript_source: "claude_jsonl";
60
- model: string | null;
79
+ model?: string | null;
61
80
  base_input_tokens: number;
62
81
  output_tokens: number;
63
82
  cache_write_tokens: number;
@@ -68,10 +87,39 @@ export interface Db90Payload extends IngestPayload {
68
87
  prompt_text?: string;
69
88
  assistant_text?: string;
70
89
  scannable: true;
90
+ cost_model: "token_count";
91
+ nav_tool_calls: number;
92
+ total_tool_calls: number;
93
+ message_ids?: string[];
94
+ };
95
+ }
96
+ /** Payload shape for derivative tool-use children (cost_usd: 0, no tokens). */
97
+ export interface ClaudeDerivativePayload extends IngestPayload {
98
+ tool_name: "claude_code";
99
+ event_type: ClaudeDerivativeEventType;
100
+ cost_usd: 0;
101
+ occurred_at: string;
102
+ model?: string;
103
+ project_id?: string;
104
+ metadata: {
105
+ session_id: string;
106
+ claude_session_id: string;
107
+ transcript_source: "claude_jsonl";
108
+ cost_model: "derivative";
109
+ parent_session_id: string;
110
+ tool_name_inner: string;
111
+ tool_use_id: string;
112
+ summary: string;
113
+ scannable: false;
114
+ risk_level: "none";
115
+ risk_categories: string[];
116
+ risk_score: 0;
71
117
  };
72
118
  }
119
+ /** Union of all Claude transcript payloads expected by the ingest API. */
120
+ export type ClaudeMappedPayload = ClaudePayload | ClaudeDerivativePayload;
73
121
  /** Options for mapTranscriptTurn. */
74
- export interface ToDb90PayloadOptions {
122
+ export interface ToClaudePayloadOptions {
75
123
  projectId?: string | null;
76
124
  pricing?: PricingTable;
77
125
  }
@@ -79,5 +127,5 @@ export interface ToDb90PayloadOptions {
79
127
  export declare function findTranscriptFiles(baseDirs?: string[]): string[];
80
128
  /** Streams a JSONL file and splits Claude transcripts into individual turns. */
81
129
  export declare function parseTranscriptFile(filePath: string, verbose?: boolean): Promise<ClaudeTranscriptTurn[]>;
82
- /** Converts a Claude transcript turn to a db90 ingest payload. */
83
- export declare function mapTranscriptTurn(turn: ClaudeTranscriptTurn, options?: ToDb90PayloadOptions): Db90Payload;
130
+ /** Converts a Claude transcript turn to parent chat and derivative tool-use payloads. */
131
+ export declare function mapTranscriptTurn(turn: ClaudeTranscriptTurn, options?: ToClaudePayloadOptions): ClaudeMappedPayload[];
@@ -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,18 +420,50 @@ 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)
306
431
  payload.model = turn.model;
307
- if (turn.tokensIn > 0)
308
- payload.tokens_in = turn.tokensIn;
432
+ if (baseInputTokens > 0)
433
+ payload.tokens_in = baseInputTokens;
309
434
  if (turn.tokensOut > 0)
310
435
  payload.tokens_out = turn.tokensOut;
311
- if (turn.tokensIn > 0 || turn.tokensOut > 0) {
312
- payload.tokens_total = turn.tokensIn + turn.tokensOut;
436
+ if (baseInputTokens > 0 || turn.tokensOut > 0) {
437
+ payload.tokens_total = baseInputTokens + turn.tokensOut;
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
  }
@@ -0,0 +1,23 @@
1
+ import Database from "better-sqlite3";
2
+ export type CursorSqliteOpenFailureReason = "missing" | "outside_root" | "native_binding" | "unknown";
3
+ export type CursorSqliteOpenResult = {
4
+ ok: true;
5
+ db: Database.Database;
6
+ } | {
7
+ ok: false;
8
+ reason: CursorSqliteOpenFailureReason;
9
+ message: string;
10
+ };
11
+ export type CursorSqlitePathResult = {
12
+ ok: true;
13
+ path: string;
14
+ } | {
15
+ ok: false;
16
+ reason: CursorSqliteOpenFailureReason;
17
+ message: string;
18
+ };
19
+ export interface CursorSqliteOpenOptions {
20
+ rootDir?: string;
21
+ }
22
+ export declare function openCursorSqliteReadonly(dbPath: string, options?: CursorSqliteOpenOptions): CursorSqliteOpenResult;
23
+ export declare function resolveCursorSqlitePath(dbPath: string, options?: CursorSqliteOpenOptions): CursorSqlitePathResult;
@@ -0,0 +1,68 @@
1
+ import { existsSync, realpathSync } from "node:fs";
2
+ import { resolve, sep } from "node:path";
3
+ import Database from "better-sqlite3";
4
+ function validatedRealPathWithinRoot(path, rootDir) {
5
+ const realPath = realpathSync(path);
6
+ const realRoot = realpathSync(rootDir);
7
+ if (realPath === realRoot)
8
+ return realPath;
9
+ const rootWithSep = realRoot.endsWith(sep) ? realRoot : `${realRoot}${sep}`;
10
+ return realPath.startsWith(rootWithSep) ? realPath : null;
11
+ }
12
+ function isNativeBindingError(message) {
13
+ return (message.includes("Could not locate the bindings file") ||
14
+ message.includes("better_sqlite3.node") ||
15
+ message.includes("dlopen"));
16
+ }
17
+ export function openCursorSqliteReadonly(dbPath, options = {}) {
18
+ const resolved = resolveCursorSqlitePath(dbPath, options);
19
+ if (!resolved.ok)
20
+ return resolved;
21
+ try {
22
+ const db = new Database(resolved.path, { readonly: true, fileMustExist: true });
23
+ return { ok: true, db };
24
+ }
25
+ catch (err) {
26
+ const message = err instanceof Error ? err.message : String(err);
27
+ return {
28
+ ok: false,
29
+ reason: isNativeBindingError(message) ? "native_binding" : "unknown",
30
+ message,
31
+ };
32
+ }
33
+ }
34
+ export function resolveCursorSqlitePath(dbPath, options = {}) {
35
+ const normalizedPath = resolve(dbPath);
36
+ if (!existsSync(normalizedPath)) {
37
+ return {
38
+ ok: false,
39
+ reason: "missing",
40
+ message: `Database file does not exist: ${normalizedPath}`,
41
+ };
42
+ }
43
+ const { rootDir } = options;
44
+ let pathToOpen = normalizedPath;
45
+ if (rootDir) {
46
+ const normalizedRoot = resolve(rootDir);
47
+ try {
48
+ const realPath = validatedRealPathWithinRoot(normalizedPath, normalizedRoot);
49
+ if (realPath === null) {
50
+ return {
51
+ ok: false,
52
+ reason: "outside_root",
53
+ message: `Database path escapes root: ${normalizedPath}`,
54
+ };
55
+ }
56
+ pathToOpen = realPath;
57
+ }
58
+ catch (err) {
59
+ const message = err instanceof Error ? err.message : String(err);
60
+ return {
61
+ ok: false,
62
+ reason: "unknown",
63
+ message,
64
+ };
65
+ }
66
+ }
67
+ return { ok: true, path: pathToOpen };
68
+ }
@@ -2,7 +2,7 @@ import type { IngestPayload } from "../lib/index.js";
2
2
  import { type RiskLevel } from "../risk-scanner.js";
3
3
  export declare function cursorUserDir(): string;
4
4
  /** Smoke-test better-sqlite3 against the global Cursor state DB (CUR-V02 / verify scripts). */
5
- export declare function probeCursorGlobalStateDb(verbose?: boolean): boolean;
5
+ export declare function probeCursorGlobalStateDb(verbose?: boolean, baseDir?: string): boolean;
6
6
  export declare function findCursorDbs(baseDir?: string): string[];
7
7
  export interface CursorRow {
8
8
  requestId?: string | null;
@@ -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 {};