@aixle/insights 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +137 -0
  3. package/dist/auth/credentials.d.ts +23 -0
  4. package/dist/auth/credentials.js +174 -0
  5. package/dist/auth/exchange.d.ts +25 -0
  6. package/dist/auth/exchange.js +87 -0
  7. package/dist/auth/flow.d.ts +24 -0
  8. package/dist/auth/flow.js +66 -0
  9. package/dist/auth/keycloak.d.ts +35 -0
  10. package/dist/auth/keycloak.js +170 -0
  11. package/dist/cli.d.ts +51 -0
  12. package/dist/cli.js +426 -0
  13. package/dist/client.d.ts +28 -0
  14. package/dist/client.js +102 -0
  15. package/dist/collect-cursor-payloads.d.ts +57 -0
  16. package/dist/collect-cursor-payloads.js +134 -0
  17. package/dist/credentials.d.ts +2 -0
  18. package/dist/credentials.js +1 -0
  19. package/dist/cursor-checkpoints.d.ts +12 -0
  20. package/dist/cursor-checkpoints.js +28 -0
  21. package/dist/cursor-config.d.ts +5 -0
  22. package/dist/cursor-config.js +34 -0
  23. package/dist/cursor-payload-contract.d.ts +17 -0
  24. package/dist/cursor-payload-contract.js +258 -0
  25. package/dist/cursor-settings.d.ts +6 -0
  26. package/dist/cursor-settings.js +38 -0
  27. package/dist/cursor-store-audit.d.ts +48 -0
  28. package/dist/cursor-store-audit.js +155 -0
  29. package/dist/daily-stats-versions.d.ts +31 -0
  30. package/dist/daily-stats-versions.js +170 -0
  31. package/dist/health.d.ts +31 -0
  32. package/dist/health.js +195 -0
  33. package/dist/hooks/cursor-hooks-mapper.d.ts +22 -0
  34. package/dist/hooks/cursor-hooks-mapper.js +84 -0
  35. package/dist/hooks/cursor-hooks-reader.d.ts +30 -0
  36. package/dist/hooks/cursor-hooks-reader.js +117 -0
  37. package/dist/hooks/hook-forwarder.mjs +110 -0
  38. package/dist/hooks/hooks-config.d.ts +92 -0
  39. package/dist/hooks/hooks-config.js +235 -0
  40. package/dist/install/claude.d.ts +37 -0
  41. package/dist/install/claude.js +144 -0
  42. package/dist/install/index.d.ts +8 -0
  43. package/dist/install/index.js +11 -0
  44. package/dist/lib/args.d.ts +26 -0
  45. package/dist/lib/args.js +17 -0
  46. package/dist/lib/client.d.ts +33 -0
  47. package/dist/lib/client.js +52 -0
  48. package/dist/lib/config.d.ts +26 -0
  49. package/dist/lib/config.js +39 -0
  50. package/dist/lib/index.d.ts +4 -0
  51. package/dist/lib/index.js +4 -0
  52. package/dist/lib/project-resolver.d.ts +48 -0
  53. package/dist/lib/project-resolver.js +203 -0
  54. package/dist/lock.d.ts +9 -0
  55. package/dist/lock.js +84 -0
  56. package/dist/log.d.ts +14 -0
  57. package/dist/log.js +81 -0
  58. package/dist/pricing.d.ts +40 -0
  59. package/dist/pricing.js +149 -0
  60. package/dist/readers/claude.d.ts +83 -0
  61. package/dist/readers/claude.js +317 -0
  62. package/dist/readers/cursor.d.ts +134 -0
  63. package/dist/readers/cursor.js +900 -0
  64. package/dist/risk-scanner.d.ts +8 -0
  65. package/dist/risk-scanner.js +59 -0
  66. package/dist/server.d.ts +14 -0
  67. package/dist/server.js +234 -0
  68. package/dist/state.d.ts +69 -0
  69. package/dist/state.js +155 -0
  70. package/dist/sync.d.ts +74 -0
  71. package/dist/sync.js +679 -0
  72. package/package.json +66 -0
@@ -0,0 +1,38 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { cursorUserDir } from "./readers/cursor.js";
4
+ // Keys are tried in specificity order — Cursor-namespaced keys first, then
5
+ // generic fallbacks. "model" is last to avoid capturing unrelated workspace
6
+ // settings that happen to have a "model" key.
7
+ const SETTINGS_MODEL_KEYS = [
8
+ "cursor.aiModel",
9
+ "aiModel",
10
+ "cursor.general.preferredModel",
11
+ "model",
12
+ ];
13
+ /**
14
+ * Best-effort: read active model name from Cursor's settings.json.
15
+ * Returns null on any error (file absent, unreadable, no matching key).
16
+ * Never throws.
17
+ */
18
+ export function readCursorActiveModel(baseDir) {
19
+ const settingsPath = join(baseDir ?? cursorUserDir(), "settings.json");
20
+ try {
21
+ if (!existsSync(settingsPath))
22
+ return null;
23
+ const raw = readFileSync(settingsPath, "utf-8");
24
+ const parsed = JSON.parse(raw);
25
+ if (typeof parsed !== "object" || parsed === null)
26
+ return null;
27
+ const obj = parsed;
28
+ for (const key of SETTINGS_MODEL_KEYS) {
29
+ const val = obj[key];
30
+ if (typeof val === "string" && val.trim().length > 0)
31
+ return val.trim();
32
+ }
33
+ return null;
34
+ }
35
+ catch {
36
+ return null;
37
+ }
38
+ }
@@ -0,0 +1,48 @@
1
+ import { DailyStatsVersionDiscovery } from "./daily-stats-versions.js";
2
+ export type { DailyStatsVersionDiscovery } from "./daily-stats-versions.js";
3
+ export type PathCLegacyVerdict = "no_legacy_dbs" | "legacy_present_empty" | "legacy_has_rows";
4
+ export interface LegacyDbAuditEntry {
5
+ db_path_redacted: string;
6
+ file_bytes: number;
7
+ has_feedback_table: boolean;
8
+ feedback_row_count: number;
9
+ }
10
+ export interface StateVscdbAuditEntry {
11
+ db_path_redacted: string;
12
+ exists: boolean;
13
+ daily_stats_key_count: number;
14
+ has_recent_commit: boolean;
15
+ }
16
+ export interface CursorStoreAuditReport {
17
+ captured_at: string;
18
+ platform: NodeJS.Platform;
19
+ sqlite_probe_ok: boolean;
20
+ state_vscdb: {
21
+ total_paths: number;
22
+ global: StateVscdbAuditEntry;
23
+ workspace_scoped_count: number;
24
+ workspace_with_daily_stats: number;
25
+ workspace_with_recent_commit: number;
26
+ };
27
+ legacy_cursor_db: {
28
+ count: number;
29
+ with_feedback_table: number;
30
+ total_feedback_rows: number;
31
+ entries: LegacyDbAuditEntry[];
32
+ };
33
+ path_c_verdict: PathCLegacyVerdict;
34
+ /** Human-readable summary for CUR-V07 / DATA-CURRENT.md. */
35
+ ingest_note: string;
36
+ /** CUR-V11 — version prefixes under `aiCodeTracking.dailyStats.*` (install-wide). */
37
+ daily_stats_versions: DailyStatsVersionDiscovery;
38
+ /** Short note when v1.6+ or unparsed keys need cursor-6 work. */
39
+ daily_stats_version_note: string;
40
+ }
41
+ export declare function redactCursorPath(p: string): string;
42
+ export declare function auditStateVscdbFile(dbPath: string): StateVscdbAuditEntry;
43
+ export declare function auditLegacyCursorDbFile(dbPath: string): LegacyDbAuditEntry;
44
+ /**
45
+ * CUR-V07 — inventory local Cursor stores (state.vscdb vs legacy cursor.db).
46
+ * Does not read disk outside Cursor's User directory unless `baseDir` is passed (tests).
47
+ */
48
+ export declare function auditCursorLocalStores(baseDir?: string): CursorStoreAuditReport;
@@ -0,0 +1,155 @@
1
+ import { existsSync, statSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ import Database from "better-sqlite3";
5
+ import { discoverDailyStatsVersionsInDb, mergeDailyStatsVersionDiscoveries, } from "./daily-stats-versions.js";
6
+ import { cursorUserDir, findCursorDbs, findStateVscDbs, isGlobalStateDbPath, probeCursorGlobalStateDb, } from "./readers/cursor.js";
7
+ const LEGACY_TABLE = "CursorRequestFeedback";
8
+ const STATE_TABLE = "ItemTable";
9
+ const RECENT_COMMIT_KEY = "aiCodeTracking.recentCommit";
10
+ export function redactCursorPath(p) {
11
+ return p.replaceAll(homedir(), "~");
12
+ }
13
+ function tableExists(db, tableName) {
14
+ const row = db
15
+ .prepare("SELECT name FROM sqlite_master WHERE type='table' AND name=?")
16
+ .get(tableName);
17
+ return row !== undefined;
18
+ }
19
+ export function auditStateVscdbFile(dbPath) {
20
+ const entry = {
21
+ db_path_redacted: redactCursorPath(dbPath),
22
+ exists: existsSync(dbPath),
23
+ daily_stats_key_count: 0,
24
+ has_recent_commit: false,
25
+ };
26
+ if (!entry.exists)
27
+ return entry;
28
+ let db = null;
29
+ try {
30
+ db = new Database(dbPath, { readonly: true });
31
+ const ds = db
32
+ .prepare(`SELECT count(*) AS c FROM ${STATE_TABLE} WHERE key LIKE 'aiCodeTracking.dailyStats%'`)
33
+ .get();
34
+ const rc = db
35
+ .prepare(`SELECT 1 FROM ${STATE_TABLE} WHERE key = ? LIMIT 1`)
36
+ .get(RECENT_COMMIT_KEY);
37
+ entry.daily_stats_key_count = ds.c;
38
+ entry.has_recent_commit = rc !== undefined;
39
+ }
40
+ catch {
41
+ // Leave counts at zero — caller checks sqlite_probe_ok.
42
+ }
43
+ finally {
44
+ db?.close();
45
+ }
46
+ return entry;
47
+ }
48
+ export function auditLegacyCursorDbFile(dbPath) {
49
+ const entry = {
50
+ db_path_redacted: redactCursorPath(dbPath),
51
+ file_bytes: existsSync(dbPath) ? statSync(dbPath).size : 0,
52
+ has_feedback_table: false,
53
+ feedback_row_count: 0,
54
+ };
55
+ if (!existsSync(dbPath))
56
+ return entry;
57
+ let db = null;
58
+ try {
59
+ db = new Database(dbPath, { readonly: true });
60
+ if (!tableExists(db, LEGACY_TABLE))
61
+ return entry;
62
+ entry.has_feedback_table = true;
63
+ const row = db.prepare(`SELECT count(*) AS c FROM ${LEGACY_TABLE}`).get();
64
+ entry.feedback_row_count = row.c;
65
+ }
66
+ catch {
67
+ return entry;
68
+ }
69
+ finally {
70
+ db?.close();
71
+ }
72
+ return entry;
73
+ }
74
+ function dailyStatsVersionNote(discovery) {
75
+ if (discovery.buckets.length === 0) {
76
+ return "No aiCodeTracking.dailyStats keys found in any state.vscdb.";
77
+ }
78
+ const versions = discovery.buckets.map((b) => `${b.version} (${b.key_count})`).join(", ");
79
+ if (discovery.has_version_newer_than_v1_5) {
80
+ const extra = discovery.unmatched_keys.length > 0
81
+ ? ` Unparsed keys: ${discovery.unmatched_keys.slice(0, 3).join(", ")}${discovery.unmatched_keys.length > 3 ? "…" : ""}.`
82
+ : "";
83
+ return (`Found dailyStats version(s): ${versions}. Highest: ${discovery.highest_version}.` +
84
+ " v1.6+ or non-standard keys present — track as cursor-6 (schema discovery / mapper)." +
85
+ extra);
86
+ }
87
+ return (`Found dailyStats version(s): ${versions}. Highest: ${discovery.highest_version}.` +
88
+ " Reader accepts any v* dated key; no cursor-6 follow-up on this install.");
89
+ }
90
+ function pathCIngestNote(verdict, legacyCount, totalRows) {
91
+ switch (verdict) {
92
+ case "no_legacy_dbs":
93
+ return (`No workspaceStorage/**/cursor.db files found (${legacyCount} paths). ` +
94
+ "Path C (legacy per-request) contributes zero events on this install; rely on state.vscdb Paths A/B.");
95
+ case "legacy_present_empty":
96
+ return (`Found ${legacyCount} cursor.db file(s) but CursorRequestFeedback is empty or missing. ` +
97
+ "Path C wired in sync but produces no payloads until/unless Cursor writes legacy rows.");
98
+ case "legacy_has_rows":
99
+ return (`Found ${legacyCount} cursor.db file(s) with ${totalRows} CursorRequestFeedback row(s). ` +
100
+ "Path C can emit real token counts and model names when synced.");
101
+ }
102
+ }
103
+ /**
104
+ * CUR-V07 — inventory local Cursor stores (state.vscdb vs legacy cursor.db).
105
+ * Does not read disk outside Cursor's User directory unless `baseDir` is passed (tests).
106
+ */
107
+ export function auditCursorLocalStores(baseDir) {
108
+ const sqlite_probe_ok = probeCursorGlobalStateDb(false);
109
+ const statePaths = findStateVscDbs(baseDir);
110
+ const globalPath = statePaths.find((p) => isGlobalStateDbPath(p)) ??
111
+ join(baseDir ?? cursorUserDir(), "globalStorage", "state.vscdb");
112
+ const global = auditStateVscdbFile(globalPath);
113
+ const workspacePaths = statePaths.filter((p) => !isGlobalStateDbPath(p));
114
+ const workspaceAudits = workspacePaths.map(auditStateVscdbFile);
115
+ const versionDiscoveries = statePaths
116
+ .filter((p) => existsSync(p))
117
+ .map(discoverDailyStatsVersionsInDb);
118
+ const daily_stats_versions = mergeDailyStatsVersionDiscoveries(versionDiscoveries);
119
+ const legacyPaths = findCursorDbs(baseDir);
120
+ const legacyEntries = legacyPaths.map(auditLegacyCursorDbFile);
121
+ const withFeedbackTable = legacyEntries.filter((e) => e.has_feedback_table).length;
122
+ const totalFeedbackRows = legacyEntries.reduce((sum, e) => sum + e.feedback_row_count, 0);
123
+ let path_c_verdict;
124
+ if (legacyPaths.length === 0) {
125
+ path_c_verdict = "no_legacy_dbs";
126
+ }
127
+ else if (totalFeedbackRows === 0) {
128
+ path_c_verdict = "legacy_present_empty";
129
+ }
130
+ else {
131
+ path_c_verdict = "legacy_has_rows";
132
+ }
133
+ return {
134
+ captured_at: new Date().toISOString(),
135
+ platform: process.platform,
136
+ sqlite_probe_ok,
137
+ state_vscdb: {
138
+ total_paths: statePaths.length,
139
+ global,
140
+ workspace_scoped_count: workspacePaths.length,
141
+ workspace_with_daily_stats: workspaceAudits.filter((e) => e.daily_stats_key_count > 0).length,
142
+ workspace_with_recent_commit: workspaceAudits.filter((e) => e.has_recent_commit).length,
143
+ },
144
+ legacy_cursor_db: {
145
+ count: legacyPaths.length,
146
+ with_feedback_table: withFeedbackTable,
147
+ total_feedback_rows: totalFeedbackRows,
148
+ entries: legacyEntries,
149
+ },
150
+ path_c_verdict,
151
+ ingest_note: pathCIngestNote(path_c_verdict, legacyPaths.length, totalFeedbackRows),
152
+ daily_stats_versions,
153
+ daily_stats_version_note: dailyStatsVersionNote(daily_stats_versions),
154
+ };
155
+ }
@@ -0,0 +1,31 @@
1
+ /** Full key shape for a dated dailyStats row. */
2
+ export declare const DAILY_STATS_KEY_RE: RegExp;
3
+ export interface DailyStatsVersionBucket {
4
+ version: string;
5
+ key_count: number;
6
+ date_min: string | null;
7
+ date_max: string | null;
8
+ /** Up to three sample keys (for verification docs). */
9
+ sample_keys: string[];
10
+ }
11
+ export interface DailyStatsVersionDiscovery {
12
+ buckets: DailyStatsVersionBucket[];
13
+ /** Keys matching dailyStats% but not matching {@link DAILY_STATS_KEY_RE}. */
14
+ unmatched_keys: string[];
15
+ highest_version: string | null;
16
+ /** True when a version newer than v1.5 is present (cursor-6 follow-up). */
17
+ has_version_newer_than_v1_5: boolean;
18
+ }
19
+ export declare function parseDailyStatsKey(key: string): {
20
+ version: string;
21
+ date: string;
22
+ } | null;
23
+ export declare function parseVersionTag(tag: string): number[];
24
+ export declare function compareVersionTags(a: string, b: string): number;
25
+ export declare function isVersionNewerThanV1_5(version: string): boolean;
26
+ /**
27
+ * Read all `aiCodeTracking.dailyStats%` keys from one `state.vscdb` file.
28
+ */
29
+ export declare function discoverDailyStatsVersionsInDb(dbPath: string): DailyStatsVersionDiscovery;
30
+ /** Merge discoveries from global + workspace `state.vscdb` files (dedupe sample keys only). */
31
+ export declare function mergeDailyStatsVersionDiscoveries(discoveries: DailyStatsVersionDiscovery[]): DailyStatsVersionDiscovery;
@@ -0,0 +1,170 @@
1
+ /**
2
+ * CUR-V11 — discover `aiCodeTracking.dailyStats` version prefixes on disk.
3
+ * Keys look like: aiCodeTracking.dailyStats.v1.5.2026-05-20
4
+ */
5
+ import Database from "better-sqlite3";
6
+ const STATE_TABLE = "ItemTable";
7
+ const DAILY_STATS_LIKE = "aiCodeTracking.dailyStats%";
8
+ /** Full key shape for a dated dailyStats row. */
9
+ export const DAILY_STATS_KEY_RE = /^aiCodeTracking\.dailyStats\.(v[\d.]+)\.(\d{4}-\d{2}-\d{2})$/;
10
+ export function parseDailyStatsKey(key) {
11
+ const m = key.match(DAILY_STATS_KEY_RE);
12
+ if (!m)
13
+ return null;
14
+ return { version: m[1], date: m[2] };
15
+ }
16
+ export function parseVersionTag(tag) {
17
+ if (!tag.startsWith("v"))
18
+ return [];
19
+ return tag
20
+ .slice(1)
21
+ .split(".")
22
+ .map((part) => parseInt(part, 10))
23
+ .filter((n) => !Number.isNaN(n));
24
+ }
25
+ export function compareVersionTags(a, b) {
26
+ const pa = parseVersionTag(a);
27
+ const pb = parseVersionTag(b);
28
+ const len = Math.max(pa.length, pb.length);
29
+ for (let i = 0; i < len; i++) {
30
+ const diff = (pa[i] ?? 0) - (pb[i] ?? 0);
31
+ if (diff !== 0)
32
+ return diff;
33
+ }
34
+ return 0;
35
+ }
36
+ const V1_5 = "v1.5";
37
+ export function isVersionNewerThanV1_5(version) {
38
+ return compareVersionTags(version, V1_5) > 0;
39
+ }
40
+ function minDate(a, b) {
41
+ return a === null || b < a ? b : a;
42
+ }
43
+ function maxDate(a, b) {
44
+ return a === null || b > a ? b : a;
45
+ }
46
+ function mergeBuckets(target, discovery) {
47
+ for (const bucket of discovery.buckets) {
48
+ const existing = target.get(bucket.version);
49
+ if (!existing) {
50
+ target.set(bucket.version, { ...bucket, sample_keys: [...bucket.sample_keys] });
51
+ continue;
52
+ }
53
+ existing.key_count += bucket.key_count;
54
+ existing.date_min =
55
+ bucket.date_min === null
56
+ ? existing.date_min
57
+ : minDate(existing.date_min, bucket.date_min);
58
+ existing.date_max =
59
+ bucket.date_max === null
60
+ ? existing.date_max
61
+ : maxDate(existing.date_max, bucket.date_max);
62
+ for (const key of bucket.sample_keys) {
63
+ if (existing.sample_keys.length >= 3)
64
+ break;
65
+ if (!existing.sample_keys.includes(key))
66
+ existing.sample_keys.push(key);
67
+ }
68
+ }
69
+ }
70
+ /**
71
+ * Read all `aiCodeTracking.dailyStats%` keys from one `state.vscdb` file.
72
+ */
73
+ export function discoverDailyStatsVersionsInDb(dbPath) {
74
+ const byVersion = new Map();
75
+ const unmatched = [];
76
+ let db = null;
77
+ try {
78
+ db = new Database(dbPath, { readonly: true });
79
+ const table = db
80
+ .prepare("SELECT name FROM sqlite_master WHERE type='table' AND name=?")
81
+ .get(STATE_TABLE);
82
+ if (!table) {
83
+ return emptyDiscovery();
84
+ }
85
+ const rows = db
86
+ .prepare(`SELECT key FROM ${STATE_TABLE} WHERE key LIKE ?`)
87
+ .all(DAILY_STATS_LIKE);
88
+ for (const { key } of rows) {
89
+ const parsed = parseDailyStatsKey(key);
90
+ if (!parsed) {
91
+ unmatched.push(key);
92
+ continue;
93
+ }
94
+ const bucket = byVersion.get(parsed.version) ?? {
95
+ count: 0,
96
+ dateMin: null,
97
+ dateMax: null,
98
+ samples: [],
99
+ };
100
+ bucket.count += 1;
101
+ bucket.dateMin = minDate(bucket.dateMin, parsed.date);
102
+ bucket.dateMax = maxDate(bucket.dateMax, parsed.date);
103
+ if (bucket.samples.length < 3)
104
+ bucket.samples.push(key);
105
+ byVersion.set(parsed.version, bucket);
106
+ }
107
+ }
108
+ catch {
109
+ return emptyDiscovery();
110
+ }
111
+ finally {
112
+ db?.close();
113
+ }
114
+ return buildDiscovery(byVersion, unmatched);
115
+ }
116
+ function emptyDiscovery() {
117
+ return {
118
+ buckets: [],
119
+ unmatched_keys: [],
120
+ highest_version: null,
121
+ has_version_newer_than_v1_5: false,
122
+ };
123
+ }
124
+ function buildDiscovery(byVersion, unmatched) {
125
+ const buckets = [...byVersion.entries()]
126
+ .map(([version, b]) => ({
127
+ version,
128
+ key_count: b.count,
129
+ date_min: b.dateMin,
130
+ date_max: b.dateMax,
131
+ sample_keys: b.samples,
132
+ }))
133
+ .sort((a, b) => compareVersionTags(a.version, b.version));
134
+ let highest = null;
135
+ for (const b of buckets) {
136
+ if (highest === null || compareVersionTags(b.version, highest) > 0) {
137
+ highest = b.version;
138
+ }
139
+ }
140
+ const hasNewer = buckets.some((b) => isVersionNewerThanV1_5(b.version));
141
+ return {
142
+ buckets,
143
+ unmatched_keys: unmatched.sort(),
144
+ highest_version: highest,
145
+ has_version_newer_than_v1_5: hasNewer || unmatched.length > 0,
146
+ };
147
+ }
148
+ /** Merge discoveries from global + workspace `state.vscdb` files (dedupe sample keys only). */
149
+ export function mergeDailyStatsVersionDiscoveries(discoveries) {
150
+ const merged = new Map();
151
+ const unmatched = new Set();
152
+ for (const d of discoveries) {
153
+ mergeBuckets(merged, d);
154
+ for (const key of d.unmatched_keys)
155
+ unmatched.add(key);
156
+ }
157
+ const buckets = [...merged.values()].sort((a, b) => compareVersionTags(a.version, b.version));
158
+ let highest = null;
159
+ for (const b of buckets) {
160
+ if (highest === null || compareVersionTags(b.version, highest) > 0) {
161
+ highest = b.version;
162
+ }
163
+ }
164
+ return {
165
+ buckets,
166
+ unmatched_keys: [...unmatched].sort(),
167
+ highest_version: highest,
168
+ has_version_newer_than_v1_5: buckets.some((b) => isVersionNewerThanV1_5(b.version)) || unmatched.size > 0,
169
+ };
170
+ }
@@ -0,0 +1,31 @@
1
+ import type { TelemetryToolId } from "./auth/credentials.js";
2
+ import { type McpOperatorState } from "./state.js";
3
+ import { type SyncResult } from "./sync.js";
4
+ export interface HealthSnapshot {
5
+ authenticated: boolean;
6
+ configured: boolean;
7
+ host: string | null;
8
+ ingest_tools: TelemetryToolId[];
9
+ app_dir: string;
10
+ log_path: string;
11
+ state_file_paths: string[];
12
+ state_tracked_sessions: number;
13
+ /** Whether the Cursor hooks forwarder is installed in ~/.cursor/hooks.json. */
14
+ hooks_installed: boolean;
15
+ /** Number of unprocessed events waiting in the hooks queue file. */
16
+ hooks_queue_depth: number;
17
+ /** Best-effort merge of credential-scoped `mcp_operator` (latest `last_sync_at`). */
18
+ persisted: McpOperatorState | null;
19
+ /** In-process telemetry (same fields as `getSyncTelemetry`). */
20
+ process: {
21
+ last_sync_at: string | null;
22
+ last_result: SyncResult | null;
23
+ recent_errors: string[];
24
+ };
25
+ }
26
+ /** Pick the freshest persisted operator block by `last_sync_at` lexicographic (ISO-safe). */
27
+ export declare function mergePersistedOperators(snapshots: McpOperatorState[]): McpOperatorState | null;
28
+ export declare function buildHealthSnapshot(): Promise<HealthSnapshot>;
29
+ /** MCP `db90_status` JSON — single source shared with CLI health. */
30
+ export declare function healthSnapshotToStatusPayload(snapshot: HealthSnapshot): Record<string, unknown>;
31
+ export declare function formatHealthForCli(snapshot: HealthSnapshot): string;
package/dist/health.js ADDED
@@ -0,0 +1,195 @@
1
+ import { loadCredentials, credentialsHaveAnyToken } from "./credentials.js";
2
+ import { readState, getAppDir, credentialStateFilePath, } from "./state.js";
3
+ import { getSyncTelemetry } from "./sync.js";
4
+ import { getMcpLogPath } from "./log.js";
5
+ import { verifyHooksConfig } from "./hooks/hooks-config.js";
6
+ function snapshotToResult(s) {
7
+ if (!s)
8
+ return null;
9
+ return {
10
+ sent: s.sent,
11
+ failed: s.failed,
12
+ skipped: s.skipped,
13
+ locked: s.locked,
14
+ errors: s.errors,
15
+ rateLimitedUntil: s.rate_limited_until ?? undefined,
16
+ };
17
+ }
18
+ function mergeErrors(a, b) {
19
+ const seen = new Set();
20
+ const out = [];
21
+ for (const x of [...a, ...b]) {
22
+ if (seen.has(x))
23
+ continue;
24
+ seen.add(x);
25
+ out.push(x);
26
+ if (out.length >= 20)
27
+ break;
28
+ }
29
+ return out;
30
+ }
31
+ /** Pick the freshest persisted operator block by `last_sync_at` lexicographic (ISO-safe). */
32
+ export function mergePersistedOperators(snapshots) {
33
+ if (snapshots.length === 0)
34
+ return null;
35
+ let best = snapshots[0];
36
+ for (const s of snapshots.slice(1)) {
37
+ const a = best.last_sync_at ?? "";
38
+ const b = s.last_sync_at ?? "";
39
+ if (b > a)
40
+ best = s;
41
+ }
42
+ return best;
43
+ }
44
+ function collectPersistedAndPaths(appDir, host, creds) {
45
+ const seenTok = new Set();
46
+ const blocks = [];
47
+ const paths = [];
48
+ let tracked = 0;
49
+ for (const tok of Object.values(creds.accounts)) {
50
+ if (typeof tok !== "string" || tok.length === 0 || seenTok.has(tok))
51
+ continue;
52
+ seenTok.add(tok);
53
+ paths.push(credentialStateFilePath(appDir, host, tok));
54
+ const st = readState(appDir, host, tok);
55
+ tracked += Object.keys(st.sessions).length;
56
+ if (st.mcp_operator)
57
+ blocks.push(st.mcp_operator);
58
+ }
59
+ return { paths, merged: mergePersistedOperators(blocks), tracked };
60
+ }
61
+ export async function buildHealthSnapshot() {
62
+ const appDir = getAppDir();
63
+ const logPath = getMcpLogPath(appDir);
64
+ const telemetry = getSyncTelemetry();
65
+ const hooksReport = verifyHooksConfig(appDir);
66
+ try {
67
+ const creds = await loadCredentials();
68
+ if (!creds || !credentialsHaveAnyToken(creds)) {
69
+ return {
70
+ authenticated: false,
71
+ configured: false,
72
+ host: null,
73
+ ingest_tools: [],
74
+ app_dir: appDir,
75
+ log_path: logPath,
76
+ state_file_paths: [],
77
+ state_tracked_sessions: 0,
78
+ hooks_installed: hooksReport.hooks_json_installed,
79
+ hooks_queue_depth: hooksReport.queue_depth,
80
+ persisted: null,
81
+ process: {
82
+ last_sync_at: telemetry.lastSyncAt,
83
+ last_result: telemetry.lastResult,
84
+ recent_errors: telemetry.recentErrors,
85
+ },
86
+ };
87
+ }
88
+ const ingest_tools = Object.entries(creds.accounts)
89
+ .filter(([, tok]) => typeof tok === "string" && tok.length > 0)
90
+ .map(([k]) => k)
91
+ .sort();
92
+ const { paths, merged, tracked } = collectPersistedAndPaths(appDir, creds.host, creds);
93
+ return {
94
+ authenticated: true,
95
+ configured: true,
96
+ host: creds.host,
97
+ ingest_tools,
98
+ app_dir: appDir,
99
+ log_path: logPath,
100
+ state_file_paths: paths.sort(),
101
+ state_tracked_sessions: tracked,
102
+ hooks_installed: hooksReport.hooks_json_installed,
103
+ hooks_queue_depth: hooksReport.queue_depth,
104
+ persisted: merged,
105
+ process: {
106
+ last_sync_at: telemetry.lastSyncAt,
107
+ last_result: telemetry.lastResult,
108
+ recent_errors: telemetry.recentErrors,
109
+ },
110
+ };
111
+ }
112
+ catch (err) {
113
+ return {
114
+ authenticated: false,
115
+ configured: false,
116
+ host: null,
117
+ ingest_tools: [],
118
+ app_dir: appDir,
119
+ log_path: logPath,
120
+ state_file_paths: [],
121
+ state_tracked_sessions: 0,
122
+ hooks_installed: hooksReport.hooks_json_installed,
123
+ hooks_queue_depth: hooksReport.queue_depth,
124
+ persisted: null,
125
+ process: {
126
+ last_sync_at: telemetry.lastSyncAt,
127
+ last_result: telemetry.lastResult,
128
+ recent_errors: [
129
+ ...telemetry.recentErrors,
130
+ err instanceof Error ? err.message : String(err),
131
+ ].slice(-20),
132
+ },
133
+ };
134
+ }
135
+ }
136
+ /** MCP `db90_status` JSON — single source shared with CLI health. */
137
+ export function healthSnapshotToStatusPayload(snapshot) {
138
+ const proc = snapshot.process;
139
+ const pers = snapshot.persisted;
140
+ const lastSyncAt = proc.last_sync_at ?? pers?.last_sync_at ?? null;
141
+ const lastResult = proc.last_result ?? snapshotToResult(pers?.last_result ?? null);
142
+ const errors = mergeErrors(proc.recent_errors, pers?.recent_errors ?? []);
143
+ return {
144
+ authenticated: snapshot.authenticated,
145
+ configured: snapshot.configured,
146
+ host: snapshot.host,
147
+ ingest_tools: snapshot.ingest_tools,
148
+ last_sync_at: lastSyncAt,
149
+ last_result: lastResult,
150
+ sessions_synced: lastResult?.sent ?? 0,
151
+ skipped: lastResult?.skipped ?? 0,
152
+ state_tracked_sessions: snapshot.state_tracked_sessions,
153
+ errors,
154
+ app_dir: snapshot.app_dir,
155
+ log_path: snapshot.log_path,
156
+ state_file_paths: snapshot.state_file_paths,
157
+ persisted_operator: pers,
158
+ };
159
+ }
160
+ export function formatHealthForCli(snapshot) {
161
+ const lines = ["aixle-insights health diagnostic", ""];
162
+ lines.push(`app_dir: ${snapshot.app_dir}`);
163
+ lines.push(`log_path: ${snapshot.log_path}`);
164
+ lines.push(`authenticated: ${snapshot.authenticated}`);
165
+ lines.push(`configured: ${snapshot.configured}`);
166
+ lines.push(`host: ${snapshot.host ?? "(none)"}`);
167
+ lines.push(`ingest_tools: ${snapshot.ingest_tools.length ? snapshot.ingest_tools.join(", ") : "(none)"}`);
168
+ lines.push(`state_file_paths:`);
169
+ if (snapshot.state_file_paths.length === 0) {
170
+ lines.push(" (none — no credential-scoped state files yet)");
171
+ }
172
+ else {
173
+ for (const p of snapshot.state_file_paths) {
174
+ lines.push(` - ${p}`);
175
+ }
176
+ }
177
+ const sta = healthSnapshotToStatusPayload(snapshot);
178
+ lines.push("");
179
+ lines.push(`last_sync_at: ${String(sta["last_sync_at"] ?? "null")}`);
180
+ lines.push(`last_result: ${JSON.stringify(sta["last_result"] ?? null)}`);
181
+ lines.push(`state_tracked_sessions: ${String(snapshot.state_tracked_sessions)}`);
182
+ lines.push(`persisted_operator: ${JSON.stringify(snapshot.persisted)}`);
183
+ const err = sta["errors"];
184
+ lines.push("");
185
+ lines.push(`recent_errors (${Array.isArray(err) ? err.length : 0}):`);
186
+ if (Array.isArray(err) && err.length > 0) {
187
+ for (const e of err) {
188
+ lines.push(` - ${e}`);
189
+ }
190
+ }
191
+ else {
192
+ lines.push(" (none)");
193
+ }
194
+ return lines.join("\n");
195
+ }