@aixle/insights 0.1.0
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/LICENSE +21 -0
- package/README.md +137 -0
- package/dist/auth/credentials.d.ts +23 -0
- package/dist/auth/credentials.js +174 -0
- package/dist/auth/exchange.d.ts +25 -0
- package/dist/auth/exchange.js +87 -0
- package/dist/auth/flow.d.ts +24 -0
- package/dist/auth/flow.js +66 -0
- package/dist/auth/keycloak.d.ts +35 -0
- package/dist/auth/keycloak.js +170 -0
- package/dist/cli.d.ts +51 -0
- package/dist/cli.js +426 -0
- package/dist/client.d.ts +28 -0
- package/dist/client.js +102 -0
- package/dist/collect-cursor-payloads.d.ts +57 -0
- package/dist/collect-cursor-payloads.js +134 -0
- package/dist/credentials.d.ts +2 -0
- package/dist/credentials.js +1 -0
- package/dist/cursor-checkpoints.d.ts +12 -0
- package/dist/cursor-checkpoints.js +28 -0
- package/dist/cursor-config.d.ts +5 -0
- package/dist/cursor-config.js +34 -0
- package/dist/cursor-payload-contract.d.ts +17 -0
- package/dist/cursor-payload-contract.js +258 -0
- package/dist/cursor-settings.d.ts +6 -0
- package/dist/cursor-settings.js +38 -0
- package/dist/cursor-store-audit.d.ts +48 -0
- package/dist/cursor-store-audit.js +155 -0
- package/dist/daily-stats-versions.d.ts +31 -0
- package/dist/daily-stats-versions.js +170 -0
- package/dist/health.d.ts +31 -0
- package/dist/health.js +195 -0
- package/dist/hooks/cursor-hooks-mapper.d.ts +22 -0
- package/dist/hooks/cursor-hooks-mapper.js +84 -0
- package/dist/hooks/cursor-hooks-reader.d.ts +30 -0
- package/dist/hooks/cursor-hooks-reader.js +117 -0
- package/dist/hooks/hook-forwarder.mjs +110 -0
- package/dist/hooks/hooks-config.d.ts +92 -0
- package/dist/hooks/hooks-config.js +235 -0
- package/dist/install/claude.d.ts +37 -0
- package/dist/install/claude.js +144 -0
- package/dist/install/index.d.ts +8 -0
- package/dist/install/index.js +11 -0
- package/dist/lib/args.d.ts +26 -0
- package/dist/lib/args.js +17 -0
- package/dist/lib/client.d.ts +33 -0
- package/dist/lib/client.js +52 -0
- package/dist/lib/config.d.ts +26 -0
- package/dist/lib/config.js +39 -0
- package/dist/lib/index.d.ts +4 -0
- package/dist/lib/index.js +4 -0
- package/dist/lib/project-resolver.d.ts +48 -0
- package/dist/lib/project-resolver.js +203 -0
- package/dist/lock.d.ts +9 -0
- package/dist/lock.js +84 -0
- package/dist/log.d.ts +14 -0
- package/dist/log.js +81 -0
- package/dist/pricing.d.ts +40 -0
- package/dist/pricing.js +149 -0
- package/dist/readers/claude.d.ts +83 -0
- package/dist/readers/claude.js +317 -0
- package/dist/readers/cursor.d.ts +134 -0
- package/dist/readers/cursor.js +900 -0
- package/dist/risk-scanner.d.ts +8 -0
- package/dist/risk-scanner.js +59 -0
- package/dist/server.d.ts +14 -0
- package/dist/server.js +234 -0
- package/dist/state.d.ts +69 -0
- package/dist/state.js +155 -0
- package/dist/sync.d.ts +74 -0
- package/dist/sync.js +679 -0
- package/package.json +66 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { IngestPayload } from "../lib/index.js";
|
|
2
|
+
import { type PricingTable } from "../pricing.js";
|
|
3
|
+
import { type RiskLevel } from "../risk-scanner.js";
|
|
4
|
+
/** True when prompt text alone matches known local-command injection markers. */
|
|
5
|
+
export declare function isClaudeLocalCommandNoisePrompt(promptText: string): boolean;
|
|
6
|
+
/**
|
|
7
|
+
* Returns true for transcript turns that are Claude Code local-command noise
|
|
8
|
+
* (caveat, /exit, stdout) with no model usage — safe to omit from ingest.
|
|
9
|
+
*/
|
|
10
|
+
export declare function isClaudeNoiseTranscriptTurn(turn: ClaudeTranscriptTurn): boolean;
|
|
11
|
+
/**
|
|
12
|
+
* True for turns that look mid-flight: a user prompt is present but the
|
|
13
|
+
* assistant hasn't responded yet (no text, no model, no output tokens).
|
|
14
|
+
*
|
|
15
|
+
* Skipping these is safe: the turn is NOT checkpointed (we never push it to
|
|
16
|
+
* finalizedTurns), so the next sync cycle re-parses the JSONL and persists
|
|
17
|
+
* the now-complete turn once the assistant has finished writing.
|
|
18
|
+
*
|
|
19
|
+
* Genuinely interrupted turns where the assistant never writes anything are
|
|
20
|
+
* also dropped by this guard — that's correct. The classic example is the
|
|
21
|
+
* "[Request interrupted by user for tool use]" placeholder Claude Code
|
|
22
|
+
* injects after a tool-use interruption; it's a system marker, not real
|
|
23
|
+
* user activity, so losing it produces cleaner Events table rows.
|
|
24
|
+
*/
|
|
25
|
+
export declare function isIncompleteTranscriptTurn(turn: ClaudeTranscriptTurn): boolean;
|
|
26
|
+
export interface ClaudeTranscriptTurn {
|
|
27
|
+
sessionId: string;
|
|
28
|
+
turnId: string;
|
|
29
|
+
promptId?: string;
|
|
30
|
+
filePath: string;
|
|
31
|
+
fileSize: number;
|
|
32
|
+
cwd?: string;
|
|
33
|
+
model: string | null;
|
|
34
|
+
tokensIn: number;
|
|
35
|
+
tokensOut: number;
|
|
36
|
+
cacheWriteTokens: number;
|
|
37
|
+
cacheReadTokens: number;
|
|
38
|
+
occurredAt: string;
|
|
39
|
+
promptText: string;
|
|
40
|
+
assistantText: string;
|
|
41
|
+
riskLevel: RiskLevel;
|
|
42
|
+
riskScore: number;
|
|
43
|
+
riskCategories: string[];
|
|
44
|
+
}
|
|
45
|
+
/** Payload shape expected by the db90 ingest API. */
|
|
46
|
+
export interface Db90Payload extends IngestPayload {
|
|
47
|
+
tool_name: "claude_code";
|
|
48
|
+
event_type: "chat";
|
|
49
|
+
model?: string;
|
|
50
|
+
tokens_in?: number;
|
|
51
|
+
tokens_out?: number;
|
|
52
|
+
tokens_total?: number;
|
|
53
|
+
cost_usd: number | null;
|
|
54
|
+
occurred_at: string;
|
|
55
|
+
project_id?: string;
|
|
56
|
+
metadata: {
|
|
57
|
+
session_id: string;
|
|
58
|
+
claude_session_id: string;
|
|
59
|
+
transcript_source: "claude_jsonl";
|
|
60
|
+
model: string | null;
|
|
61
|
+
base_input_tokens: number;
|
|
62
|
+
output_tokens: number;
|
|
63
|
+
cache_write_tokens: number;
|
|
64
|
+
cache_read_tokens: number;
|
|
65
|
+
risk_level: RiskLevel;
|
|
66
|
+
risk_categories: string[];
|
|
67
|
+
risk_score: number;
|
|
68
|
+
prompt_text?: string;
|
|
69
|
+
assistant_text?: string;
|
|
70
|
+
scannable: true;
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/** Options for mapTranscriptTurn. */
|
|
74
|
+
export interface ToDb90PayloadOptions {
|
|
75
|
+
projectId?: string | null;
|
|
76
|
+
pricing?: PricingTable;
|
|
77
|
+
}
|
|
78
|
+
/** Finds all *.jsonl transcript files across both Claude project directory roots. */
|
|
79
|
+
export declare function findTranscriptFiles(baseDirs?: string[]): string[];
|
|
80
|
+
/** Streams a JSONL file and splits Claude transcripts into individual turns. */
|
|
81
|
+
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;
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import { createReadStream, statSync } from "node:fs";
|
|
2
|
+
import { finished } from "node:stream/promises";
|
|
3
|
+
import { createInterface } from "node:readline";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { glob } from "glob";
|
|
7
|
+
import { calculateCost } from "../pricing.js";
|
|
8
|
+
import { scanText } from "../risk-scanner.js";
|
|
9
|
+
/** Prompt substrings emitted for local IDE commands — not real user prompts. */
|
|
10
|
+
const LOCAL_COMMAND_NOISE_PROMPT_PATTERNS = [
|
|
11
|
+
/<local-command-caveat\b/i,
|
|
12
|
+
/<local-command-stdout\b/i,
|
|
13
|
+
/<command-name>/i,
|
|
14
|
+
];
|
|
15
|
+
function hasZeroTokenUsage(turn) {
|
|
16
|
+
return (turn.tokensIn === 0 &&
|
|
17
|
+
turn.tokensOut === 0 &&
|
|
18
|
+
turn.cacheWriteTokens === 0 &&
|
|
19
|
+
turn.cacheReadTokens === 0);
|
|
20
|
+
}
|
|
21
|
+
/** True when prompt text alone matches known local-command injection markers. */
|
|
22
|
+
export function isClaudeLocalCommandNoisePrompt(promptText) {
|
|
23
|
+
const prompt = promptText.trim();
|
|
24
|
+
if (!prompt)
|
|
25
|
+
return false;
|
|
26
|
+
return LOCAL_COMMAND_NOISE_PROMPT_PATTERNS.some((pattern) => pattern.test(prompt));
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Returns true for transcript turns that are Claude Code local-command noise
|
|
30
|
+
* (caveat, /exit, stdout) with no model usage — safe to omit from ingest.
|
|
31
|
+
*/
|
|
32
|
+
export function isClaudeNoiseTranscriptTurn(turn) {
|
|
33
|
+
if (!hasZeroTokenUsage(turn))
|
|
34
|
+
return false;
|
|
35
|
+
if (turn.model !== null)
|
|
36
|
+
return false;
|
|
37
|
+
if (turn.assistantText.trim().length > 0)
|
|
38
|
+
return false;
|
|
39
|
+
return isClaudeLocalCommandNoisePrompt(turn.promptText);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* True for turns that look mid-flight: a user prompt is present but the
|
|
43
|
+
* assistant hasn't responded yet (no text, no model, no output tokens).
|
|
44
|
+
*
|
|
45
|
+
* Skipping these is safe: the turn is NOT checkpointed (we never push it to
|
|
46
|
+
* finalizedTurns), so the next sync cycle re-parses the JSONL and persists
|
|
47
|
+
* the now-complete turn once the assistant has finished writing.
|
|
48
|
+
*
|
|
49
|
+
* Genuinely interrupted turns where the assistant never writes anything are
|
|
50
|
+
* also dropped by this guard — that's correct. The classic example is the
|
|
51
|
+
* "[Request interrupted by user for tool use]" placeholder Claude Code
|
|
52
|
+
* injects after a tool-use interruption; it's a system marker, not real
|
|
53
|
+
* user activity, so losing it produces cleaner Events table rows.
|
|
54
|
+
*/
|
|
55
|
+
export function isIncompleteTranscriptTurn(turn) {
|
|
56
|
+
return (turn.model === null &&
|
|
57
|
+
turn.assistantText.trim().length === 0 &&
|
|
58
|
+
turn.tokensOut === 0);
|
|
59
|
+
}
|
|
60
|
+
/** Returns the two candidate Claude project directories (v1.0.30+ and legacy). */
|
|
61
|
+
function claudeProjectDirs() {
|
|
62
|
+
const home = homedir();
|
|
63
|
+
return [
|
|
64
|
+
join(home, ".config", "claude", "projects"),
|
|
65
|
+
join(home, ".claude", "projects"),
|
|
66
|
+
];
|
|
67
|
+
}
|
|
68
|
+
/** Finds all *.jsonl transcript files across both Claude project directory roots. */
|
|
69
|
+
export function findTranscriptFiles(baseDirs) {
|
|
70
|
+
const dirs = baseDirs ?? claudeProjectDirs();
|
|
71
|
+
const files = [];
|
|
72
|
+
for (const dir of dirs) {
|
|
73
|
+
try {
|
|
74
|
+
const matches = glob.sync("**/*.jsonl", { cwd: dir, absolute: true });
|
|
75
|
+
files.push(...matches);
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
// directory does not exist — skip
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// De-duplicate in case both paths resolve to the same files (symlinks, etc.)
|
|
82
|
+
return [...new Set(files)];
|
|
83
|
+
}
|
|
84
|
+
/** Extracts text strings from a content field (block array or plain string). */
|
|
85
|
+
function extractContentText(content) {
|
|
86
|
+
if (typeof content === "string")
|
|
87
|
+
return [content];
|
|
88
|
+
if (!Array.isArray(content))
|
|
89
|
+
return [];
|
|
90
|
+
return content.flatMap((block) => {
|
|
91
|
+
if (typeof block !== "object" || block === null)
|
|
92
|
+
return [];
|
|
93
|
+
const { type, text } = block;
|
|
94
|
+
return type === "text" && typeof text === "string" ? [text] : [];
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
function hasTextContent(content) {
|
|
98
|
+
if (typeof content === "string")
|
|
99
|
+
return content.trim().length > 0;
|
|
100
|
+
if (!Array.isArray(content))
|
|
101
|
+
return false;
|
|
102
|
+
return content.some((block) => {
|
|
103
|
+
if (typeof block !== "object" || block === null)
|
|
104
|
+
return false;
|
|
105
|
+
const { type, text } = block;
|
|
106
|
+
return type === "text" && typeof text === "string" && text.trim().length > 0;
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
function newTurn(sessionId, turnIndex, filePath, fileSize, occurredAt, promptId) {
|
|
110
|
+
return {
|
|
111
|
+
sessionId,
|
|
112
|
+
turnId: `${sessionId}:${turnIndex}`,
|
|
113
|
+
promptId,
|
|
114
|
+
filePath,
|
|
115
|
+
fileSize,
|
|
116
|
+
cwd: undefined,
|
|
117
|
+
model: null,
|
|
118
|
+
tokensIn: 0,
|
|
119
|
+
tokensOut: 0,
|
|
120
|
+
cacheWriteTokens: 0,
|
|
121
|
+
cacheReadTokens: 0,
|
|
122
|
+
occurredAt,
|
|
123
|
+
promptText: "",
|
|
124
|
+
assistantText: "",
|
|
125
|
+
riskLevel: "low",
|
|
126
|
+
riskScore: 0,
|
|
127
|
+
riskCategories: [],
|
|
128
|
+
persisted: false,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
function appendText(existing, addition) {
|
|
132
|
+
if (!addition.trim())
|
|
133
|
+
return existing;
|
|
134
|
+
return existing ? `${existing}\n\n${addition}` : addition;
|
|
135
|
+
}
|
|
136
|
+
function enrichTurnRisk(turn) {
|
|
137
|
+
if (!turn.promptText.trim())
|
|
138
|
+
return;
|
|
139
|
+
const result = scanText(turn.promptText);
|
|
140
|
+
turn.riskLevel = result.risk_level;
|
|
141
|
+
turn.riskScore = result.risk_score;
|
|
142
|
+
turn.riskCategories = result.risk_categories;
|
|
143
|
+
}
|
|
144
|
+
/** Streams a JSONL file and splits Claude transcripts into individual turns. */
|
|
145
|
+
export async function parseTranscriptFile(filePath, verbose = false) {
|
|
146
|
+
const turns = [];
|
|
147
|
+
let fileSize = 0;
|
|
148
|
+
try {
|
|
149
|
+
fileSize = statSync(filePath).size;
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
return turns;
|
|
153
|
+
}
|
|
154
|
+
const stream = createReadStream(filePath, { encoding: "utf-8" });
|
|
155
|
+
const rl = createInterface({
|
|
156
|
+
input: stream,
|
|
157
|
+
crlfDelay: Infinity,
|
|
158
|
+
});
|
|
159
|
+
let lineNumber = 0;
|
|
160
|
+
let currentTurn = null;
|
|
161
|
+
let currentTurnIndex = 0;
|
|
162
|
+
const turnsByPromptId = new Map();
|
|
163
|
+
const finalizedTurns = [];
|
|
164
|
+
const flushCurrentTurn = () => {
|
|
165
|
+
if (!currentTurn)
|
|
166
|
+
return;
|
|
167
|
+
if (!currentTurn.persisted) {
|
|
168
|
+
enrichTurnRisk(currentTurn);
|
|
169
|
+
const hasContent = currentTurn.promptText.trim().length > 0 || currentTurn.assistantText.trim().length > 0;
|
|
170
|
+
if (hasContent &&
|
|
171
|
+
!isClaudeNoiseTranscriptTurn(currentTurn) &&
|
|
172
|
+
!isIncompleteTranscriptTurn(currentTurn)) {
|
|
173
|
+
currentTurn.persisted = true;
|
|
174
|
+
finalizedTurns.push(currentTurn);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
currentTurn = null;
|
|
178
|
+
};
|
|
179
|
+
try {
|
|
180
|
+
for await (const line of rl) {
|
|
181
|
+
lineNumber++;
|
|
182
|
+
const trimmed = line.trim();
|
|
183
|
+
if (!trimmed)
|
|
184
|
+
continue;
|
|
185
|
+
let entry;
|
|
186
|
+
try {
|
|
187
|
+
entry = JSON.parse(trimmed);
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
if (verbose) {
|
|
191
|
+
console.warn(`[warn] ${filePath}:${lineNumber} — invalid JSON, skipping`);
|
|
192
|
+
}
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
const sessionId = entry.sessionId;
|
|
196
|
+
const promptId = entry.promptId;
|
|
197
|
+
const timestamp = entry.timestamp ?? new Date().toISOString();
|
|
198
|
+
const cwd = typeof entry.cwd === "string" && entry.cwd.trim().length > 0 ? entry.cwd : undefined;
|
|
199
|
+
if (entry.type === "user") {
|
|
200
|
+
if (!sessionId || !entry.message?.content)
|
|
201
|
+
continue;
|
|
202
|
+
const text = extractContentText(entry.message.content).join("\n\n").trim();
|
|
203
|
+
// Claude emits tool_result-only user entries after assistant tool_use.
|
|
204
|
+
// Those are part of the active turn, not a new user prompt.
|
|
205
|
+
if (hasTextContent(entry.message.content)) {
|
|
206
|
+
if (entry.isMeta === true || isClaudeLocalCommandNoisePrompt(text)) {
|
|
207
|
+
if (verbose) {
|
|
208
|
+
console.log("[verbose] Skipping Claude local-command/meta user line");
|
|
209
|
+
}
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
flushCurrentTurn();
|
|
213
|
+
currentTurnIndex += 1;
|
|
214
|
+
currentTurn = newTurn(sessionId, currentTurnIndex, filePath, fileSize, timestamp, promptId);
|
|
215
|
+
currentTurn.cwd = cwd ?? currentTurn.cwd;
|
|
216
|
+
currentTurn.promptText = appendText(currentTurn.promptText, text);
|
|
217
|
+
currentTurn.occurredAt = timestamp;
|
|
218
|
+
if (promptId)
|
|
219
|
+
turnsByPromptId.set(promptId, currentTurn);
|
|
220
|
+
}
|
|
221
|
+
else if (promptId && turnsByPromptId.has(promptId)) {
|
|
222
|
+
currentTurn = turnsByPromptId.get(promptId) ?? null;
|
|
223
|
+
if (currentTurn) {
|
|
224
|
+
currentTurn.cwd = cwd ?? currentTurn.cwd;
|
|
225
|
+
currentTurn.occurredAt = timestamp > currentTurn.occurredAt ? timestamp : currentTurn.occurredAt;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
else if (currentTurn && currentTurn.sessionId === sessionId) {
|
|
229
|
+
currentTurn.cwd = cwd ?? currentTurn.cwd;
|
|
230
|
+
currentTurn.occurredAt = timestamp > currentTurn.occurredAt ? timestamp : currentTurn.occurredAt;
|
|
231
|
+
}
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
if (entry.type === "assistant") {
|
|
235
|
+
if (!sessionId || !entry.message)
|
|
236
|
+
continue;
|
|
237
|
+
if (!currentTurn || currentTurn.sessionId !== sessionId) {
|
|
238
|
+
currentTurnIndex += 1;
|
|
239
|
+
currentTurn = newTurn(sessionId, currentTurnIndex, filePath, fileSize, timestamp);
|
|
240
|
+
}
|
|
241
|
+
currentTurn.cwd = cwd ?? currentTurn.cwd;
|
|
242
|
+
const usage = entry.message.usage;
|
|
243
|
+
if (usage) {
|
|
244
|
+
currentTurn.tokensIn +=
|
|
245
|
+
(usage.input_tokens ?? 0) +
|
|
246
|
+
(usage.cache_creation_input_tokens ?? 0) +
|
|
247
|
+
(usage.cache_read_input_tokens ?? 0);
|
|
248
|
+
currentTurn.tokensOut += usage.output_tokens ?? 0;
|
|
249
|
+
currentTurn.cacheWriteTokens += usage.cache_creation_input_tokens ?? 0;
|
|
250
|
+
currentTurn.cacheReadTokens += usage.cache_read_input_tokens ?? 0;
|
|
251
|
+
}
|
|
252
|
+
else if (verbose) {
|
|
253
|
+
console.warn(`[warn] ${filePath}:${lineNumber} — assistant message has no usage`);
|
|
254
|
+
}
|
|
255
|
+
if (entry.message.model)
|
|
256
|
+
currentTurn.model = entry.message.model;
|
|
257
|
+
currentTurn.occurredAt = timestamp > currentTurn.occurredAt ? timestamp : currentTurn.occurredAt;
|
|
258
|
+
const text = extractContentText(entry.message.content).join("\n\n").trim();
|
|
259
|
+
currentTurn.assistantText = appendText(currentTurn.assistantText, text);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
catch (err) {
|
|
264
|
+
if (verbose) {
|
|
265
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
266
|
+
console.warn(`[warn] ${filePath} — stream error, skipping file: ${message}`);
|
|
267
|
+
}
|
|
268
|
+
rl.close();
|
|
269
|
+
stream.destroy();
|
|
270
|
+
await finished(stream).catch(() => undefined);
|
|
271
|
+
return turns;
|
|
272
|
+
}
|
|
273
|
+
flushCurrentTurn();
|
|
274
|
+
return finalizedTurns.map(({ persisted: _persisted, ...turn }) => turn);
|
|
275
|
+
}
|
|
276
|
+
/** Converts a Claude transcript turn to a db90 ingest payload. */
|
|
277
|
+
export function mapTranscriptTurn(turn, options) {
|
|
278
|
+
const { projectId, pricing } = options ?? {};
|
|
279
|
+
const baseInputTokens = Math.max(0, turn.tokensIn - turn.cacheWriteTokens - turn.cacheReadTokens);
|
|
280
|
+
const cost = pricing
|
|
281
|
+
? calculateCost(turn.model, baseInputTokens, turn.tokensOut, turn.cacheWriteTokens, turn.cacheReadTokens, pricing)
|
|
282
|
+
: null;
|
|
283
|
+
const payload = {
|
|
284
|
+
tool_name: "claude_code",
|
|
285
|
+
event_type: "chat",
|
|
286
|
+
cost_usd: cost,
|
|
287
|
+
occurred_at: turn.occurredAt,
|
|
288
|
+
metadata: {
|
|
289
|
+
session_id: turn.turnId,
|
|
290
|
+
claude_session_id: turn.sessionId,
|
|
291
|
+
transcript_source: "claude_jsonl",
|
|
292
|
+
model: turn.model,
|
|
293
|
+
base_input_tokens: baseInputTokens,
|
|
294
|
+
output_tokens: turn.tokensOut,
|
|
295
|
+
cache_write_tokens: turn.cacheWriteTokens,
|
|
296
|
+
cache_read_tokens: turn.cacheReadTokens,
|
|
297
|
+
risk_level: turn.riskLevel,
|
|
298
|
+
risk_categories: turn.riskCategories,
|
|
299
|
+
risk_score: turn.riskScore,
|
|
300
|
+
prompt_text: turn.promptText || undefined,
|
|
301
|
+
assistant_text: turn.assistantText || undefined,
|
|
302
|
+
scannable: true,
|
|
303
|
+
},
|
|
304
|
+
};
|
|
305
|
+
if (turn.model)
|
|
306
|
+
payload.model = turn.model;
|
|
307
|
+
if (turn.tokensIn > 0)
|
|
308
|
+
payload.tokens_in = turn.tokensIn;
|
|
309
|
+
if (turn.tokensOut > 0)
|
|
310
|
+
payload.tokens_out = turn.tokensOut;
|
|
311
|
+
if (turn.tokensIn > 0 || turn.tokensOut > 0) {
|
|
312
|
+
payload.tokens_total = turn.tokensIn + turn.tokensOut;
|
|
313
|
+
}
|
|
314
|
+
if (projectId)
|
|
315
|
+
payload.project_id = projectId;
|
|
316
|
+
return payload;
|
|
317
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import type { IngestPayload } from "../lib/index.js";
|
|
2
|
+
import { type RiskLevel } from "../risk-scanner.js";
|
|
3
|
+
export declare function cursorUserDir(): string;
|
|
4
|
+
/** Smoke-test better-sqlite3 against the global Cursor state DB (CUR-V02 / verify scripts). */
|
|
5
|
+
export declare function probeCursorGlobalStateDb(verbose?: boolean): boolean;
|
|
6
|
+
export declare function findCursorDbs(baseDir?: string): string[];
|
|
7
|
+
export interface CursorRow {
|
|
8
|
+
requestId?: string | null;
|
|
9
|
+
timestamp?: number | string | null;
|
|
10
|
+
model?: string | null;
|
|
11
|
+
promptTokens?: number | null;
|
|
12
|
+
generatedTokens?: number | null;
|
|
13
|
+
type?: number | null;
|
|
14
|
+
sessionId?: string | null;
|
|
15
|
+
[key: string]: unknown;
|
|
16
|
+
}
|
|
17
|
+
export declare function readLegacyEvents(since: Date | null, baseDir?: string, verbose?: boolean): Array<{
|
|
18
|
+
row: CursorRow;
|
|
19
|
+
workspacePath: string;
|
|
20
|
+
}>;
|
|
21
|
+
export interface DailyStatsEntry {
|
|
22
|
+
date: string;
|
|
23
|
+
value: unknown;
|
|
24
|
+
dbPath: string;
|
|
25
|
+
}
|
|
26
|
+
export declare function isGlobalStateDbPath(dbPath: string): boolean;
|
|
27
|
+
type WorkspaceScope = "global" | "workspace";
|
|
28
|
+
export declare function dedupeDailyStatsEntries(entries: DailyStatsEntry[]): DailyStatsEntry[];
|
|
29
|
+
export declare function findStateVscDbs(baseDir?: string): string[];
|
|
30
|
+
export interface DailyStatsReadResult {
|
|
31
|
+
raw: DailyStatsEntry[];
|
|
32
|
+
deduped: DailyStatsEntry[];
|
|
33
|
+
}
|
|
34
|
+
export declare function readDailyStatsWithDedupe(since: Date | null, baseDir?: string, verbose?: boolean): DailyStatsReadResult;
|
|
35
|
+
export declare function readDailyStats(since: Date | null, baseDir?: string, verbose?: boolean): DailyStatsEntry[];
|
|
36
|
+
export interface RecentCommitSnapshot {
|
|
37
|
+
value: Record<string, unknown>;
|
|
38
|
+
dbPath: string;
|
|
39
|
+
}
|
|
40
|
+
export declare function readRecentCommitSnapshots(since: Date | null, baseDir?: string, verbose?: boolean): RecentCommitSnapshot[];
|
|
41
|
+
export declare function readEvents(since: Date | null, baseDir?: string, verbose?: boolean): Array<{
|
|
42
|
+
row: CursorRow;
|
|
43
|
+
workspacePath: string;
|
|
44
|
+
}>;
|
|
45
|
+
interface CursorComposerHeader {
|
|
46
|
+
composerId: string;
|
|
47
|
+
name: string | null;
|
|
48
|
+
workspacePath: string | null;
|
|
49
|
+
lastUpdatedAt: string | null;
|
|
50
|
+
}
|
|
51
|
+
export interface CursorTranscriptTurn {
|
|
52
|
+
turnId: string;
|
|
53
|
+
sessionId: string;
|
|
54
|
+
filePath: string;
|
|
55
|
+
fileSize: number;
|
|
56
|
+
/** SHA-256 prefix (32 hex chars) of the JSONL content — preferred over fileSize for change detection. */
|
|
57
|
+
contentHash?: string;
|
|
58
|
+
workspacePath: string | null;
|
|
59
|
+
composerName: string | null;
|
|
60
|
+
occurredAt: string;
|
|
61
|
+
promptText: string;
|
|
62
|
+
assistantText: string;
|
|
63
|
+
tokensIn: number;
|
|
64
|
+
tokensOut: number;
|
|
65
|
+
riskLevel: RiskLevel;
|
|
66
|
+
riskScore: number;
|
|
67
|
+
riskCategories: string[];
|
|
68
|
+
}
|
|
69
|
+
export declare function findCursorTranscriptFiles(projectDirs?: string[]): string[];
|
|
70
|
+
export declare function parseCursorTranscriptFile(filePath: string, composerHeaders: Map<string, CursorComposerHeader>, verbose?: boolean): Promise<CursorTranscriptTurn[]>;
|
|
71
|
+
export declare function readCursorTranscriptSessions(cursorUserBaseDir?: string, transcriptProjectDirs?: string[], verbose?: boolean): Promise<CursorTranscriptTurn[]>;
|
|
72
|
+
declare const LINE_COST_MODEL: "estimated_line_count";
|
|
73
|
+
declare const TOKEN_COST_MODEL: "token_count";
|
|
74
|
+
declare const TRANSCRIPT_COST_MODEL: "estimated_transcript_text";
|
|
75
|
+
export declare const HOOK_COST_MODEL: "cursor_hook";
|
|
76
|
+
type CursorLineCostModel = typeof LINE_COST_MODEL;
|
|
77
|
+
type CursorTokenCostModel = typeof TOKEN_COST_MODEL;
|
|
78
|
+
export type CursorHookCostModel = typeof HOOK_COST_MODEL;
|
|
79
|
+
export interface PricingConfig {
|
|
80
|
+
tokens_per_line: number;
|
|
81
|
+
completion_output_per_mtok: number;
|
|
82
|
+
chat_input_per_mtok: number;
|
|
83
|
+
chat_output_per_mtok: number;
|
|
84
|
+
}
|
|
85
|
+
export declare const DEFAULT_CURSOR_PRICING: PricingConfig;
|
|
86
|
+
export type Db90CursorPayloadMetadata = {
|
|
87
|
+
session_id?: string;
|
|
88
|
+
cursor_session_id: string | null;
|
|
89
|
+
workspace: string;
|
|
90
|
+
workspace_scope?: WorkspaceScope;
|
|
91
|
+
workspace_folder?: string;
|
|
92
|
+
cost_model: CursorLineCostModel | CursorTokenCostModel | typeof TRANSCRIPT_COST_MODEL | CursorHookCostModel;
|
|
93
|
+
scannable: boolean;
|
|
94
|
+
risk_level: RiskLevel | "none";
|
|
95
|
+
source?: "recent_commit";
|
|
96
|
+
transcript_source?: "agent_transcript";
|
|
97
|
+
composer_name?: string;
|
|
98
|
+
prompt_text?: string;
|
|
99
|
+
assistant_text?: string;
|
|
100
|
+
risk_categories?: string[];
|
|
101
|
+
risk_score?: number;
|
|
102
|
+
commit_hash?: string;
|
|
103
|
+
commit_message?: string;
|
|
104
|
+
repo_name?: string;
|
|
105
|
+
branch_name?: string;
|
|
106
|
+
ai_percentage?: number;
|
|
107
|
+
ingest_source?: "cursor_hook";
|
|
108
|
+
hook_event_name?: string;
|
|
109
|
+
generation_id?: string;
|
|
110
|
+
hook_tool_name?: string;
|
|
111
|
+
duration_ms?: number;
|
|
112
|
+
};
|
|
113
|
+
export interface CursorDb90Payload extends IngestPayload {
|
|
114
|
+
tool_name: "cursor";
|
|
115
|
+
event_type: "completion" | "chat" | "commit";
|
|
116
|
+
model: string;
|
|
117
|
+
tokens_in: number;
|
|
118
|
+
tokens_out: number;
|
|
119
|
+
cost_usd: number;
|
|
120
|
+
occurred_at: string;
|
|
121
|
+
project_id?: string;
|
|
122
|
+
metadata: Db90CursorPayloadMetadata;
|
|
123
|
+
}
|
|
124
|
+
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[];
|
|
126
|
+
/**
|
|
127
|
+
* Maps Cursor’s latest-commit snapshot (`aiCodeTracking.recentCommit`) to a single commit-classified event.
|
|
128
|
+
* Cursor only keeps one recent commit row (overwritten on each new commit).
|
|
129
|
+
* Line-cost math still follows the chat-style line proxy (`computeLineCost("chat", …)`); only `event_type` differs.
|
|
130
|
+
*/
|
|
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 {};
|