@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.
Files changed (45) hide show
  1. package/README.md +36 -8
  2. package/dist/auth/credentials.d.ts +7 -1
  3. package/dist/auth/credentials.js +71 -14
  4. package/dist/auth/exchange.d.ts +1 -1
  5. package/dist/auth/exchange.js +1 -1
  6. package/dist/auth/flow.d.ts +8 -1
  7. package/dist/auth/flow.js +27 -5
  8. package/dist/auth/keycloak.d.ts +1 -1
  9. package/dist/auth/keycloak.js +20 -1
  10. package/dist/cli.d.ts +5 -3
  11. package/dist/cli.js +69 -21
  12. package/dist/collect-cursor-payloads.d.ts +4 -3
  13. package/dist/collect-cursor-payloads.js +8 -5
  14. package/dist/cursor-checkpoints.d.ts +2 -2
  15. package/dist/cursor-payload-contract.d.ts +5 -5
  16. package/dist/cursor-payload-contract.js +6 -0
  17. package/dist/cursor-settings.d.ts +9 -4
  18. package/dist/cursor-settings.js +80 -10
  19. package/dist/health.d.ts +3 -1
  20. package/dist/health.js +13 -1
  21. package/dist/hooks/cursor-hooks-mapper.d.ts +3 -3
  22. package/dist/hooks/cursor-hooks-mapper.js +1 -1
  23. package/dist/hooks/cursor-hooks-reader.d.ts +2 -0
  24. package/dist/hooks/cursor-hooks-reader.js +2 -2
  25. package/dist/install/cursor.d.ts +34 -0
  26. package/dist/install/cursor.js +193 -0
  27. package/dist/install/index.d.ts +6 -4
  28. package/dist/install/index.js +6 -1
  29. package/dist/lib/client.d.ts +7 -0
  30. package/dist/lib/client.js +17 -0
  31. package/dist/lib/config.js +7 -2
  32. package/dist/lib/project-resolver.d.ts +5 -4
  33. package/dist/lib/project-resolver.js +20 -8
  34. package/dist/lib/transport-security.d.ts +1 -0
  35. package/dist/lib/transport-security.js +1 -1
  36. package/dist/readers/claude.d.ts +54 -6
  37. package/dist/readers/claude.js +154 -2
  38. package/dist/readers/cursor.d.ts +10 -7
  39. package/dist/readers/cursor.js +101 -15
  40. package/dist/server.d.ts +20 -3
  41. package/dist/server.js +101 -67
  42. package/dist/state.js +7 -2
  43. package/dist/sync.d.ts +4 -2
  44. package/dist/sync.js +61 -46
  45. 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 `db90_status` — tolerates missing/malformed credentials and state. */
81
- export async function buildDb90StatusPayload() {
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: "db90_sync_now", reason: "missing_credentials" }, false);
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
- /** In-process MCP server instance (stdio not attached). */
108
- export function createDb90McpServer() {
109
- const server = new McpServer({ name: SERVER_NAME, version: SERVER_VERSION }, { capabilities: { tools: {} } });
110
- server.registerTool("db90_status", {
111
- description: "Returns Aixle Insights MCP connectivity and last sync metadata from disk (credentials + state). No arguments.",
112
- }, async () => jsonContent(await buildDb90StatusPayload()));
113
- server.registerTool("db90_sync_now", {
114
- description: "Runs one DB90 ingest sync cycle for enabled tools immediately (matches background cadence). " +
115
- "Optional `tools` subset filter: omit to sync every tool credential you have authenticated (Claude transcripts + Cursor telemetry).",
116
- inputSchema: SYNC_NOW_INPUT_SCHEMA,
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: err instanceof Error ? err.message : String(err),
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
- catch (err) {
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: err instanceof Error ? err.message : String(err),
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 = createDb90McpServer();
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" }, false);
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
- // missing or malformed state file — start fresh
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 { CursorDb90Payload } from "./readers/cursor.js";
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: CursorDb90Payload): string | undefined;
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 payload = mapClaudeTranscriptTurn(turn, { projectId: resolvedProjectId, pricing });
231
- if (verbose && payload.cost_usd === null) {
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
- console.log(`[dry-run] Would send Claude turn ${turn.turnId}:`);
245
- console.log(JSON.stringify(payload, null, 2));
246
- totalSent++;
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
- if (verbose) {
250
- console.log(`[verbose] Sending Claude turn ${turn.turnId} (${turn.tokensIn + turn.tokensOut} tokens)`);
251
- }
252
- const ok = await postEvent(payload, host, token, {
253
- on429: (retryAfter, quotaExceeded) => {
254
- const currentBackoff = backoffUntilByCredential.get(backoffKey);
255
- const nextBackoff = new Date(Math.max(currentBackoff?.getTime() ?? 0, Date.now() + retryAfter * 1000));
256
- backoffUntilByCredential.set(backoffKey, nextBackoff);
257
- shouldStopForBackoff = true;
258
- const reason = quotaExceeded ? "Monthly quota exceeded" : "Rate limited";
259
- mcpLog.warn("sync_rate_limit_pause", {
260
- tool: "claude_code",
261
- retry_until: nextBackoff.toISOString(),
262
- quota_exceeded: quotaExceeded,
263
- }, true);
264
- console.warn(`[aixle-insights] ${reason}. Pausing until ${nextBackoff.toISOString()}.`);
265
- // Persist backoff to state so it survives process restarts
266
- state = { ...state, rate_limited_until: nextBackoff.toISOString() };
267
- writeState(state, appDir, host, token);
268
- },
269
- });
270
- if (ok) {
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
- totalFailed++;
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.0",
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.19.0"
22
+ "node": ">=20"
23
23
  },
24
24
  "license": "MIT",
25
25
  "repository": {