@aixle/insights 0.2.0 → 0.2.2-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 +38 -9
- package/dist/auth/credentials.d.ts +7 -1
- package/dist/auth/credentials.js +71 -14
- package/dist/auth/exchange.d.ts +1 -1
- package/dist/auth/exchange.js +1 -1
- package/dist/auth/flow.d.ts +8 -1
- package/dist/auth/flow.js +27 -5
- package/dist/auth/keycloak.d.ts +1 -1
- package/dist/auth/keycloak.js +20 -1
- package/dist/cli.d.ts +5 -3
- package/dist/cli.js +69 -21
- package/dist/collect-cursor-payloads.d.ts +4 -3
- package/dist/collect-cursor-payloads.js +8 -5
- package/dist/cursor-checkpoints.d.ts +2 -2
- package/dist/cursor-payload-contract.d.ts +5 -5
- package/dist/cursor-payload-contract.js +6 -0
- package/dist/cursor-settings.d.ts +9 -4
- package/dist/cursor-settings.js +80 -10
- package/dist/health.d.ts +3 -1
- package/dist/health.js +13 -1
- package/dist/hooks/cursor-hooks-mapper.d.ts +3 -3
- package/dist/hooks/cursor-hooks-mapper.js +1 -1
- package/dist/hooks/cursor-hooks-reader.d.ts +2 -0
- package/dist/hooks/cursor-hooks-reader.js +10 -3
- 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 +57 -11
- package/dist/lib/repo-path-safety.d.ts +35 -0
- package/dist/lib/repo-path-safety.js +102 -0
- package/dist/lib/spawn-arg-safety.d.ts +25 -0
- package/dist/lib/spawn-arg-safety.js +49 -0
- package/dist/lib/transport-security.d.ts +1 -0
- package/dist/lib/transport-security.js +1 -1
- package/dist/readers/claude.d.ts +54 -6
- package/dist/readers/claude.js +154 -2
- package/dist/readers/cursor.d.ts +10 -7
- package/dist/readers/cursor.js +113 -17
- package/dist/risk-scanner.js +7 -0
- package/dist/server.d.ts +20 -3
- package/dist/server.js +101 -67
- package/dist/state.js +7 -2
- package/dist/sync.d.ts +13 -2
- package/dist/sync.js +86 -58
- package/package.json +6 -2
package/dist/readers/cursor.js
CHANGED
|
@@ -74,7 +74,10 @@ export function probeCursorGlobalStateDb(verbose = false, baseDir) {
|
|
|
74
74
|
export function findCursorDbs(baseDir) {
|
|
75
75
|
const dir = join(baseDir ?? cursorUserDir(), "workspaceStorage");
|
|
76
76
|
try {
|
|
77
|
-
|
|
77
|
+
// Pattern stays forward-slash and the directory goes through `cwd`. Building
|
|
78
|
+
// it with `join` emitted `\` on Windows, and glob treats `\` as an escape
|
|
79
|
+
// character on every platform, so this silently matched nothing there.
|
|
80
|
+
return glob.sync("**/cursor.db", { cwd: dir, absolute: true });
|
|
78
81
|
}
|
|
79
82
|
catch {
|
|
80
83
|
return [];
|
|
@@ -244,7 +247,14 @@ export function findStateVscDbs(baseDir) {
|
|
|
244
247
|
const results = [];
|
|
245
248
|
results.push(join(userDir, "globalStorage", "state.vscdb"));
|
|
246
249
|
try {
|
|
247
|
-
|
|
250
|
+
// Forward-slash pattern + `cwd`, as above. This one mattered most: the
|
|
251
|
+
// globalStorage path is pushed unconditionally, so on Windows the function
|
|
252
|
+
// still returned a result while silently dropping every per-workspace
|
|
253
|
+
// state.vscdb — partial data loss that looked like working software.
|
|
254
|
+
results.push(...glob.sync("workspaceStorage/**/state.vscdb", {
|
|
255
|
+
cwd: userDir,
|
|
256
|
+
absolute: true,
|
|
257
|
+
}));
|
|
248
258
|
}
|
|
249
259
|
catch {
|
|
250
260
|
/* ignore */
|
|
@@ -447,6 +457,62 @@ function toIsoFromMs(value) {
|
|
|
447
457
|
const date = new Date(value);
|
|
448
458
|
return Number.isNaN(date.getTime()) ? null : date.toISOString();
|
|
449
459
|
}
|
|
460
|
+
/** Coerce epoch ms/seconds, numeric strings, or ISO-8601 into an ISO timestamp. */
|
|
461
|
+
function coerceTranscriptTimestamp(value) {
|
|
462
|
+
if (value == null)
|
|
463
|
+
return null;
|
|
464
|
+
if (typeof value === "number")
|
|
465
|
+
return toIsoFromMs(value);
|
|
466
|
+
if (typeof value !== "string")
|
|
467
|
+
return null;
|
|
468
|
+
const trimmed = value.trim();
|
|
469
|
+
if (!trimmed)
|
|
470
|
+
return null;
|
|
471
|
+
const fromEpoch = toIsoString(trimmed);
|
|
472
|
+
if (fromEpoch)
|
|
473
|
+
return fromEpoch;
|
|
474
|
+
const ms = Date.parse(trimmed);
|
|
475
|
+
if (Number.isNaN(ms))
|
|
476
|
+
return null;
|
|
477
|
+
return new Date(ms).toISOString();
|
|
478
|
+
}
|
|
479
|
+
/** Prefer explicit per-line / per-message times when Cursor includes them. */
|
|
480
|
+
function extractLineOccurredAt(entry) {
|
|
481
|
+
const candidates = [
|
|
482
|
+
entry.timestamp,
|
|
483
|
+
entry.createdAt,
|
|
484
|
+
entry.unixMs,
|
|
485
|
+
entry.message?.timestamp,
|
|
486
|
+
entry.message?.createdAt,
|
|
487
|
+
];
|
|
488
|
+
for (const candidate of candidates) {
|
|
489
|
+
const iso = coerceTranscriptTimestamp(candidate);
|
|
490
|
+
if (iso)
|
|
491
|
+
return iso;
|
|
492
|
+
}
|
|
493
|
+
return null;
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* Spread turns across [start, end] when the JSONL has no per-message times.
|
|
497
|
+
* Prevents first-sync backfill from collapsing every turn onto lastUpdatedAt/mtime
|
|
498
|
+
* (DB90DV-605 weekly chart spike).
|
|
499
|
+
*/
|
|
500
|
+
function interpolateTurnOccurredAt(startIso, endIso, index, total, fallbackIso) {
|
|
501
|
+
if (total <= 0)
|
|
502
|
+
return fallbackIso;
|
|
503
|
+
// Single turn: prefer session end (lastUpdatedAt) — matches prior composer-header behavior.
|
|
504
|
+
if (total === 1)
|
|
505
|
+
return endIso ?? startIso ?? fallbackIso;
|
|
506
|
+
const startMs = startIso ? Date.parse(startIso) : NaN;
|
|
507
|
+
const endMs = endIso ? Date.parse(endIso) : NaN;
|
|
508
|
+
if (Number.isNaN(startMs) || Number.isNaN(endMs)) {
|
|
509
|
+
return startIso ?? endIso ?? fallbackIso;
|
|
510
|
+
}
|
|
511
|
+
if (endMs <= startMs)
|
|
512
|
+
return startIso ?? fallbackIso;
|
|
513
|
+
const ms = startMs + ((endMs - startMs) * index) / (total - 1);
|
|
514
|
+
return new Date(ms).toISOString();
|
|
515
|
+
}
|
|
450
516
|
function readComposerHeaders(baseDir) {
|
|
451
517
|
const userDir = baseDir ?? cursorUserDir();
|
|
452
518
|
const dbPath = join(userDir, "globalStorage", "state.vscdb");
|
|
@@ -491,6 +557,7 @@ function readComposerHeaders(baseDir) {
|
|
|
491
557
|
composerId,
|
|
492
558
|
name: typeof composer.name === "string" ? composer.name : null,
|
|
493
559
|
workspacePath: typeof uri?.fsPath === "string" ? uri.fsPath : null,
|
|
560
|
+
createdAt: toIsoFromMs(composer.createdAt),
|
|
494
561
|
lastUpdatedAt: toIsoFromMs(composer.lastUpdatedAt),
|
|
495
562
|
});
|
|
496
563
|
}
|
|
@@ -563,11 +630,15 @@ function workspaceFromTranscriptFile(filePath) {
|
|
|
563
630
|
const MAX_TRANSCRIPT_BYTES = 50 * 1024 * 1024; // 50 MB
|
|
564
631
|
export async function parseCursorTranscriptFile(filePath, composerHeaders, verbose = false) {
|
|
565
632
|
let fileSize = 0;
|
|
566
|
-
let
|
|
633
|
+
let fileMtimeIso = new Date().toISOString();
|
|
634
|
+
let fileBirthIso = null;
|
|
567
635
|
try {
|
|
568
636
|
const stat = statSync(filePath);
|
|
569
637
|
fileSize = stat.size;
|
|
570
|
-
|
|
638
|
+
fileMtimeIso = stat.mtime.toISOString();
|
|
639
|
+
if (stat.birthtime && !Number.isNaN(stat.birthtime.getTime()) && stat.birthtime.getTime() > 0) {
|
|
640
|
+
fileBirthIso = stat.birthtime.toISOString();
|
|
641
|
+
}
|
|
571
642
|
}
|
|
572
643
|
catch {
|
|
573
644
|
return [];
|
|
@@ -580,15 +651,25 @@ export async function parseCursorTranscriptFile(filePath, composerHeaders, verbo
|
|
|
580
651
|
}
|
|
581
652
|
const sessionId = basename(filePath, ".jsonl");
|
|
582
653
|
const header = composerHeaders.get(sessionId);
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
const
|
|
654
|
+
const sessionFallback = header?.lastUpdatedAt ?? fileMtimeIso;
|
|
655
|
+
const sessionStart = header?.createdAt ?? fileBirthIso ?? sessionFallback;
|
|
656
|
+
const sessionEnd = header?.lastUpdatedAt ?? fileMtimeIso;
|
|
657
|
+
const drafts = [];
|
|
586
658
|
let currentPromptParts = [];
|
|
587
659
|
let currentAssistantParts = [];
|
|
660
|
+
let currentTurnOccurredAt = null;
|
|
588
661
|
let turnIndex = 0;
|
|
589
662
|
const hasher = createHash("sha256");
|
|
590
663
|
const stream = createReadStream(filePath, { encoding: "utf-8" });
|
|
591
664
|
const rl = createInterface({ input: stream, crlfDelay: Infinity });
|
|
665
|
+
const noteLineTime = (entry) => {
|
|
666
|
+
const lineAt = extractLineOccurredAt(entry);
|
|
667
|
+
if (!lineAt)
|
|
668
|
+
return;
|
|
669
|
+
// Prefer the first timestamp in the turn (usually the user message).
|
|
670
|
+
if (!currentTurnOccurredAt)
|
|
671
|
+
currentTurnOccurredAt = lineAt;
|
|
672
|
+
};
|
|
592
673
|
const finalizeTurn = () => {
|
|
593
674
|
const promptText = currentPromptParts.join("\n\n").trim();
|
|
594
675
|
const assistantText = currentAssistantParts.join("\n\n").trim();
|
|
@@ -596,14 +677,14 @@ export async function parseCursorTranscriptFile(filePath, composerHeaders, verbo
|
|
|
596
677
|
return;
|
|
597
678
|
const risk = scanText(promptText);
|
|
598
679
|
turnIndex += 1;
|
|
599
|
-
|
|
680
|
+
drafts.push({
|
|
600
681
|
turnId: `${sessionId}:${turnIndex}`,
|
|
601
682
|
sessionId,
|
|
602
683
|
filePath,
|
|
603
684
|
fileSize,
|
|
604
685
|
workspacePath: header?.workspacePath ?? workspaceFromTranscriptFile(filePath),
|
|
605
686
|
composerName: header?.name ?? null,
|
|
606
|
-
|
|
687
|
+
turnOccurredAt: currentTurnOccurredAt,
|
|
607
688
|
promptText,
|
|
608
689
|
assistantText,
|
|
609
690
|
tokensIn: estimateTokens(promptText),
|
|
@@ -612,6 +693,7 @@ export async function parseCursorTranscriptFile(filePath, composerHeaders, verbo
|
|
|
612
693
|
riskScore: risk.risk_score,
|
|
613
694
|
riskCategories: risk.risk_categories,
|
|
614
695
|
});
|
|
696
|
+
currentTurnOccurredAt = null;
|
|
615
697
|
};
|
|
616
698
|
let lineNumber = 0;
|
|
617
699
|
try {
|
|
@@ -640,9 +722,11 @@ export async function parseCursorTranscriptFile(filePath, composerHeaders, verbo
|
|
|
640
722
|
currentPromptParts = [];
|
|
641
723
|
currentAssistantParts = [];
|
|
642
724
|
}
|
|
725
|
+
noteLineTime(entry);
|
|
643
726
|
currentPromptParts.push(...texts.map(stripUserQueryWrapper).filter((text) => text.length > 0));
|
|
644
727
|
}
|
|
645
728
|
else if (entry.role === "assistant") {
|
|
729
|
+
noteLineTime(entry);
|
|
646
730
|
currentAssistantParts.push(...texts.map((text) => text.trim()).filter((text) => text.length > 0));
|
|
647
731
|
}
|
|
648
732
|
}
|
|
@@ -659,9 +743,16 @@ export async function parseCursorTranscriptFile(filePath, composerHeaders, verbo
|
|
|
659
743
|
}
|
|
660
744
|
finalizeTurn();
|
|
661
745
|
const contentHash = hasher.digest("hex").slice(0, 32);
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
746
|
+
const total = drafts.length;
|
|
747
|
+
return drafts.map((draft, index) => {
|
|
748
|
+
const { turnOccurredAt, ...rest } = draft;
|
|
749
|
+
return {
|
|
750
|
+
...rest,
|
|
751
|
+
contentHash,
|
|
752
|
+
occurredAt: turnOccurredAt ??
|
|
753
|
+
interpolateTurnOccurredAt(sessionStart, sessionEnd, index, total, sessionFallback),
|
|
754
|
+
};
|
|
755
|
+
});
|
|
665
756
|
}
|
|
666
757
|
export async function readCursorTranscriptSessions(cursorUserBaseDir, transcriptProjectDirs, verbose = false) {
|
|
667
758
|
const composerHeaders = readComposerHeaders(cursorUserBaseDir);
|
|
@@ -730,7 +821,7 @@ function dailyStatsSessionId(date, eventType, modelKey) {
|
|
|
730
821
|
return `cursor:daily_stats:${date}:${suffix}`;
|
|
731
822
|
}
|
|
732
823
|
function buildPayload(opts) {
|
|
733
|
-
const { eventType, tokensIn, tokensOut, costUsd, occurredAt, dbPath, date, model = "unknown", modelKey, projectId, costModel = LINE_COST_MODEL, } = opts;
|
|
824
|
+
const { eventType, tokensIn, tokensOut, costUsd, occurredAt, dbPath, date, model = "unknown", modelResolution, modelKey, projectId, costModel = LINE_COST_MODEL, } = opts;
|
|
734
825
|
const payload = {
|
|
735
826
|
tool_name: "cursor",
|
|
736
827
|
event_type: eventType,
|
|
@@ -746,13 +837,14 @@ function buildPayload(opts) {
|
|
|
746
837
|
cost_model: costModel,
|
|
747
838
|
scannable: false,
|
|
748
839
|
risk_level: "none",
|
|
840
|
+
...(modelResolution !== undefined ? { model_resolution: modelResolution } : {}),
|
|
749
841
|
},
|
|
750
842
|
};
|
|
751
843
|
if (projectId)
|
|
752
844
|
payload.project_id = projectId;
|
|
753
845
|
return payload;
|
|
754
846
|
}
|
|
755
|
-
export function mapDailyStats(entry, projectId, pricing = DEFAULT_CURSOR_PRICING, model) {
|
|
847
|
+
export function mapDailyStats(entry, projectId, pricing = DEFAULT_CURSOR_PRICING, model, modelResolution) {
|
|
756
848
|
const { date, value, dbPath } = entry;
|
|
757
849
|
const occurredAt = `${date}T00:00:00.000Z`;
|
|
758
850
|
const results = [];
|
|
@@ -773,6 +865,7 @@ export function mapDailyStats(entry, projectId, pricing = DEFAULT_CURSOR_PRICING
|
|
|
773
865
|
dbPath,
|
|
774
866
|
date,
|
|
775
867
|
model,
|
|
868
|
+
modelResolution,
|
|
776
869
|
projectId,
|
|
777
870
|
}));
|
|
778
871
|
}
|
|
@@ -781,11 +874,12 @@ export function mapDailyStats(entry, projectId, pricing = DEFAULT_CURSOR_PRICING
|
|
|
781
874
|
eventType: "chat",
|
|
782
875
|
tokensIn: composerSuggested,
|
|
783
876
|
tokensOut: composerAccepted,
|
|
784
|
-
costUsd: computeLineCost("chat",
|
|
877
|
+
costUsd: computeLineCost("chat", composerSuggested, pricing),
|
|
785
878
|
occurredAt,
|
|
786
879
|
dbPath,
|
|
787
880
|
date,
|
|
788
881
|
model,
|
|
882
|
+
modelResolution,
|
|
789
883
|
projectId,
|
|
790
884
|
}));
|
|
791
885
|
}
|
|
@@ -820,7 +914,7 @@ export function mapDailyStats(entry, projectId, pricing = DEFAULT_CURSOR_PRICING
|
|
|
820
914
|
* Cursor only keeps one recent commit row (overwritten on each new commit).
|
|
821
915
|
* Line-cost math still follows the chat-style line proxy (`computeLineCost("chat", …)`); only `event_type` differs.
|
|
822
916
|
*/
|
|
823
|
-
export function mapRecentCommit(entry, projectId, pricing = DEFAULT_CURSOR_PRICING, model) {
|
|
917
|
+
export function mapRecentCommit(entry, projectId, pricing = DEFAULT_CURSOR_PRICING, model, modelResolution) {
|
|
824
918
|
const { value: obj, dbPath } = entry;
|
|
825
919
|
const occurredAt = toIsoString(obj.timestamp);
|
|
826
920
|
if (!occurredAt)
|
|
@@ -866,6 +960,7 @@ export function mapRecentCommit(entry, projectId, pricing = DEFAULT_CURSOR_PRICI
|
|
|
866
960
|
: undefined,
|
|
867
961
|
scannable: false,
|
|
868
962
|
risk_level: "none",
|
|
963
|
+
...(modelResolution !== undefined ? { model_resolution: modelResolution } : {}),
|
|
869
964
|
},
|
|
870
965
|
};
|
|
871
966
|
if (projectId)
|
|
@@ -902,7 +997,7 @@ export function mapEvent(row, workspacePath, projectId, pricing = DEFAULT_CURSOR
|
|
|
902
997
|
payload.project_id = projectId;
|
|
903
998
|
return payload;
|
|
904
999
|
}
|
|
905
|
-
export function mapTranscriptTurn(turn, projectId, pricing = DEFAULT_CURSOR_PRICING, model) {
|
|
1000
|
+
export function mapTranscriptTurn(turn, projectId, pricing = DEFAULT_CURSOR_PRICING, model, modelResolution) {
|
|
906
1001
|
const payload = {
|
|
907
1002
|
tool_name: "cursor",
|
|
908
1003
|
event_type: "chat",
|
|
@@ -924,6 +1019,7 @@ export function mapTranscriptTurn(turn, projectId, pricing = DEFAULT_CURSOR_PRIC
|
|
|
924
1019
|
composer_name: turn.composerName ?? undefined,
|
|
925
1020
|
prompt_text: turn.promptText || undefined,
|
|
926
1021
|
assistant_text: turn.assistantText || undefined,
|
|
1022
|
+
...(modelResolution !== undefined ? { model_resolution: modelResolution } : {}),
|
|
927
1023
|
},
|
|
928
1024
|
};
|
|
929
1025
|
if (projectId)
|
package/dist/risk-scanner.js
CHANGED
|
@@ -12,6 +12,10 @@ const CATEGORIES = {
|
|
|
12
12
|
weight: 3,
|
|
13
13
|
patterns: [
|
|
14
14
|
/\b\d{3}-\d{2}-\d{4}\b/g, // SSN
|
|
15
|
+
/* eslint-disable-next-line security/detect-unsafe-regex -- Flagged for
|
|
16
|
+
`{3}` inside `(?:…)?`. Every branch is a fixed-length digit run
|
|
17
|
+
anchored by \b, so the match is bounded and cannot backtrack
|
|
18
|
+
super-linearly. */
|
|
15
19
|
/\b(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|3[47][0-9]{13}|3(?:0[0-5]|[68][0-9])[0-9]{11}|6(?:011|5[0-9]{2})[0-9]{12}|(?:2131|1800|35\d{3})\d{11})\b/g, // Credit card
|
|
16
20
|
],
|
|
17
21
|
},
|
|
@@ -19,6 +23,9 @@ const CATEGORIES = {
|
|
|
19
23
|
weight: 1,
|
|
20
24
|
patterns: [
|
|
21
25
|
/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/gi, // Email
|
|
26
|
+
/* eslint-disable-next-line security/detect-unsafe-regex -- Flagged for
|
|
27
|
+
`?` nested in `?`. All quantified groups are fixed-length digit or
|
|
28
|
+
separator classes anchored by \b; matching is bounded. */
|
|
22
29
|
/\b(?:\+?1[-.\s]?)?(?:\([0-9]{3}\)|[0-9]{3})[-.\s]?[0-9]{3}[-.\s]?[0-9]{4}\b/g, // Phone
|
|
23
30
|
],
|
|
24
31
|
},
|
package/dist/server.d.ts
CHANGED
|
@@ -7,8 +7,25 @@ export declare const SYNC_NOW_INPUT_SCHEMA: z.ZodObject<{
|
|
|
7
7
|
cursor: "cursor";
|
|
8
8
|
}>>>;
|
|
9
9
|
}, z.core.$strict>;
|
|
10
|
-
/**
|
|
11
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Whether a `credential_validation_failed` warning should mirror to stderr.
|
|
12
|
+
* Mirrors for one-shot/startup sources so an installed-but-uninitialized MCP is
|
|
13
|
+
* visible, but NOT for the recurring background `"interval"` tick — that fires
|
|
14
|
+
* every SYNC_INTERVAL_MS for the whole process lifetime and would spam the logs.
|
|
15
|
+
* The event is always written to mcp.log regardless of this return value.
|
|
16
|
+
*/
|
|
17
|
+
export declare function shouldMirrorMissingCredentials(source: string): boolean;
|
|
18
|
+
/** Structured status for `aixle_insights_status` — tolerates missing/malformed credentials and state. */
|
|
19
|
+
export declare function buildAixleInsightsStatusPayload(): Promise<Record<string, unknown>>;
|
|
12
20
|
/** In-process MCP server instance (stdio not attached). */
|
|
13
|
-
export declare function
|
|
21
|
+
export declare function createAixleInsightsMcpServer(): McpServer;
|
|
22
|
+
/**
|
|
23
|
+
* Subscribes `shutdown` to the given stream's "end" and "close" events.
|
|
24
|
+
* Closing stdin is how the OS signals a stdio-transport MCP server that its
|
|
25
|
+
* parent process is gone — this fires even when the parent can't deliver a
|
|
26
|
+
* signal (e.g. it was itself SIGKILL'd, or the OS reparented this process
|
|
27
|
+
* without ever sending one). Exported standalone so it's testable with a
|
|
28
|
+
* plain EventEmitter instead of the real process.stdin.
|
|
29
|
+
*/
|
|
30
|
+
export declare function wireParentExitShutdown(stdin: Pick<NodeJS.ReadStream, "on">, shutdown: () => void): void;
|
|
14
31
|
export declare function startServer(): Promise<void>;
|
package/dist/server.js
CHANGED
|
@@ -27,6 +27,12 @@ export const SYNC_NOW_INPUT_SCHEMA = z
|
|
|
27
27
|
}
|
|
28
28
|
})
|
|
29
29
|
.strict();
|
|
30
|
+
const AUTHENTICATE_INPUT_SCHEMA = z.object({
|
|
31
|
+
keycloakUrl: z.string().optional(),
|
|
32
|
+
clientId: z.string().optional(),
|
|
33
|
+
});
|
|
34
|
+
/** DB90DV-569: `db90_*` names are deprecated aliases kept for one release for existing callers. */
|
|
35
|
+
const DEPRECATED_ALIAS_NOTE = "(Deprecated — use `{name}` instead; kept temporarily for backward compatibility, see DB90DV-569.) ";
|
|
30
36
|
function jsonContent(value) {
|
|
31
37
|
return {
|
|
32
38
|
content: [
|
|
@@ -56,6 +62,16 @@ function migrateAllLegacyState(creds) {
|
|
|
56
62
|
function syncResultOk(result) {
|
|
57
63
|
return !result.locked && result.failed === 0;
|
|
58
64
|
}
|
|
65
|
+
/**
|
|
66
|
+
* Whether a `credential_validation_failed` warning should mirror to stderr.
|
|
67
|
+
* Mirrors for one-shot/startup sources so an installed-but-uninitialized MCP is
|
|
68
|
+
* visible, but NOT for the recurring background `"interval"` tick — that fires
|
|
69
|
+
* every SYNC_INTERVAL_MS for the whole process lifetime and would spam the logs.
|
|
70
|
+
* The event is always written to mcp.log regardless of this return value.
|
|
71
|
+
*/
|
|
72
|
+
export function shouldMirrorMissingCredentials(source) {
|
|
73
|
+
return source !== "interval";
|
|
74
|
+
}
|
|
59
75
|
// Process-lifetime cache keyed on the inputs that drive resolveProjectId: host,
|
|
60
76
|
// lookup token, and the current repo's git remote. Re-resolve when any of them
|
|
61
77
|
// changes (re-auth, repo cwd change). `source: "none"` is never cached so a
|
|
@@ -70,22 +86,22 @@ async function getProjectResolutionForSync(creds) {
|
|
|
70
86
|
if (cachedProjectResolution?.key === cacheKey) {
|
|
71
87
|
return cachedProjectResolution.value;
|
|
72
88
|
}
|
|
73
|
-
const result = await resolveProjectId(undefined, undefined, creds.host, token, false);
|
|
89
|
+
const result = await resolveProjectId(undefined, undefined, creds.host, token, false, creds.insecureHttpAllowed === true);
|
|
74
90
|
mcpLog.info("project_attribution_resolved", { project_id: result.projectId, source: result.source }, false);
|
|
75
91
|
if (result.source !== "none") {
|
|
76
92
|
cachedProjectResolution = { key: cacheKey, value: result };
|
|
77
93
|
}
|
|
78
94
|
return result;
|
|
79
95
|
}
|
|
80
|
-
/** Structured status for `
|
|
81
|
-
export async function
|
|
96
|
+
/** Structured status for `aixle_insights_status` — tolerates missing/malformed credentials and state. */
|
|
97
|
+
export async function buildAixleInsightsStatusPayload() {
|
|
82
98
|
const snapshot = await buildHealthSnapshot();
|
|
83
99
|
return healthSnapshotToStatusPayload(snapshot);
|
|
84
100
|
}
|
|
85
101
|
async function executeSync(parsed) {
|
|
86
102
|
const creds = await loadCredentials();
|
|
87
103
|
if (!creds || !credentialsHaveAnyToken(creds)) {
|
|
88
|
-
mcpLog.warn("credential_validation_failed", { source: "
|
|
104
|
+
mcpLog.warn("credential_validation_failed", { source: "aixle_insights_sync_now", reason: "missing_credentials" }, shouldMirrorMissingCredentials("aixle_insights_sync_now"));
|
|
89
105
|
return { ok: false, error: "missing_credentials" };
|
|
90
106
|
}
|
|
91
107
|
migrateAllLegacyState(creds);
|
|
@@ -104,79 +120,96 @@ async function executeSync(parsed) {
|
|
|
104
120
|
});
|
|
105
121
|
return { ok: syncResultOk(result), result };
|
|
106
122
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
}, async (input) => {
|
|
118
|
-
try {
|
|
119
|
-
const parsed = SYNC_NOW_INPUT_SCHEMA.parse(input ?? {});
|
|
120
|
-
return jsonContent(await executeSync(parsed));
|
|
121
|
-
}
|
|
122
|
-
catch (err) {
|
|
123
|
-
if (err instanceof z.ZodError) {
|
|
124
|
-
return jsonContent({
|
|
125
|
-
ok: false,
|
|
126
|
-
error: "validation_error",
|
|
127
|
-
details: err.flatten(),
|
|
128
|
-
});
|
|
129
|
-
}
|
|
123
|
+
async function statusHandler() {
|
|
124
|
+
return jsonContent(await buildAixleInsightsStatusPayload());
|
|
125
|
+
}
|
|
126
|
+
async function syncNowHandler(input) {
|
|
127
|
+
try {
|
|
128
|
+
const parsed = SYNC_NOW_INPUT_SCHEMA.parse(input ?? {});
|
|
129
|
+
return jsonContent(await executeSync(parsed));
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
if (err instanceof z.ZodError) {
|
|
130
133
|
return jsonContent({
|
|
131
134
|
ok: false,
|
|
132
|
-
error:
|
|
133
|
-
|
|
134
|
-
}
|
|
135
|
-
});
|
|
136
|
-
server.registerTool("db90_authenticate", {
|
|
137
|
-
description: "Starts Keycloak device login and returns the visit URL/code for the user. Use aixle-insights init for the full terminal flow that saves credentials.",
|
|
138
|
-
inputSchema: z.object({
|
|
139
|
-
keycloakUrl: z.string().optional(),
|
|
140
|
-
clientId: z.string().optional(),
|
|
141
|
-
}),
|
|
142
|
-
}, async (input) => {
|
|
143
|
-
try {
|
|
144
|
-
const args = input;
|
|
145
|
-
const kc = (args.keycloakUrl?.trim() || defaultKeycloakIssuer()).trim();
|
|
146
|
-
if (!kc) {
|
|
147
|
-
return jsonContent({
|
|
148
|
-
ok: false,
|
|
149
|
-
error: "keycloakUrl or KEYCLOAK_ISSUER / DB90_KEYCLOAK_ISSUER is required",
|
|
150
|
-
});
|
|
151
|
-
}
|
|
152
|
-
const clientId = args.clientId?.trim() || defaultKeycloakClientId();
|
|
153
|
-
const device = await startDeviceAuthorization({
|
|
154
|
-
issuer: kc,
|
|
155
|
-
clientId,
|
|
156
|
-
});
|
|
157
|
-
return jsonContent({
|
|
158
|
-
ok: true,
|
|
159
|
-
verificationUri: device.verification_uri,
|
|
160
|
-
verificationUriComplete: device.verification_uri_complete ?? null,
|
|
161
|
-
userCode: device.user_code,
|
|
162
|
-
expiresIn: device.expires_in,
|
|
163
|
-
interval: device.interval ?? 5,
|
|
164
|
-
issuer: kc,
|
|
165
|
-
clientId,
|
|
166
|
-
message: `Visit ${device.verification_uri} and enter code ${device.user_code}`,
|
|
135
|
+
error: "validation_error",
|
|
136
|
+
details: err.flatten(),
|
|
167
137
|
});
|
|
168
138
|
}
|
|
169
|
-
|
|
139
|
+
return jsonContent({
|
|
140
|
+
ok: false,
|
|
141
|
+
error: err instanceof Error ? err.message : String(err),
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
async function authenticateHandler(args) {
|
|
146
|
+
try {
|
|
147
|
+
const kc = (args.keycloakUrl?.trim() || defaultKeycloakIssuer()).trim();
|
|
148
|
+
if (!kc) {
|
|
170
149
|
return jsonContent({
|
|
171
150
|
ok: false,
|
|
172
|
-
error:
|
|
151
|
+
error: "keycloakUrl or KEYCLOAK_ISSUER / DB90_KEYCLOAK_ISSUER is required",
|
|
173
152
|
});
|
|
174
153
|
}
|
|
175
|
-
|
|
154
|
+
const clientId = args.clientId?.trim() || defaultKeycloakClientId();
|
|
155
|
+
const device = await startDeviceAuthorization({
|
|
156
|
+
issuer: kc,
|
|
157
|
+
clientId,
|
|
158
|
+
});
|
|
159
|
+
return jsonContent({
|
|
160
|
+
ok: true,
|
|
161
|
+
verificationUri: device.verification_uri,
|
|
162
|
+
verificationUriComplete: device.verification_uri_complete ?? null,
|
|
163
|
+
userCode: device.user_code,
|
|
164
|
+
expiresIn: device.expires_in,
|
|
165
|
+
interval: device.interval ?? 5,
|
|
166
|
+
issuer: kc,
|
|
167
|
+
clientId,
|
|
168
|
+
message: `Visit ${device.verification_uri} and enter code ${device.user_code}`,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
catch (err) {
|
|
172
|
+
return jsonContent({
|
|
173
|
+
ok: false,
|
|
174
|
+
error: err instanceof Error ? err.message : String(err),
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/** In-process MCP server instance (stdio not attached). */
|
|
179
|
+
export function createAixleInsightsMcpServer() {
|
|
180
|
+
const server = new McpServer({ name: SERVER_NAME, version: SERVER_VERSION }, { capabilities: { tools: {} } });
|
|
181
|
+
const statusDescription = "Returns Aixle Insights MCP connectivity and last sync metadata from disk (credentials + state). No arguments.";
|
|
182
|
+
server.registerTool("aixle_insights_status", { description: statusDescription }, statusHandler);
|
|
183
|
+
server.registerTool("db90_status", { description: DEPRECATED_ALIAS_NOTE.replace("{name}", "aixle_insights_status") + statusDescription }, statusHandler);
|
|
184
|
+
const syncNowDescription = "Runs one DB90 ingest sync cycle for enabled tools immediately (matches background cadence). " +
|
|
185
|
+
"Optional `tools` subset filter: omit to sync every tool credential you have authenticated (Claude transcripts + Cursor telemetry).";
|
|
186
|
+
server.registerTool("aixle_insights_sync_now", { description: syncNowDescription, inputSchema: SYNC_NOW_INPUT_SCHEMA }, syncNowHandler);
|
|
187
|
+
server.registerTool("db90_sync_now", {
|
|
188
|
+
description: DEPRECATED_ALIAS_NOTE.replace("{name}", "aixle_insights_sync_now") + syncNowDescription,
|
|
189
|
+
inputSchema: SYNC_NOW_INPUT_SCHEMA,
|
|
190
|
+
}, syncNowHandler);
|
|
191
|
+
const authenticateDescription = "Starts Keycloak device login and returns the visit URL/code for the user. Use aixle-insights init for the full terminal flow that saves credentials.";
|
|
192
|
+
server.registerTool("aixle_insights_authenticate", { description: authenticateDescription, inputSchema: AUTHENTICATE_INPUT_SCHEMA }, authenticateHandler);
|
|
193
|
+
server.registerTool("db90_authenticate", {
|
|
194
|
+
description: DEPRECATED_ALIAS_NOTE.replace("{name}", "aixle_insights_authenticate") + authenticateDescription,
|
|
195
|
+
inputSchema: AUTHENTICATE_INPUT_SCHEMA,
|
|
196
|
+
}, authenticateHandler);
|
|
176
197
|
return server;
|
|
177
198
|
}
|
|
199
|
+
/**
|
|
200
|
+
* Subscribes `shutdown` to the given stream's "end" and "close" events.
|
|
201
|
+
* Closing stdin is how the OS signals a stdio-transport MCP server that its
|
|
202
|
+
* parent process is gone — this fires even when the parent can't deliver a
|
|
203
|
+
* signal (e.g. it was itself SIGKILL'd, or the OS reparented this process
|
|
204
|
+
* without ever sending one). Exported standalone so it's testable with a
|
|
205
|
+
* plain EventEmitter instead of the real process.stdin.
|
|
206
|
+
*/
|
|
207
|
+
export function wireParentExitShutdown(stdin, shutdown) {
|
|
208
|
+
stdin.on("end", shutdown);
|
|
209
|
+
stdin.on("close", shutdown);
|
|
210
|
+
}
|
|
178
211
|
export async function startServer() {
|
|
179
|
-
const server =
|
|
212
|
+
const server = createAixleInsightsMcpServer();
|
|
180
213
|
const transport = new StdioServerTransport();
|
|
181
214
|
await server.connect(transport);
|
|
182
215
|
let intervalId;
|
|
@@ -195,12 +228,13 @@ export async function startServer() {
|
|
|
195
228
|
};
|
|
196
229
|
process.on("SIGINT", onSignal);
|
|
197
230
|
process.on("SIGTERM", onSignal);
|
|
231
|
+
wireParentExitShutdown(process.stdin, onSignal);
|
|
198
232
|
const runBackground = async (source) => {
|
|
199
233
|
if (shuttingDown)
|
|
200
234
|
return;
|
|
201
235
|
const creds = await loadCredentials();
|
|
202
236
|
if (!creds || !credentialsHaveAnyToken(creds)) {
|
|
203
|
-
mcpLog.warn("credential_validation_failed", { source, reason: "missing_credentials" },
|
|
237
|
+
mcpLog.warn("credential_validation_failed", { source, reason: "missing_credentials" }, shouldMirrorMissingCredentials(source));
|
|
204
238
|
return;
|
|
205
239
|
}
|
|
206
240
|
try {
|
package/dist/state.js
CHANGED
|
@@ -2,6 +2,7 @@ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { createHash, randomBytes } from "node:crypto";
|
|
5
|
+
import { mcpLog } from "./log.js";
|
|
5
6
|
export function getAppDir() {
|
|
6
7
|
const override = process.env["AIXLE_INSIGHTS_HOME"]?.trim();
|
|
7
8
|
if (override && override.length > 0)
|
|
@@ -123,8 +124,12 @@ export function readState(dir, host, token) {
|
|
|
123
124
|
}
|
|
124
125
|
}
|
|
125
126
|
}
|
|
126
|
-
catch {
|
|
127
|
-
|
|
127
|
+
catch (err) {
|
|
128
|
+
const code = err?.code;
|
|
129
|
+
if (code !== "ENOENT") {
|
|
130
|
+
// State file exists but failed to parse — distinguishes tampering from "never created".
|
|
131
|
+
mcpLog.warn("state_parse_failed", { path: filePath, error: err instanceof Error ? err.message : String(err) }, false);
|
|
132
|
+
}
|
|
128
133
|
}
|
|
129
134
|
return { version: 1, sessions: {} };
|
|
130
135
|
}
|
package/dist/sync.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { TelemetryToolId, StoredCredentials } from "./auth/credentials.js";
|
|
|
2
2
|
import { type PricingTable } from "./pricing.js";
|
|
3
3
|
import type { PricingConfig } from "./readers/cursor.js";
|
|
4
4
|
import { type ProjectResolution } from "./lib/index.js";
|
|
5
|
-
import type {
|
|
5
|
+
import type { CursorPayload } from "./readers/cursor.js";
|
|
6
6
|
/** Prefix for Claude Code session keys in shared MCP state. */
|
|
7
7
|
export declare const CLAUDE_STATE_PREFIX: "claude_code:";
|
|
8
8
|
export { CURSOR_WATERMARK_KEY, CURSOR_EVENTS_WATERMARK_KEY, CURSOR_DAILY_STATS_WATERMARK_KEY, CURSOR_RECENT_COMMIT_WATERMARK_KEY, CURSOR_TRANSCRIPT_TURN_PREFIX, cursorTranscriptTurnStateKey, filterRecentCommitsByHashDedup, } from "./cursor-checkpoints.js";
|
|
@@ -24,6 +24,8 @@ export interface SyncOptions {
|
|
|
24
24
|
verbose: boolean;
|
|
25
25
|
projectId: string | null;
|
|
26
26
|
projectIdSource?: ProjectResolution["source"];
|
|
27
|
+
/** Mirrors StoredCredentials.insecureHttpAllowed — set when `init --insecure` was used for this host. */
|
|
28
|
+
allowInsecureHttp?: boolean;
|
|
27
29
|
pricing: PricingTable;
|
|
28
30
|
appDir?: string;
|
|
29
31
|
transcriptBaseDirs?: string[];
|
|
@@ -65,7 +67,16 @@ export declare function getSyncTelemetry(): {
|
|
|
65
67
|
lastResult: SyncResult | null;
|
|
66
68
|
recentErrors: string[];
|
|
67
69
|
};
|
|
68
|
-
|
|
70
|
+
/**
|
|
71
|
+
* The repo path for a Cursor payload, normalized and safe to hand to project
|
|
72
|
+
* resolution. Returns an absolute, `..`-collapsed path or undefined.
|
|
73
|
+
*
|
|
74
|
+
* `workspace_folder` comes from the workspace's own `workspace.json` and
|
|
75
|
+
* `workspace` from a composer `uri.fsPath` or the `state.vscdb` path — all
|
|
76
|
+
* untrusted. When the preferred field is unusable we fall through to the other
|
|
77
|
+
* rather than giving up. See DB90DV-547.
|
|
78
|
+
*/
|
|
79
|
+
export declare function cursorRepoPathFromPayload(payload: CursorPayload): string | undefined;
|
|
69
80
|
/**
|
|
70
81
|
* Parallel multi-tool cycle under the global advisory ingest lock (`state.lock`).
|
|
71
82
|
*/
|