@aixle/insights 0.2.0 → 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 +36 -8
- 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 +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 +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 +101 -15
- 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/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,7 @@ export declare function getSyncTelemetry(): {
|
|
|
65
67
|
lastResult: SyncResult | null;
|
|
66
68
|
recentErrors: string[];
|
|
67
69
|
};
|
|
68
|
-
export declare function cursorRepoPathFromPayload(payload:
|
|
70
|
+
export declare function cursorRepoPathFromPayload(payload: CursorPayload): string | undefined;
|
|
69
71
|
/**
|
|
70
72
|
* Parallel multi-tool cycle under the global advisory ingest lock (`state.lock`).
|
|
71
73
|
*/
|
package/dist/sync.js
CHANGED
|
@@ -102,7 +102,7 @@ function explicitProjectId(projectId, projectIdSource) {
|
|
|
102
102
|
? projectId
|
|
103
103
|
: undefined;
|
|
104
104
|
}
|
|
105
|
-
async function resolveProjectIdForRepoPathCached(repoPath, host, token, verbose, cache) {
|
|
105
|
+
async function resolveProjectIdForRepoPathCached(repoPath, host, token, verbose, cache, allowInsecureHttp = false) {
|
|
106
106
|
const normalized = repoPath?.trim();
|
|
107
107
|
if (!normalized)
|
|
108
108
|
return null;
|
|
@@ -113,7 +113,7 @@ async function resolveProjectIdForRepoPathCached(repoPath, host, token, verbose,
|
|
|
113
113
|
cache.set(normalized, null);
|
|
114
114
|
return null;
|
|
115
115
|
}
|
|
116
|
-
const result = await lookupProjectByRemote(canonicalRemote, host, token, verbose);
|
|
116
|
+
const result = await lookupProjectByRemote(canonicalRemote, host, token, verbose, allowInsecureHttp);
|
|
117
117
|
const projectId = result && typeof result === "object" && "project_id" in result ? result.project_id : null;
|
|
118
118
|
cache.set(normalized, projectId);
|
|
119
119
|
return projectId;
|
|
@@ -131,7 +131,7 @@ export function cursorRepoPathFromPayload(payload) {
|
|
|
131
131
|
return undefined;
|
|
132
132
|
}
|
|
133
133
|
async function runClaudeSlice(options) {
|
|
134
|
-
const { token, host, dryRun, verbose, projectId, pricing } = options;
|
|
134
|
+
const { token, host, dryRun, verbose, projectId, pricing, allowInsecureHttp = false } = options;
|
|
135
135
|
const appDir = options.appDir ?? getAppDir();
|
|
136
136
|
const backoffKey = credentialStateKey(host, token);
|
|
137
137
|
const errors = [];
|
|
@@ -222,13 +222,16 @@ async function runClaudeSlice(options) {
|
|
|
222
222
|
// effectively one network call per unique cwd per sync.
|
|
223
223
|
const resolvedProjectId = scopeDir
|
|
224
224
|
? (projectId ??
|
|
225
|
-
(await resolveProjectIdForRepoPathCached(turn.cwd, host, token, verbose, projectLookupCache)) ??
|
|
225
|
+
(await resolveProjectIdForRepoPathCached(turn.cwd, host, token, verbose, projectLookupCache, allowInsecureHttp)) ??
|
|
226
226
|
undefined)
|
|
227
227
|
: (explicitProject ??
|
|
228
|
-
(await resolveProjectIdForRepoPathCached(turn.cwd, host, token, verbose, projectLookupCache)) ??
|
|
228
|
+
(await resolveProjectIdForRepoPathCached(turn.cwd, host, token, verbose, projectLookupCache, allowInsecureHttp)) ??
|
|
229
229
|
undefined);
|
|
230
|
-
const
|
|
231
|
-
if (
|
|
230
|
+
const payloads = mapClaudeTranscriptTurn(turn, { projectId: resolvedProjectId, pricing });
|
|
231
|
+
if (!payloads?.length)
|
|
232
|
+
continue;
|
|
233
|
+
const parentPayload = payloads[0];
|
|
234
|
+
if (verbose && parentPayload?.cost_usd === null) {
|
|
232
235
|
if (!turn.model) {
|
|
233
236
|
if (turn.tokensIn > 0 || turn.tokensOut > 0) {
|
|
234
237
|
console.warn(`[warn] Claude turn ${turn.turnId} has usage but no model — cost_usd will be null`);
|
|
@@ -241,45 +244,53 @@ async function runClaudeSlice(options) {
|
|
|
241
244
|
}
|
|
242
245
|
}
|
|
243
246
|
if (dryRun) {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
+
for (const payload of payloads) {
|
|
248
|
+
console.log(`[dry-run] Would send Claude ${payload.event_type} ${payload.metadata.session_id}:`);
|
|
249
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
250
|
+
}
|
|
251
|
+
totalSent += payloads.length;
|
|
247
252
|
continue;
|
|
248
253
|
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
254
|
+
let allOk = true;
|
|
255
|
+
for (const payload of payloads) {
|
|
256
|
+
if (verbose) {
|
|
257
|
+
console.log(`[verbose] Sending Claude ${payload.event_type} ${payload.metadata.session_id}`);
|
|
258
|
+
}
|
|
259
|
+
const ok = await postEvent(payload, host, token, {
|
|
260
|
+
on429: (retryAfter, quotaExceeded) => {
|
|
261
|
+
const currentBackoff = backoffUntilByCredential.get(backoffKey);
|
|
262
|
+
const nextBackoff = new Date(Math.max(currentBackoff?.getTime() ?? 0, Date.now() + retryAfter * 1000));
|
|
263
|
+
backoffUntilByCredential.set(backoffKey, nextBackoff);
|
|
264
|
+
shouldStopForBackoff = true;
|
|
265
|
+
const reason = quotaExceeded ? "Monthly quota exceeded" : "Rate limited";
|
|
266
|
+
mcpLog.warn("sync_rate_limit_pause", {
|
|
267
|
+
tool: "claude_code",
|
|
268
|
+
retry_until: nextBackoff.toISOString(),
|
|
269
|
+
quota_exceeded: quotaExceeded,
|
|
270
|
+
}, true);
|
|
271
|
+
console.warn(`[aixle-insights] ${reason}. Pausing until ${nextBackoff.toISOString()}.`);
|
|
272
|
+
// Persist backoff to state so it survives process restarts
|
|
273
|
+
state = { ...state, rate_limited_until: nextBackoff.toISOString() };
|
|
274
|
+
writeState(state, appDir, host, token);
|
|
275
|
+
},
|
|
276
|
+
});
|
|
277
|
+
if (!ok) {
|
|
278
|
+
allOk = false;
|
|
279
|
+
totalFailed++;
|
|
280
|
+
errors.push(`Failed to post Claude ${payload.event_type} for turn ${turn.turnId}`);
|
|
281
|
+
mcpLog.error("sync_ingest_final_failure", { tool: "claude_code", session_id: turn.turnId }, true);
|
|
282
|
+
break;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
if (allOk) {
|
|
286
|
+
totalSent += payloads.length;
|
|
271
287
|
state = markSessionSent(state, sKey, turn.fileSize);
|
|
272
288
|
writeState(state, appDir, host, token);
|
|
273
|
-
totalSent++;
|
|
274
289
|
}
|
|
275
|
-
else {
|
|
276
|
-
|
|
277
|
-
errors.push(`Failed to post Claude turn ${turn.turnId}`);
|
|
278
|
-
mcpLog.error("sync_ingest_final_failure", { tool: "claude_code", session_id: turn.turnId }, true);
|
|
279
|
-
if (shouldStopForBackoff) {
|
|
280
|
-
break;
|
|
281
|
-
}
|
|
290
|
+
else if (shouldStopForBackoff) {
|
|
291
|
+
break;
|
|
282
292
|
}
|
|
293
|
+
// Non-backoff failure: continue to next turn (failed turn will retry next sync)
|
|
283
294
|
}
|
|
284
295
|
const result = { sent: totalSent, failed: totalFailed, skipped: totalSkipped };
|
|
285
296
|
const currentBackoff = backoffUntilByCredential.get(backoffKey);
|
|
@@ -290,7 +301,7 @@ async function runClaudeSlice(options) {
|
|
|
290
301
|
return result;
|
|
291
302
|
}
|
|
292
303
|
async function runCursorSlice(params) {
|
|
293
|
-
const { token, host, dryRun, verbose, projectId, projectIdSource, projectLookupToken, appDir, cursorBaseDir, cursorTranscriptProjectDirs, scopeDir, fullScan = false, cursorPricing = resolveCursorPricing(undefined, appDir), } = params;
|
|
304
|
+
const { token, host, dryRun, verbose, projectId, projectIdSource, projectLookupToken, allowInsecureHttp = false, appDir, cursorBaseDir, cursorTranscriptProjectDirs, scopeDir, fullScan = false, cursorPricing = resolveCursorPricing(undefined, appDir), } = params;
|
|
294
305
|
const backoffKey = credentialStateKey(host, token);
|
|
295
306
|
// Read state first so we can restore a persisted rate-limit backoff from a prior process.
|
|
296
307
|
const stateBefore = readState(appDir, host, token);
|
|
@@ -331,6 +342,7 @@ async function runCursorSlice(params) {
|
|
|
331
342
|
host,
|
|
332
343
|
token,
|
|
333
344
|
projectLookupToken: lookupToken,
|
|
345
|
+
allowInsecureHttp,
|
|
334
346
|
verbose,
|
|
335
347
|
cursorBaseDir,
|
|
336
348
|
cursorTranscriptProjectDirs,
|
|
@@ -345,7 +357,7 @@ async function runCursorSlice(params) {
|
|
|
345
357
|
// Same fallback as Claude: when the pre-resolved projectId is null, do a
|
|
346
358
|
// per-payload lookup from the payload's workspace. Cache dedupes by path.
|
|
347
359
|
const resolved = projectId ??
|
|
348
|
-
(await resolveProjectIdForRepoPathCached(ws, host, lookupToken, verbose, projectLookupCache));
|
|
360
|
+
(await resolveProjectIdForRepoPathCached(ws, host, lookupToken, verbose, projectLookupCache, allowInsecureHttp));
|
|
349
361
|
if (resolved)
|
|
350
362
|
payload.project_id = resolved;
|
|
351
363
|
else
|
|
@@ -365,7 +377,7 @@ async function runCursorSlice(params) {
|
|
|
365
377
|
continue;
|
|
366
378
|
for (const payload of group.payloads) {
|
|
367
379
|
const repoPath = cursorRepoPathFromPayload(payload);
|
|
368
|
-
const resolvedProjectId = await resolveProjectIdForRepoPathCached(repoPath, host, lookupToken, verbose, projectLookupCache);
|
|
380
|
+
const resolvedProjectId = await resolveProjectIdForRepoPathCached(repoPath, host, lookupToken, verbose, projectLookupCache, allowInsecureHttp);
|
|
369
381
|
if (resolvedProjectId)
|
|
370
382
|
payload.project_id = resolvedProjectId;
|
|
371
383
|
else
|
|
@@ -438,7 +450,7 @@ async function runCursorSlice(params) {
|
|
|
438
450
|
totalSkipped++;
|
|
439
451
|
continue;
|
|
440
452
|
}
|
|
441
|
-
const ok = await postEvent(payload, host, token, { on429 });
|
|
453
|
+
const ok = await postEvent(payload, host, token, { on429, allowInsecureHttp });
|
|
442
454
|
if (ok) {
|
|
443
455
|
totalSent++;
|
|
444
456
|
const turnId = payload.metadata.session_id;
|
|
@@ -455,7 +467,7 @@ async function runCursorSlice(params) {
|
|
|
455
467
|
}
|
|
456
468
|
continue;
|
|
457
469
|
}
|
|
458
|
-
const batchResult = await postEvents(group.payloads, host, token, { on429 });
|
|
470
|
+
const batchResult = await postEvents(group.payloads, host, token, { on429, allowInsecureHttp });
|
|
459
471
|
totalSent += batchResult.sent;
|
|
460
472
|
totalFailed += batchResult.failed;
|
|
461
473
|
if (batchResult.failed > 0) {
|
|
@@ -501,10 +513,11 @@ async function runCursorSlice(params) {
|
|
|
501
513
|
state: stateMut,
|
|
502
514
|
host,
|
|
503
515
|
token,
|
|
516
|
+
allowInsecureHttp,
|
|
504
517
|
on429,
|
|
505
518
|
resolveProjectId: explicitProject
|
|
506
519
|
? undefined
|
|
507
|
-
: (workspace) => resolveProjectIdForRepoPathCached(workspace, host, lookupToken, verbose, projectLookupCache),
|
|
520
|
+
: (workspace) => resolveProjectIdForRepoPathCached(workspace, host, lookupToken, verbose, projectLookupCache, allowInsecureHttp),
|
|
508
521
|
verbose,
|
|
509
522
|
});
|
|
510
523
|
totalSent += hooksResult.sent;
|
|
@@ -590,6 +603,7 @@ export async function syncTelemetryTools(options) {
|
|
|
590
603
|
dryRun,
|
|
591
604
|
verbose,
|
|
592
605
|
projectId,
|
|
606
|
+
allowInsecureHttp: credentials.insecureHttpAllowed === true,
|
|
593
607
|
pricing,
|
|
594
608
|
appDir,
|
|
595
609
|
transcriptBaseDirs: options.transcriptBaseDirs,
|
|
@@ -606,6 +620,7 @@ export async function syncTelemetryTools(options) {
|
|
|
606
620
|
projectId,
|
|
607
621
|
projectIdSource,
|
|
608
622
|
projectLookupToken,
|
|
623
|
+
allowInsecureHttp: credentials.insecureHttpAllowed === true,
|
|
609
624
|
appDir,
|
|
610
625
|
cursorBaseDir: options.cursorBaseDir,
|
|
611
626
|
cursorTranscriptProjectDirs: options.cursorTranscriptProjectDirs,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aixle/insights",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1-staging",
|
|
4
4
|
"description": "stdio MCP server for AI coding-assistant telemetry — Claude transcript sync + Cursor SQLite ingest.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"provenance": false
|
|
20
20
|
},
|
|
21
21
|
"engines": {
|
|
22
|
-
"node": ">=20
|
|
22
|
+
"node": ">=20"
|
|
23
23
|
},
|
|
24
24
|
"license": "MIT",
|
|
25
25
|
"repository": {
|