@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.
- package/README.md +154 -3
- 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 +10 -1
- package/dist/auth/flow.js +38 -5
- package/dist/auth/keycloak.d.ts +1 -1
- package/dist/auth/keycloak.js +20 -1
- package/dist/cli.d.ts +7 -3
- package/dist/cli.js +87 -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 +7 -0
- package/dist/cursor-settings.d.ts +9 -4
- package/dist/cursor-settings.js +80 -10
- package/dist/cursor-store-audit.d.ts +2 -2
- package/dist/cursor-store-audit.js +22 -11
- package/dist/daily-stats-versions.d.ts +3 -1
- package/dist/daily-stats-versions.js +6 -7
- 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 +13 -0
- package/dist/lib/transport-security.js +47 -0
- package/dist/pricing.d.ts +9 -1
- package/dist/pricing.js +39 -8
- package/dist/readers/claude.d.ts +54 -6
- package/dist/readers/claude.js +158 -6
- package/dist/readers/cursor-sqlite.d.ts +23 -0
- package/dist/readers/cursor-sqlite.js +68 -0
- package/dist/readers/cursor.d.ts +11 -8
- package/dist/readers/cursor.js +149 -31
- 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/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-
|
|
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
|
-
//
|
|
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-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
}
|
package/dist/readers/claude.d.ts
CHANGED
|
@@ -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
|
|
46
|
-
export interface
|
|
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
|
|
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
|
|
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
|
|
83
|
-
export declare function mapTranscriptTurn(turn: ClaudeTranscriptTurn, options?:
|
|
130
|
+
/** Converts a Claude transcript turn to parent chat and derivative tool-use payloads. */
|
|
131
|
+
export declare function mapTranscriptTurn(turn: ClaudeTranscriptTurn, options?: ToClaudePayloadOptions): ClaudeMappedPayload[];
|
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,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 (
|
|
308
|
-
payload.tokens_in =
|
|
432
|
+
if (baseInputTokens > 0)
|
|
433
|
+
payload.tokens_in = baseInputTokens;
|
|
309
434
|
if (turn.tokensOut > 0)
|
|
310
435
|
payload.tokens_out = turn.tokensOut;
|
|
311
|
-
if (
|
|
312
|
-
payload.tokens_total =
|
|
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
|
-
|
|
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
|
+
}
|
package/dist/readers/cursor.d.ts
CHANGED
|
@@ -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
|
|
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 {};
|