@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/health.d.ts
CHANGED
|
@@ -16,6 +16,8 @@ export interface HealthSnapshot {
|
|
|
16
16
|
hooks_queue_depth: number;
|
|
17
17
|
/** Best-effort merge of credential-scoped `mcp_operator` (latest `last_sync_at`). */
|
|
18
18
|
persisted: McpOperatorState | null;
|
|
19
|
+
/** True only when buildHealthSnapshot caught an unexpected error building the snapshot. */
|
|
20
|
+
snapshot_failed?: boolean;
|
|
19
21
|
/** In-process telemetry (same fields as `getSyncTelemetry`). */
|
|
20
22
|
process: {
|
|
21
23
|
last_sync_at: string | null;
|
|
@@ -26,6 +28,6 @@ export interface HealthSnapshot {
|
|
|
26
28
|
/** Pick the freshest persisted operator block by `last_sync_at` lexicographic (ISO-safe). */
|
|
27
29
|
export declare function mergePersistedOperators(snapshots: McpOperatorState[]): McpOperatorState | null;
|
|
28
30
|
export declare function buildHealthSnapshot(): Promise<HealthSnapshot>;
|
|
29
|
-
/** MCP `
|
|
31
|
+
/** MCP `aixle_insights_status` JSON — single source shared with CLI health. */
|
|
30
32
|
export declare function healthSnapshotToStatusPayload(snapshot: HealthSnapshot): Record<string, unknown>;
|
|
31
33
|
export declare function formatHealthForCli(snapshot: HealthSnapshot): string;
|
package/dist/health.js
CHANGED
|
@@ -122,6 +122,7 @@ export async function buildHealthSnapshot() {
|
|
|
122
122
|
hooks_installed: hooksReport.hooks_json_installed,
|
|
123
123
|
hooks_queue_depth: hooksReport.queue_depth,
|
|
124
124
|
persisted: null,
|
|
125
|
+
snapshot_failed: true,
|
|
125
126
|
process: {
|
|
126
127
|
last_sync_at: telemetry.lastSyncAt,
|
|
127
128
|
last_result: telemetry.lastResult,
|
|
@@ -133,18 +134,25 @@ export async function buildHealthSnapshot() {
|
|
|
133
134
|
};
|
|
134
135
|
}
|
|
135
136
|
}
|
|
136
|
-
/** MCP `
|
|
137
|
+
/** MCP `aixle_insights_status` JSON — single source shared with CLI health. */
|
|
137
138
|
export function healthSnapshotToStatusPayload(snapshot) {
|
|
138
139
|
const proc = snapshot.process;
|
|
139
140
|
const pers = snapshot.persisted;
|
|
140
141
|
const lastSyncAt = proc.last_sync_at ?? pers?.last_sync_at ?? null;
|
|
141
142
|
const lastResult = proc.last_result ?? snapshotToResult(pers?.last_result ?? null);
|
|
142
143
|
const errors = mergeErrors(proc.recent_errors, pers?.recent_errors ?? []);
|
|
144
|
+
const needsInit = !snapshot.authenticated && snapshot.snapshot_failed !== true;
|
|
145
|
+
const onboardingMessage = needsInit
|
|
146
|
+
? "aixle-insights is installed but not connected. Run `aixle-insights init` to start tracking — " +
|
|
147
|
+
"activity since install is not lost; the first sync after init backfills all of it."
|
|
148
|
+
: null;
|
|
143
149
|
return {
|
|
144
150
|
authenticated: snapshot.authenticated,
|
|
145
151
|
configured: snapshot.configured,
|
|
146
152
|
host: snapshot.host,
|
|
147
153
|
ingest_tools: snapshot.ingest_tools,
|
|
154
|
+
needs_init: needsInit,
|
|
155
|
+
onboarding_message: onboardingMessage,
|
|
148
156
|
last_sync_at: lastSyncAt,
|
|
149
157
|
last_result: lastResult,
|
|
150
158
|
sessions_synced: lastResult?.sent ?? 0,
|
|
@@ -191,5 +199,9 @@ export function formatHealthForCli(snapshot) {
|
|
|
191
199
|
else {
|
|
192
200
|
lines.push(" (none)");
|
|
193
201
|
}
|
|
202
|
+
if (typeof sta["onboarding_message"] === "string") {
|
|
203
|
+
lines.push("");
|
|
204
|
+
lines.push(sta["onboarding_message"]);
|
|
205
|
+
}
|
|
194
206
|
return lines.join("\n");
|
|
195
207
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { HookLogEvent } from "./hooks-config.js";
|
|
2
|
-
import type {
|
|
2
|
+
import type { CursorPayload } from "../readers/cursor.js";
|
|
3
3
|
export { HookLogEvent };
|
|
4
4
|
/** State key prefix for hook event dedup in state.sessions. */
|
|
5
5
|
export declare const CURSOR_HOOK_STATE_PREFIX: "cursor:hook:";
|
|
@@ -16,7 +16,7 @@ export declare function shouldIngestHookEvent(event: HookLogEvent, verbose?: boo
|
|
|
16
16
|
/** Warn-only version gate — Cursor builds can have unusual version strings. */
|
|
17
17
|
export declare function warnOnCursorVersion(event: HookLogEvent, verbose: boolean): void;
|
|
18
18
|
/**
|
|
19
|
-
* Map a validated hook event to the
|
|
19
|
+
* Map a validated hook event to the CursorPayload contract.
|
|
20
20
|
* Call shouldIngestHookEvent() before this — it does not re-validate.
|
|
21
21
|
*/
|
|
22
|
-
export declare function mapHookEventToPayload(event: HookLogEvent, projectId?: string | null):
|
|
22
|
+
export declare function mapHookEventToPayload(event: HookLogEvent, projectId?: string | null): CursorPayload;
|
|
@@ -47,7 +47,7 @@ export function warnOnCursorVersion(event, verbose) {
|
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
49
|
/**
|
|
50
|
-
* Map a validated hook event to the
|
|
50
|
+
* Map a validated hook event to the CursorPayload contract.
|
|
51
51
|
* Call shouldIngestHookEvent() before this — it does not re-validate.
|
|
52
52
|
*/
|
|
53
53
|
export function mapHookEventToPayload(event, projectId) {
|
|
@@ -8,6 +8,8 @@ export interface ProcessHooksQueueParams {
|
|
|
8
8
|
state: State;
|
|
9
9
|
host: string;
|
|
10
10
|
token: string;
|
|
11
|
+
/** Mirrors StoredCredentials.insecureHttpAllowed — set when `init --insecure` was used for this host. */
|
|
12
|
+
allowInsecureHttp?: boolean;
|
|
11
13
|
/** Called when a 429 is received. */
|
|
12
14
|
on429: (retryAfter: number, quotaExceeded: boolean) => void;
|
|
13
15
|
/** If true, skip events already in state.sessions. */
|
|
@@ -47,7 +47,7 @@ function rewriteQueueKeepingLines(queuePath, allLines, keepIndices) {
|
|
|
47
47
|
* Partial failure: lines that succeeded are removed; lines that failed stay.
|
|
48
48
|
*/
|
|
49
49
|
export async function processHooksQueue(params) {
|
|
50
|
-
const { queuePath, scopeDir, host, token, on429, skipSeen = true, resolveProjectId, verbose = false, } = params;
|
|
50
|
+
const { queuePath, scopeDir, host, token, allowInsecureHttp = false, on429, skipSeen = true, resolveProjectId, verbose = false, } = params;
|
|
51
51
|
if (!existsSync(queuePath)) {
|
|
52
52
|
return { sent: 0, failed: 0, skipped: 0, state: params.state };
|
|
53
53
|
}
|
|
@@ -97,7 +97,7 @@ export async function processHooksQueue(params) {
|
|
|
97
97
|
projectId = await resolveProjectId(workspace);
|
|
98
98
|
}
|
|
99
99
|
const payload = mapHookEventToPayload(event, projectId);
|
|
100
|
-
const ok = await postEvent(payload, host, token, { on429 });
|
|
100
|
+
const ok = await postEvent(payload, host, token, { on429, allowInsecureHttp });
|
|
101
101
|
if (ok) {
|
|
102
102
|
totalSent++;
|
|
103
103
|
stateMut = markSessionSent(stateMut, dedupeKey, 0);
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type InstallResult } from "./claude.js";
|
|
2
|
+
export interface InstallCursorUserMcpOptions {
|
|
3
|
+
/** Tests: full path to the Cursor MCP config file (default ~/.cursor/mcp.json). */
|
|
4
|
+
cursorConfigPath?: string;
|
|
5
|
+
force?: boolean;
|
|
6
|
+
}
|
|
7
|
+
export type UninstallResult = {
|
|
8
|
+
kind: "restored";
|
|
9
|
+
backupPath: string;
|
|
10
|
+
} | {
|
|
11
|
+
kind: "removed";
|
|
12
|
+
} | {
|
|
13
|
+
kind: "noop";
|
|
14
|
+
} | {
|
|
15
|
+
kind: "error";
|
|
16
|
+
message: string;
|
|
17
|
+
};
|
|
18
|
+
export declare function defaultCursorUserConfigPath(): string;
|
|
19
|
+
/**
|
|
20
|
+
* Merges top-level `mcpServers.aixle-insights` into ~/.cursor/mcp.json (or overridden path).
|
|
21
|
+
* Preserves all other keys and MCP server entries. Backs up the existing file
|
|
22
|
+
* (once) before the first write, and removes legacy "db90"/"insights" keys so
|
|
23
|
+
* Cursor does not spawn duplicate aixle-insights servers.
|
|
24
|
+
*/
|
|
25
|
+
export declare function installCursorUserMcp(options?: InstallCursorUserMcpOptions): InstallResult;
|
|
26
|
+
/**
|
|
27
|
+
* Removes the aixle-insights entry from ~/.cursor/mcp.json without disturbing any
|
|
28
|
+
* other server entry — including ones the user or Cursor added after install.
|
|
29
|
+
* Works on the CURRENT file (never a full-file revert to the backup). If a
|
|
30
|
+
* pre-install backup exists, the user's ORIGINAL aixle-insights entry (if any)
|
|
31
|
+
* is restored from it; otherwise our key is simply removed. The backup file is
|
|
32
|
+
* cleaned up either way.
|
|
33
|
+
*/
|
|
34
|
+
export declare function uninstallCursorUserMcp(options?: InstallCursorUserMcpOptions): UninstallResult;
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync, copyFileSync, unlinkSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { randomBytes } from "node:crypto";
|
|
5
|
+
import { desiredAixleInsightsEntry, aixleInsightsEntryMatchesDesired } from "./claude.js";
|
|
6
|
+
export function defaultCursorUserConfigPath() {
|
|
7
|
+
return join(homedir(), ".cursor", "mcp.json");
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Legacy keys to clean up on install: "db90" (pre-rebrand) and "insights" (an
|
|
11
|
+
* earlier local-dev-only convention documented in scripts/reset-local-env.mjs,
|
|
12
|
+
* never the production installer's key — see DB90DV-560 orientation.md).
|
|
13
|
+
*/
|
|
14
|
+
const LEGACY_MCP_KEYS = ["db90", "insights"];
|
|
15
|
+
const AIXLE_INSIGHTS_MCP_KEY = "aixle-insights";
|
|
16
|
+
const BACKUP_SUFFIX = ".aixle-insights-backup";
|
|
17
|
+
function readRootObject(path) {
|
|
18
|
+
if (!existsSync(path)) {
|
|
19
|
+
return {};
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
const raw = readFileSync(path, "utf-8");
|
|
23
|
+
if (!raw.trim()) {
|
|
24
|
+
return {};
|
|
25
|
+
}
|
|
26
|
+
const parsed = JSON.parse(raw);
|
|
27
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
28
|
+
return {
|
|
29
|
+
kind: "error",
|
|
30
|
+
message: `${path}: top-level JSON value must be an object.`,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
return parsed;
|
|
34
|
+
}
|
|
35
|
+
catch (e) {
|
|
36
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
37
|
+
return { kind: "error", message: `Cannot read ${path}: ${msg}` };
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function atomicWriteJson(path, data) {
|
|
41
|
+
try {
|
|
42
|
+
const dir = dirname(path);
|
|
43
|
+
mkdirSync(dir, { recursive: true });
|
|
44
|
+
const serialized = `${JSON.stringify(data, null, 2)}\n`;
|
|
45
|
+
const tmpPath = join(dir, `.aixle-insights-cursor-json-${randomBytes(8).toString("hex")}.tmp`);
|
|
46
|
+
writeFileSync(tmpPath, serialized, "utf-8");
|
|
47
|
+
renameSync(tmpPath, path);
|
|
48
|
+
return { kind: "installed" };
|
|
49
|
+
}
|
|
50
|
+
catch (e) {
|
|
51
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
52
|
+
return { kind: "error", message: `Failed to write ${path}: ${msg}` };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/** Copy the current file to a backup path, unless a backup already exists (never clobber the user's original). */
|
|
56
|
+
function backupIfNeeded(path) {
|
|
57
|
+
if (!existsSync(path))
|
|
58
|
+
return;
|
|
59
|
+
const backupPath = path + BACKUP_SUFFIX;
|
|
60
|
+
if (existsSync(backupPath))
|
|
61
|
+
return;
|
|
62
|
+
copyFileSync(path, backupPath);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Merges top-level `mcpServers.aixle-insights` into ~/.cursor/mcp.json (or overridden path).
|
|
66
|
+
* Preserves all other keys and MCP server entries. Backs up the existing file
|
|
67
|
+
* (once) before the first write, and removes legacy "db90"/"insights" keys so
|
|
68
|
+
* Cursor does not spawn duplicate aixle-insights servers.
|
|
69
|
+
*/
|
|
70
|
+
export function installCursorUserMcp(options = {}) {
|
|
71
|
+
const path = options.cursorConfigPath ?? defaultCursorUserConfigPath();
|
|
72
|
+
const force = options.force === true;
|
|
73
|
+
const desired = desiredAixleInsightsEntry();
|
|
74
|
+
const rootRead = readRootObject(path);
|
|
75
|
+
if ("kind" in rootRead && typeof rootRead.kind === "string" && rootRead.kind === "error") {
|
|
76
|
+
return rootRead;
|
|
77
|
+
}
|
|
78
|
+
const root = {
|
|
79
|
+
...rootRead,
|
|
80
|
+
};
|
|
81
|
+
const rawServers = root["mcpServers"];
|
|
82
|
+
let mcpServers;
|
|
83
|
+
if (rawServers === undefined) {
|
|
84
|
+
mcpServers = {};
|
|
85
|
+
}
|
|
86
|
+
else if (typeof rawServers === "object" &&
|
|
87
|
+
rawServers !== null &&
|
|
88
|
+
!Array.isArray(rawServers)) {
|
|
89
|
+
mcpServers = { ...rawServers };
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
return {
|
|
93
|
+
kind: "error",
|
|
94
|
+
message: `Invalid Cursor MCP config at ${path}: "mcpServers" must be an object when present.`,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
const existing = mcpServers[AIXLE_INSIGHTS_MCP_KEY];
|
|
98
|
+
const hasLegacyKey = LEGACY_MCP_KEYS.some((k) => mcpServers[k] !== undefined);
|
|
99
|
+
if (existing !== undefined && aixleInsightsEntryMatchesDesired(existing, desired)) {
|
|
100
|
+
if (!hasLegacyKey) {
|
|
101
|
+
return { kind: "already-configured" };
|
|
102
|
+
}
|
|
103
|
+
backupIfNeeded(path);
|
|
104
|
+
for (const key of LEGACY_MCP_KEYS)
|
|
105
|
+
delete mcpServers[key];
|
|
106
|
+
root["mcpServers"] = mcpServers;
|
|
107
|
+
return atomicWriteJson(path, root);
|
|
108
|
+
}
|
|
109
|
+
if (existing !== undefined && !force) {
|
|
110
|
+
return {
|
|
111
|
+
kind: "requires-force",
|
|
112
|
+
detail: 'A different "aixle-insights" MCP server entry already exists in ~/.cursor/mcp.json. Re-run with `init --force` to replace only that entry.',
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
backupIfNeeded(path);
|
|
116
|
+
for (const key of LEGACY_MCP_KEYS)
|
|
117
|
+
delete mcpServers[key];
|
|
118
|
+
mcpServers[AIXLE_INSIGHTS_MCP_KEY] = { command: desired.command, args: desired.args };
|
|
119
|
+
root["mcpServers"] = mcpServers;
|
|
120
|
+
return atomicWriteJson(path, root);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Removes the aixle-insights entry from ~/.cursor/mcp.json without disturbing any
|
|
124
|
+
* other server entry — including ones the user or Cursor added after install.
|
|
125
|
+
* Works on the CURRENT file (never a full-file revert to the backup). If a
|
|
126
|
+
* pre-install backup exists, the user's ORIGINAL aixle-insights entry (if any)
|
|
127
|
+
* is restored from it; otherwise our key is simply removed. The backup file is
|
|
128
|
+
* cleaned up either way.
|
|
129
|
+
*/
|
|
130
|
+
export function uninstallCursorUserMcp(options = {}) {
|
|
131
|
+
const path = options.cursorConfigPath ?? defaultCursorUserConfigPath();
|
|
132
|
+
const backupPath = path + BACKUP_SUFFIX;
|
|
133
|
+
const hasBackup = existsSync(backupPath);
|
|
134
|
+
const cleanupBackup = () => {
|
|
135
|
+
if (hasBackup) {
|
|
136
|
+
try {
|
|
137
|
+
unlinkSync(backupPath);
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
/* non-fatal */
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
if (!existsSync(path)) {
|
|
145
|
+
cleanupBackup();
|
|
146
|
+
return { kind: "noop" };
|
|
147
|
+
}
|
|
148
|
+
const rootRead = readRootObject(path);
|
|
149
|
+
if ("kind" in rootRead && typeof rootRead.kind === "string" && rootRead.kind === "error") {
|
|
150
|
+
return rootRead;
|
|
151
|
+
}
|
|
152
|
+
const root = { ...rootRead };
|
|
153
|
+
const rawServers = root["mcpServers"];
|
|
154
|
+
if (typeof rawServers !== "object" || rawServers === null || Array.isArray(rawServers)) {
|
|
155
|
+
cleanupBackup();
|
|
156
|
+
return { kind: "noop" };
|
|
157
|
+
}
|
|
158
|
+
const mcpServers = { ...rawServers };
|
|
159
|
+
// If a backup exists, recover the user's ORIGINAL aixle-insights entry (if they
|
|
160
|
+
// had one we overwrote) rather than just deleting ours — but only that one key,
|
|
161
|
+
// leaving every current sibling entry intact.
|
|
162
|
+
let restoredPrior = false;
|
|
163
|
+
if (hasBackup) {
|
|
164
|
+
const backupRead = readRootObject(backupPath);
|
|
165
|
+
const backupIsObject = !("kind" in backupRead &&
|
|
166
|
+
typeof backupRead.kind === "string" &&
|
|
167
|
+
backupRead.kind === "error");
|
|
168
|
+
if (backupIsObject) {
|
|
169
|
+
const backupServers = backupRead["mcpServers"];
|
|
170
|
+
if (typeof backupServers === "object" && backupServers !== null && !Array.isArray(backupServers)) {
|
|
171
|
+
const priorEntry = backupServers[AIXLE_INSIGHTS_MCP_KEY];
|
|
172
|
+
if (priorEntry !== undefined) {
|
|
173
|
+
mcpServers[AIXLE_INSIGHTS_MCP_KEY] = priorEntry;
|
|
174
|
+
restoredPrior = true;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (!restoredPrior) {
|
|
180
|
+
if (mcpServers[AIXLE_INSIGHTS_MCP_KEY] === undefined) {
|
|
181
|
+
cleanupBackup();
|
|
182
|
+
return { kind: "noop" };
|
|
183
|
+
}
|
|
184
|
+
delete mcpServers[AIXLE_INSIGHTS_MCP_KEY];
|
|
185
|
+
}
|
|
186
|
+
root["mcpServers"] = mcpServers;
|
|
187
|
+
const writeResult = atomicWriteJson(path, root);
|
|
188
|
+
if (writeResult.kind === "error") {
|
|
189
|
+
return writeResult;
|
|
190
|
+
}
|
|
191
|
+
cleanupBackup();
|
|
192
|
+
return restoredPrior ? { kind: "restored", backupPath } : { kind: "removed" };
|
|
193
|
+
}
|
package/dist/install/index.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { type InstallClaudeUserMcpOptions, type InstallResult } from "./claude.js";
|
|
2
|
-
|
|
3
|
-
export type
|
|
2
|
+
import { type InstallCursorUserMcpOptions, type UninstallResult } from "./cursor.js";
|
|
3
|
+
export type SupportedEditor = "claude" | "cursor";
|
|
4
|
+
export type { InstallClaudeUserMcpOptions, InstallCursorUserMcpOptions, InstallResult, UninstallResult, };
|
|
4
5
|
/**
|
|
5
|
-
* Editor dispatch for MCP install hooks.
|
|
6
|
+
* Editor dispatch for MCP install hooks.
|
|
6
7
|
*/
|
|
7
|
-
export declare function installEditorMcp(editor: SupportedEditor, options?: InstallClaudeUserMcpOptions): InstallResult;
|
|
8
|
+
export declare function installEditorMcp(editor: SupportedEditor, options?: InstallClaudeUserMcpOptions & InstallCursorUserMcpOptions): InstallResult;
|
|
8
9
|
export { installClaudeUserMcp, defaultClaudeUserConfigPath, desiredAixleInsightsEntry, aixleInsightsEntryMatchesDesired, } from "./claude.js";
|
|
10
|
+
export { installCursorUserMcp, uninstallCursorUserMcp, defaultCursorUserConfigPath, } from "./cursor.js";
|
package/dist/install/index.js
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import { installClaudeUserMcp, } from "./claude.js";
|
|
2
|
+
import { installCursorUserMcp, } from "./cursor.js";
|
|
2
3
|
/**
|
|
3
|
-
* Editor dispatch for MCP install hooks.
|
|
4
|
+
* Editor dispatch for MCP install hooks.
|
|
4
5
|
*/
|
|
5
6
|
export function installEditorMcp(editor, options = {}) {
|
|
6
7
|
if (editor === "claude") {
|
|
7
8
|
return installClaudeUserMcp(options);
|
|
8
9
|
}
|
|
10
|
+
if (editor === "cursor") {
|
|
11
|
+
return installCursorUserMcp(options);
|
|
12
|
+
}
|
|
9
13
|
return { kind: "error", message: `Unsupported editor for MCP install: ${String(editor)}` };
|
|
10
14
|
}
|
|
11
15
|
export { installClaudeUserMcp, defaultClaudeUserConfigPath, desiredAixleInsightsEntry, aixleInsightsEntryMatchesDesired, } from "./claude.js";
|
|
16
|
+
export { installCursorUserMcp, uninstallCursorUserMcp, defaultCursorUserConfigPath, } from "./cursor.js";
|
package/dist/lib/client.d.ts
CHANGED
|
@@ -8,6 +8,13 @@ export interface IngestPayload {
|
|
|
8
8
|
[key: string]: unknown;
|
|
9
9
|
}
|
|
10
10
|
export interface PostEventOptions {
|
|
11
|
+
/**
|
|
12
|
+
* Skip the HTTPS-or-loopback gate for this host. Only ever set this to
|
|
13
|
+
* `true` when the caller has independently confirmed the user explicitly
|
|
14
|
+
* consented via `init --insecure` for this exact credential (see
|
|
15
|
+
* `StoredCredentials.insecureHttpAllowed`). Defaults to `false`.
|
|
16
|
+
*/
|
|
17
|
+
allowInsecureHttp?: boolean;
|
|
11
18
|
/** Override default console.error on non-ok HTTP response. */
|
|
12
19
|
onHttpError?: (status: number, statusText: string, body: string) => void;
|
|
13
20
|
/** Override default console.error on network-level failure. */
|
package/dist/lib/client.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { evaluateTransportSecurity } from "./transport-security.js";
|
|
1
2
|
/**
|
|
2
3
|
* POST a single event payload to the db90 ingest endpoint.
|
|
3
4
|
*
|
|
@@ -10,6 +11,22 @@
|
|
|
10
11
|
* Never throws — callers can rely on Promise.allSettled-style aggregation.
|
|
11
12
|
*/
|
|
12
13
|
export async function postEvent(payload, host, token, options = {}) {
|
|
14
|
+
const transportSecurity = evaluateTransportSecurity(host, {
|
|
15
|
+
allowInsecureHttp: options.allowInsecureHttp === true,
|
|
16
|
+
label: "DB90 ingest host",
|
|
17
|
+
});
|
|
18
|
+
if (!transportSecurity.ok) {
|
|
19
|
+
// Deliberately does NOT call options.onNetworkError/onHttpError: the retry
|
|
20
|
+
// wrapper in src/client.ts treats those as transient and retries with
|
|
21
|
+
// backoff. A scheme rejection is permanent — retrying wastes ~21s per
|
|
22
|
+
// event for nothing. Bare console.error mirrors this file's existing
|
|
23
|
+
// unrecoverable-failure logging style.
|
|
24
|
+
console.error(`Blocked event send — ${transportSecurity.error}`);
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
if (transportSecurity.warning) {
|
|
28
|
+
console.error(`Warning: ${transportSecurity.warning}`);
|
|
29
|
+
}
|
|
13
30
|
const url = `${host.replace(/\/$/, "")}/api/v1/ingest/events`;
|
|
14
31
|
const headers = {
|
|
15
32
|
"Content-Type": "application/json",
|
package/dist/lib/config.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
+
import { mcpLog } from "../log.js";
|
|
3
4
|
/**
|
|
4
5
|
* Load a connector's `config.json` from disk. Returns `{}` on missing or
|
|
5
6
|
* malformed files — callers fall back to env vars / CLI flags / defaults.
|
|
@@ -32,8 +33,12 @@ export function loadBaseConfig(configDir, parsePricing) {
|
|
|
32
33
|
return result;
|
|
33
34
|
}
|
|
34
35
|
}
|
|
35
|
-
catch {
|
|
36
|
-
|
|
36
|
+
catch (err) {
|
|
37
|
+
const code = err?.code;
|
|
38
|
+
if (code !== "ENOENT") {
|
|
39
|
+
// Config file exists but failed to parse — distinguishes tampering from "never created".
|
|
40
|
+
mcpLog.warn("config_parse_failed", { path: configPath, error: err instanceof Error ? err.message : String(err) }, false);
|
|
41
|
+
}
|
|
37
42
|
}
|
|
38
43
|
return {};
|
|
39
44
|
}
|
|
@@ -6,8 +6,8 @@ export interface LookupResult {
|
|
|
6
6
|
project_id: string;
|
|
7
7
|
name: string;
|
|
8
8
|
}
|
|
9
|
-
export declare function resolveProjectId(flagValue: string | undefined, configValue: string | undefined, host: string, token: string, verbose: boolean): Promise<ProjectResolution>;
|
|
10
|
-
export declare function resolveProjectIdForRepoPath(repoPath: string, host: string, token: string, verbose: boolean): Promise<ProjectResolution>;
|
|
9
|
+
export declare function resolveProjectId(flagValue: string | undefined, configValue: string | undefined, host: string, token: string, verbose: boolean, allowInsecureHttp?: boolean): Promise<ProjectResolution>;
|
|
10
|
+
export declare function resolveProjectIdForRepoPath(repoPath: string, host: string, token: string, verbose: boolean, allowInsecureHttp?: boolean): Promise<ProjectResolution>;
|
|
11
11
|
export declare function getGitRemote(verbose: boolean): string | null;
|
|
12
12
|
export declare function getGitRemoteForPath(repoPath: string, verbose: boolean): string | null;
|
|
13
13
|
/**
|
|
@@ -24,7 +24,7 @@ export declare function canonicalizeGitRemote(remote: string, verbose: boolean):
|
|
|
24
24
|
* git remote. Expand to remotes the API lookup understands (`Project.normalize_git_remote`).
|
|
25
25
|
*/
|
|
26
26
|
export declare function repoNameToGitRemoteCandidates(repoName: string): string[];
|
|
27
|
-
export declare function lookupProjectByRepoName(repoName: string, host: string, token: string, verbose: boolean): Promise<LookupResult | "not-found" | null>;
|
|
27
|
+
export declare function lookupProjectByRepoName(repoName: string, host: string, token: string, verbose: boolean, allowInsecureHttp?: boolean): Promise<LookupResult | "not-found" | null>;
|
|
28
28
|
/** Payload shape shared by db90-cursor and telemetry-mcp commit mappers. */
|
|
29
29
|
export interface CommitAttributionPayload {
|
|
30
30
|
event_type?: string;
|
|
@@ -44,5 +44,6 @@ export declare function enrichCommitProjectAttribution(payloads: CommitAttributi
|
|
|
44
44
|
host: string;
|
|
45
45
|
token: string;
|
|
46
46
|
verbose?: boolean;
|
|
47
|
+
allowInsecureHttp?: boolean;
|
|
47
48
|
}): Promise<void>;
|
|
48
|
-
export declare function lookupProjectByRemote(gitRemote: string, host: string, token: string, verbose: boolean): Promise<LookupResult | "not-found" | null>;
|
|
49
|
+
export declare function lookupProjectByRemote(gitRemote: string, host: string, token: string, verbose: boolean, allowInsecureHttp?: boolean): Promise<LookupResult | "not-found" | null>;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { evaluateTransportSecurity } from "./transport-security.js";
|
|
2
3
|
/** Coerce empty string to undefined so "" is treated as "not set" */
|
|
3
4
|
function coerce(val) {
|
|
4
5
|
return val === "" ? undefined : val;
|
|
5
6
|
}
|
|
6
|
-
export async function resolveProjectId(flagValue, configValue, host, token, verbose) {
|
|
7
|
+
export async function resolveProjectId(flagValue, configValue, host, token, verbose, allowInsecureHttp = false) {
|
|
7
8
|
const flag = coerce(flagValue);
|
|
8
9
|
const config = coerce(configValue);
|
|
9
10
|
if (flag !== undefined)
|
|
@@ -13,18 +14,18 @@ export async function resolveProjectId(flagValue, configValue, host, token, verb
|
|
|
13
14
|
const gitRemote = getGitRemote(verbose);
|
|
14
15
|
if (gitRemote === null)
|
|
15
16
|
return { projectId: null, source: "none" };
|
|
16
|
-
const result = await lookupProjectByRemote(gitRemote, host, token, verbose);
|
|
17
|
+
const result = await lookupProjectByRemote(gitRemote, host, token, verbose, allowInsecureHttp);
|
|
17
18
|
if (result === "not-found")
|
|
18
19
|
return { projectId: null, source: "auto-detect-not-found" };
|
|
19
20
|
if (result !== null)
|
|
20
21
|
return { projectId: result.project_id, source: "auto-detect" };
|
|
21
22
|
return { projectId: null, source: "none" };
|
|
22
23
|
}
|
|
23
|
-
export async function resolveProjectIdForRepoPath(repoPath, host, token, verbose) {
|
|
24
|
+
export async function resolveProjectIdForRepoPath(repoPath, host, token, verbose, allowInsecureHttp = false) {
|
|
24
25
|
const gitRemote = getGitRemoteForPath(repoPath, verbose);
|
|
25
26
|
if (gitRemote === null)
|
|
26
27
|
return { projectId: null, source: "none" };
|
|
27
|
-
const result = await lookupProjectByRemote(gitRemote, host, token, verbose);
|
|
28
|
+
const result = await lookupProjectByRemote(gitRemote, host, token, verbose, allowInsecureHttp);
|
|
28
29
|
if (result === "not-found")
|
|
29
30
|
return { projectId: null, source: "auto-detect-not-found" };
|
|
30
31
|
if (result !== null)
|
|
@@ -133,7 +134,7 @@ export function repoNameToGitRemoteCandidates(repoName) {
|
|
|
133
134
|
}
|
|
134
135
|
return [];
|
|
135
136
|
}
|
|
136
|
-
export async function lookupProjectByRepoName(repoName, host, token, verbose) {
|
|
137
|
+
export async function lookupProjectByRepoName(repoName, host, token, verbose, allowInsecureHttp = false) {
|
|
137
138
|
const candidates = repoNameToGitRemoteCandidates(repoName);
|
|
138
139
|
if (candidates.length === 0) {
|
|
139
140
|
if (verbose)
|
|
@@ -141,7 +142,7 @@ export async function lookupProjectByRepoName(repoName, host, token, verbose) {
|
|
|
141
142
|
return "not-found";
|
|
142
143
|
}
|
|
143
144
|
for (const candidate of candidates) {
|
|
144
|
-
const result = await lookupProjectByRemote(candidate, host, token, verbose);
|
|
145
|
+
const result = await lookupProjectByRemote(candidate, host, token, verbose, allowInsecureHttp);
|
|
145
146
|
if (result === "not-found")
|
|
146
147
|
continue;
|
|
147
148
|
return result;
|
|
@@ -164,7 +165,7 @@ export async function enrichCommitProjectAttribution(payloads, options) {
|
|
|
164
165
|
const repoName = payload.metadata?.repo_name;
|
|
165
166
|
if (!repoName)
|
|
166
167
|
continue;
|
|
167
|
-
const result = await lookupProjectByRepoName(repoName, options.host, options.token, options.verbose ?? false);
|
|
168
|
+
const result = await lookupProjectByRepoName(repoName, options.host, options.token, options.verbose ?? false, options.allowInsecureHttp === true);
|
|
168
169
|
if (result && typeof result === "object" && "project_id" in result) {
|
|
169
170
|
payload.project_id = result.project_id;
|
|
170
171
|
if (options.verbose) {
|
|
@@ -173,7 +174,18 @@ export async function enrichCommitProjectAttribution(payloads, options) {
|
|
|
173
174
|
}
|
|
174
175
|
}
|
|
175
176
|
}
|
|
176
|
-
export async function lookupProjectByRemote(gitRemote, host, token, verbose) {
|
|
177
|
+
export async function lookupProjectByRemote(gitRemote, host, token, verbose, allowInsecureHttp = false) {
|
|
178
|
+
const transportSecurity = evaluateTransportSecurity(host, {
|
|
179
|
+
allowInsecureHttp,
|
|
180
|
+
label: "DB90 project-lookup host",
|
|
181
|
+
});
|
|
182
|
+
if (!transportSecurity.ok) {
|
|
183
|
+
console.error(`Blocked project lookup — ${transportSecurity.error}`);
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
if (transportSecurity.warning) {
|
|
187
|
+
console.error(`Warning: ${transportSecurity.warning}`);
|
|
188
|
+
}
|
|
177
189
|
const url = `${host.replace(/\/$/, "")}/api/v1/projects/lookup?git_remote=${encodeURIComponent(gitRemote)}`;
|
|
178
190
|
try {
|
|
179
191
|
const res = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type TransportSecurityResult = {
|
|
2
|
+
ok: true;
|
|
3
|
+
warning?: string;
|
|
4
|
+
} | {
|
|
5
|
+
ok: false;
|
|
6
|
+
error: string;
|
|
7
|
+
};
|
|
8
|
+
export interface TransportSecurityOptions {
|
|
9
|
+
allowInsecureHttp: boolean;
|
|
10
|
+
label: string;
|
|
11
|
+
}
|
|
12
|
+
export declare function isLoopbackHost(hostname: string): boolean;
|
|
13
|
+
export declare function evaluateTransportSecurity(rawUrl: string, options: TransportSecurityOptions): TransportSecurityResult;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
function isIpv4Loopback(hostname) {
|
|
2
|
+
const parts = hostname.split(".");
|
|
3
|
+
if (parts.length !== 4)
|
|
4
|
+
return false;
|
|
5
|
+
const octets = parts.map((part) => Number(part));
|
|
6
|
+
return octets.every((octet, index) => Number.isInteger(octet) &&
|
|
7
|
+
octet >= 0 &&
|
|
8
|
+
octet <= 255 &&
|
|
9
|
+
String(octet) === parts[index]) && octets[0] === 127;
|
|
10
|
+
}
|
|
11
|
+
export function isLoopbackHost(hostname) {
|
|
12
|
+
const normalized = hostname.toLowerCase();
|
|
13
|
+
return normalized === "localhost" ||
|
|
14
|
+
normalized === "::1" ||
|
|
15
|
+
normalized === "[::1]" ||
|
|
16
|
+
isIpv4Loopback(normalized);
|
|
17
|
+
}
|
|
18
|
+
function plaintextMessage(label, host, action) {
|
|
19
|
+
const prefix = `${label} ${host} uses remote plaintext HTTP`;
|
|
20
|
+
const guidance = "Plaintext HTTP can expose ingest tokens and telemetry. Use HTTPS for remote hosts";
|
|
21
|
+
if (action === "warn") {
|
|
22
|
+
return `${prefix}. ${guidance}; --insecure should only be used for trusted non-production test endpoints.`;
|
|
23
|
+
}
|
|
24
|
+
return `${prefix}. ${guidance}, or pass --insecure only for a trusted non-production test endpoint.`;
|
|
25
|
+
}
|
|
26
|
+
export function evaluateTransportSecurity(rawUrl, options) {
|
|
27
|
+
let parsed;
|
|
28
|
+
try {
|
|
29
|
+
parsed = new URL(rawUrl);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return { ok: false, error: `${options.label} must be a valid URL.` };
|
|
33
|
+
}
|
|
34
|
+
if (parsed.protocol === "https:") {
|
|
35
|
+
return { ok: true };
|
|
36
|
+
}
|
|
37
|
+
if (parsed.protocol !== "http:") {
|
|
38
|
+
return { ok: false, error: `${options.label} must use HTTPS, or HTTP for localhost/loopback local development.` };
|
|
39
|
+
}
|
|
40
|
+
if (isLoopbackHost(parsed.hostname)) {
|
|
41
|
+
return { ok: true };
|
|
42
|
+
}
|
|
43
|
+
if (options.allowInsecureHttp) {
|
|
44
|
+
return { ok: true, warning: plaintextMessage(options.label, parsed.host, "warn") };
|
|
45
|
+
}
|
|
46
|
+
return { ok: false, error: plaintextMessage(options.label, parsed.host, "reject") };
|
|
47
|
+
}
|
package/dist/pricing.d.ts
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
|
export interface ModelPricing {
|
|
8
8
|
input_per_mtok: number;
|
|
@@ -32,6 +32,14 @@ export declare const DEFAULT_PRICING: PricingTable;
|
|
|
32
32
|
* - Returns a new table object so mutations never affect DEFAULT_PRICING.
|
|
33
33
|
*/
|
|
34
34
|
export declare function mergePricing(base: PricingTable, overrides: PricingTable): PricingTable;
|
|
35
|
+
/**
|
|
36
|
+
* Strips a trailing -YYYYMMDD date suffix to produce the bare model key used
|
|
37
|
+
* in the pricing table. Returns the original ID when:
|
|
38
|
+
* - it has no date suffix, OR
|
|
39
|
+
* - the bare form is not in the table (i.e. the dated form IS the canonical key,
|
|
40
|
+
* like legacy "claude-3-5-sonnet-20241022").
|
|
41
|
+
*/
|
|
42
|
+
export declare function normalizeModelId(model: string, pricing: PricingTable): string;
|
|
35
43
|
export declare function calculateCost(model: string | null, baseInputTokens: number, outputTokens: number, cacheWriteTokens: number, cacheReadTokens: number, pricing: PricingTable): number | null;
|
|
36
44
|
/**
|
|
37
45
|
* Returns a human-readable warning string explaining why cost could not be
|