@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,22 @@
1
+ import type { HookLogEvent } from "./hooks-config.js";
2
+ import type { CursorDb90Payload } from "../readers/cursor.js";
3
+ export { HookLogEvent };
4
+ /** State key prefix for hook event dedup in state.sessions. */
5
+ export declare const CURSOR_HOOK_STATE_PREFIX: "cursor:hook:";
6
+ /**
7
+ * Returns the dedup key used both in state.sessions and metadata.session_id
8
+ * so MCP state wipe doesn't cause duplicates (server-side upsert on session_id).
9
+ */
10
+ export declare function hookDedupeKey(event: HookLogEvent): string;
11
+ /**
12
+ * Returns false for events that carry no attribution value or can't be deduped.
13
+ * Logs a reason at debug level so ops can diagnose unexpected drops.
14
+ */
15
+ export declare function shouldIngestHookEvent(event: HookLogEvent, verbose?: boolean): boolean;
16
+ /** Warn-only version gate — Cursor builds can have unusual version strings. */
17
+ export declare function warnOnCursorVersion(event: HookLogEvent, verbose: boolean): void;
18
+ /**
19
+ * Map a validated hook event to the CursorDb90Payload contract.
20
+ * Call shouldIngestHookEvent() before this — it does not re-validate.
21
+ */
22
+ export declare function mapHookEventToPayload(event: HookLogEvent, projectId?: string | null): CursorDb90Payload;
@@ -0,0 +1,84 @@
1
+ import { HOOK_COST_MODEL } from "../readers/cursor.js";
2
+ import { mcpLog } from "../log.js";
3
+ /** State key prefix for hook event dedup in state.sessions. */
4
+ export const CURSOR_HOOK_STATE_PREFIX = "cursor:hook:";
5
+ const MIN_CURSOR_VERSION_MAJOR = 1;
6
+ const MIN_CURSOR_VERSION_MINOR = 7;
7
+ /**
8
+ * Returns the dedup key used both in state.sessions and metadata.session_id
9
+ * so MCP state wipe doesn't cause duplicates (server-side upsert on session_id).
10
+ */
11
+ export function hookDedupeKey(event) {
12
+ return `${CURSOR_HOOK_STATE_PREFIX}${event.conversation_id ?? ""}:${event.generation_id ?? ""}:${event.hook_event_name ?? ""}`;
13
+ }
14
+ /**
15
+ * Returns false for events that carry no attribution value or can't be deduped.
16
+ * Logs a reason at debug level so ops can diagnose unexpected drops.
17
+ */
18
+ export function shouldIngestHookEvent(event, verbose = false) {
19
+ if (event.hook_event_name === "parse_error" ||
20
+ event.hook_event_name === "log_parse_error") {
21
+ mcpLog.warn("hook_event_dropped", { reason: "parse_error", event: event.hook_event_name }, verbose);
22
+ return false;
23
+ }
24
+ if (!event.conversation_id || !event.hook_event_name) {
25
+ mcpLog.warn("hook_event_dropped", { reason: "missing_dedup_fields" }, verbose);
26
+ return false;
27
+ }
28
+ if (!event.model || event.model === "unknown") {
29
+ mcpLog.info("hook_event_dropped", { reason: "no_model_attribution", hook: event.hook_event_name }, verbose);
30
+ return false;
31
+ }
32
+ return true;
33
+ }
34
+ /** Warn-only version gate — Cursor builds can have unusual version strings. */
35
+ export function warnOnCursorVersion(event, verbose) {
36
+ const v = event.cursor_version;
37
+ if (typeof v !== "string")
38
+ return;
39
+ const parts = v.split(".");
40
+ const major = parseInt(parts[0] ?? "0", 10);
41
+ const minor = parseInt(parts[1] ?? "0", 10);
42
+ if (major < MIN_CURSOR_VERSION_MAJOR || (major === MIN_CURSOR_VERSION_MAJOR && minor < MIN_CURSOR_VERSION_MINOR)) {
43
+ mcpLog.warn("hook_cursor_version_old", { cursor_version: v, required: `${MIN_CURSOR_VERSION_MAJOR}.${MIN_CURSOR_VERSION_MINOR}` }, true);
44
+ if (verbose) {
45
+ console.warn(`[aixle-insights][cursor-hooks] cursor_version ${v} is older than ${MIN_CURSOR_VERSION_MAJOR}.${MIN_CURSOR_VERSION_MINOR} — hook payload shape may differ`);
46
+ }
47
+ }
48
+ }
49
+ /**
50
+ * Map a validated hook event to the CursorDb90Payload contract.
51
+ * Call shouldIngestHookEvent() before this — it does not re-validate.
52
+ */
53
+ export function mapHookEventToPayload(event, projectId) {
54
+ const workspace = Array.isArray(event.workspace_roots) && typeof event.workspace_roots[0] === "string"
55
+ ? event.workspace_roots[0]
56
+ : "unknown";
57
+ const dedupeKey = hookDedupeKey(event);
58
+ const payload = {
59
+ tool_name: "cursor",
60
+ event_type: "chat",
61
+ model: event.model,
62
+ tokens_in: 0,
63
+ tokens_out: 0,
64
+ cost_usd: 0,
65
+ occurred_at: event.captured_at ?? new Date().toISOString(),
66
+ metadata: {
67
+ cursor_session_id: event.conversation_id ?? null,
68
+ workspace,
69
+ workspace_scope: "global",
70
+ cost_model: HOOK_COST_MODEL,
71
+ scannable: false,
72
+ risk_level: "none",
73
+ ingest_source: "cursor_hook",
74
+ hook_event_name: event.hook_event_name,
75
+ generation_id: event.generation_id,
76
+ hook_tool_name: event.tool_name,
77
+ duration_ms: typeof event.duration_ms === "number" ? event.duration_ms : undefined,
78
+ session_id: dedupeKey,
79
+ },
80
+ };
81
+ if (projectId)
82
+ payload.project_id = projectId;
83
+ return payload;
84
+ }
@@ -0,0 +1,30 @@
1
+ import type { State } from "../state.js";
2
+ import { CURSOR_HOOK_STATE_PREFIX } from "./cursor-hooks-mapper.js";
3
+ export { CURSOR_HOOK_STATE_PREFIX };
4
+ export interface ProcessHooksQueueParams {
5
+ queuePath: string;
6
+ /** Only process events whose workspace_roots[0] is under this directory. Omit to process all. */
7
+ scopeDir?: string;
8
+ state: State;
9
+ host: string;
10
+ token: string;
11
+ /** Called when a 429 is received. */
12
+ on429: (retryAfter: number, quotaExceeded: boolean) => void;
13
+ /** If true, skip events already in state.sessions. */
14
+ skipSeen?: boolean;
15
+ resolveProjectId?: (workspace: string) => Promise<string | null>;
16
+ verbose?: boolean;
17
+ }
18
+ export interface ProcessHooksResult {
19
+ sent: number;
20
+ failed: number;
21
+ skipped: number;
22
+ state: State;
23
+ }
24
+ /**
25
+ * Read the hooks queue, POST new events, and atomically rewrite the queue
26
+ * keeping only lines that failed to send (for retry on next cycle).
27
+ *
28
+ * Partial failure: lines that succeeded are removed; lines that failed stay.
29
+ */
30
+ export declare function processHooksQueue(params: ProcessHooksQueueParams): Promise<ProcessHooksResult>;
@@ -0,0 +1,117 @@
1
+ import { existsSync, readFileSync, renameSync, writeFileSync } from "node:fs";
2
+ import { homedir, tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { randomBytes } from "node:crypto";
5
+ import { markSessionSent } from "../state.js";
6
+ import { postEvent } from "../client.js";
7
+ import { mcpLog } from "../log.js";
8
+ import { shouldIngestHookEvent, mapHookEventToPayload, hookDedupeKey, warnOnCursorVersion, CURSOR_HOOK_STATE_PREFIX, } from "./cursor-hooks-mapper.js";
9
+ export { CURSOR_HOOK_STATE_PREFIX };
10
+ function isUnderScopeDir(workspace, scopeDir) {
11
+ return workspace === scopeDir || workspace.startsWith(scopeDir + "/");
12
+ }
13
+ /**
14
+ * The forwarder redacts the home directory to "~" in workspace_roots for
15
+ * privacy in the on-disk queue. Expand it back to an absolute path here so
16
+ * scopeDir comparison and git-remote project resolution operate on a real
17
+ * filesystem path. The payload's metadata.workspace stays redacted (the mapper
18
+ * reads event.workspace_roots directly).
19
+ */
20
+ function expandHome(p) {
21
+ if (p === "~")
22
+ return homedir();
23
+ if (p.startsWith("~/"))
24
+ return join(homedir(), p.slice(2));
25
+ return p;
26
+ }
27
+ function workspaceFromEvent(event) {
28
+ if (Array.isArray(event.workspace_roots) && typeof event.workspace_roots[0] === "string") {
29
+ return expandHome(event.workspace_roots[0]);
30
+ }
31
+ return "";
32
+ }
33
+ /**
34
+ * Rewrite the queue file keeping only the lines at the given indices.
35
+ * Uses atomic rename to avoid corruption if the process is interrupted.
36
+ */
37
+ function rewriteQueueKeepingLines(queuePath, allLines, keepIndices) {
38
+ const remaining = allLines.filter((_, i) => keepIndices.has(i));
39
+ const tmp = join(tmpdir(), `db90-hooks-queue-${randomBytes(6).toString("hex")}.ndjson`);
40
+ writeFileSync(tmp, remaining.length > 0 ? remaining.join("\n") + "\n" : "", "utf-8");
41
+ renameSync(tmp, queuePath);
42
+ }
43
+ /**
44
+ * Read the hooks queue, POST new events, and atomically rewrite the queue
45
+ * keeping only lines that failed to send (for retry on next cycle).
46
+ *
47
+ * Partial failure: lines that succeeded are removed; lines that failed stay.
48
+ */
49
+ export async function processHooksQueue(params) {
50
+ const { queuePath, scopeDir, host, token, on429, skipSeen = true, resolveProjectId, verbose = false, } = params;
51
+ if (!existsSync(queuePath)) {
52
+ return { sent: 0, failed: 0, skipped: 0, state: params.state };
53
+ }
54
+ // Single read — avoids a race window where the forwarder appends between two
55
+ // separate readFileSync calls, which would cause newly-appended events to be
56
+ // silently dropped during the queue rewrite.
57
+ const rawContent = readFileSync(queuePath, "utf-8");
58
+ const rawLines = rawContent.split("\n").filter((l) => l.trim().length > 0);
59
+ const allEvents = rawLines.map((line) => {
60
+ try {
61
+ return JSON.parse(line);
62
+ }
63
+ catch {
64
+ return { hook_event_name: "log_parse_error" };
65
+ }
66
+ });
67
+ if (allEvents.length === 0) {
68
+ return { sent: 0, failed: 0, skipped: 0, state: params.state };
69
+ }
70
+ let stateMut = params.state;
71
+ let totalSent = 0;
72
+ let totalFailed = 0;
73
+ let totalSkipped = 0;
74
+ const failedIndices = new Set();
75
+ for (let i = 0; i < allEvents.length; i++) {
76
+ const event = allEvents[i];
77
+ if (!shouldIngestHookEvent(event, verbose)) {
78
+ totalSkipped++;
79
+ continue;
80
+ }
81
+ warnOnCursorVersion(event, verbose);
82
+ const workspace = workspaceFromEvent(event);
83
+ if (scopeDir && workspace && !isUnderScopeDir(workspace, scopeDir)) {
84
+ if (verbose) {
85
+ console.log(`[verbose][cursor-hooks] Skipping event — workspace=${workspace} not under scopeDir=${scopeDir}`);
86
+ }
87
+ totalSkipped++;
88
+ continue;
89
+ }
90
+ const dedupeKey = hookDedupeKey(event);
91
+ if (skipSeen && stateMut.sessions[dedupeKey]) {
92
+ totalSkipped++;
93
+ continue;
94
+ }
95
+ let projectId;
96
+ if (resolveProjectId && workspace) {
97
+ projectId = await resolveProjectId(workspace);
98
+ }
99
+ const payload = mapHookEventToPayload(event, projectId);
100
+ const ok = await postEvent(payload, host, token, { on429 });
101
+ if (ok) {
102
+ totalSent++;
103
+ stateMut = markSessionSent(stateMut, dedupeKey, 0);
104
+ }
105
+ else {
106
+ totalFailed++;
107
+ failedIndices.add(i);
108
+ mcpLog.error("sync_ingest_final_failure", { tool: "cursor", group: "cursor_hook", occurred_at: payload.occurred_at }, true);
109
+ }
110
+ }
111
+ // Rewrite queue: keep only lines that failed to send (retry next cycle)
112
+ if (allEvents.length > 0) {
113
+ rewriteQueueKeepingLines(queuePath, rawLines, failedIndices);
114
+ }
115
+ mcpLog.info("cursor_hook_sync_complete", { sent: totalSent, failed: totalFailed, skipped: totalSkipped }, verbose);
116
+ return { sent: totalSent, failed: totalFailed, skipped: totalSkipped, state: stateMut };
117
+ }
@@ -0,0 +1,110 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Append redacted Cursor hook stdin to the aixle-insights hooks queue.
4
+ * Cursor invokes this via ~/.cursor/hooks.json on sessionEnd / postToolUse.
5
+ * No credentials here — aixle-insights reads the queue and POSTs on the next sync cycle.
6
+ */
7
+ import { appendFileSync, mkdirSync } from "node:fs";
8
+ import { homedir } from "node:os";
9
+ import { dirname, join } from "node:path";
10
+
11
+ const DEFAULT_APP_DIR = join(homedir(), ".aixle-insights");
12
+
13
+ /**
14
+ * Resolve the app dir. Cursor's hooks.json has no `env` support, so the
15
+ * installer passes `--app-dir <path>` in the command string. Precedence:
16
+ * CLI arg → AIXLE_INSIGHTS_HOME env → default (~/.aixle-insights).
17
+ */
18
+ function getAppDir() {
19
+ const argv = process.argv.slice(2);
20
+ const flagIdx = argv.indexOf("--app-dir");
21
+ if (flagIdx !== -1 && typeof argv[flagIdx + 1] === "string" && argv[flagIdx + 1].trim()) {
22
+ return argv[flagIdx + 1].trim();
23
+ }
24
+ return process.env.AIXLE_INSIGHTS_HOME?.trim() || DEFAULT_APP_DIR;
25
+ }
26
+
27
+ function getQueuePath() {
28
+ return join(getAppDir(), "hooks-queue.ndjson");
29
+ }
30
+
31
+ function redactPath(p) {
32
+ if (typeof p !== "string") return p;
33
+ return p.replaceAll(homedir(), "~");
34
+ }
35
+
36
+ function redactValue(key, value) {
37
+ if (value === null || value === undefined) return value;
38
+
39
+ if (key === "user_email") return "[redacted]";
40
+ if (key === "transcript_path") {
41
+ return typeof value === "string" ? "[redacted]" : value;
42
+ }
43
+ if (key === "workspace_roots" && Array.isArray(value)) {
44
+ return value.map((r) => redactPath(String(r)));
45
+ }
46
+ if (
47
+ key === "tool_input" ||
48
+ key === "tool_output" ||
49
+ key === "text" ||
50
+ key === "agent_message" ||
51
+ key === "error_message" ||
52
+ key === "command"
53
+ ) {
54
+ const s = typeof value === "string" ? value : JSON.stringify(value);
55
+ return `[redacted, ${s.length} chars]`;
56
+ }
57
+ if (typeof value === "object" && !Array.isArray(value)) {
58
+ const out = {};
59
+ for (const [k, v] of Object.entries(value)) {
60
+ out[k] = redactValue(k, v);
61
+ }
62
+ return out;
63
+ }
64
+ return value;
65
+ }
66
+
67
+ function redactHookPayload(raw) {
68
+ const out = {};
69
+ for (const [key, value] of Object.entries(raw)) {
70
+ out[key] = redactValue(key, value);
71
+ }
72
+ return out;
73
+ }
74
+
75
+ async function readStdin() {
76
+ const chunks = [];
77
+ for await (const chunk of process.stdin) {
78
+ chunks.push(chunk);
79
+ }
80
+ return Buffer.concat(chunks).toString("utf-8");
81
+ }
82
+
83
+ async function main() {
84
+ const input = await readStdin();
85
+ const queuePath = getQueuePath();
86
+
87
+ let payload;
88
+ try {
89
+ payload = JSON.parse(input || "{}");
90
+ } catch {
91
+ payload = { hook_event_name: "parse_error", raw_length: input.length };
92
+ }
93
+
94
+ const entry = {
95
+ captured_at: new Date().toISOString(),
96
+ ...redactHookPayload(payload),
97
+ };
98
+
99
+ mkdirSync(dirname(queuePath), { recursive: true });
100
+ appendFileSync(queuePath, `${JSON.stringify(entry)}\n`, "utf-8");
101
+
102
+ // Cursor requires an empty JSON object response from hook scripts.
103
+ process.stdout.write("{}");
104
+ }
105
+
106
+ main().catch((err) => {
107
+ console.error(`[db90-hook-forwarder] ${err instanceof Error ? err.message : String(err)}`);
108
+ process.stdout.write("{}");
109
+ process.exit(0);
110
+ });
@@ -0,0 +1,92 @@
1
+ export declare const USER_HOOKS_JSON: string;
2
+ export declare const HOOKS_BACKUP_SUFFIX = ".db90-backup";
3
+ export declare const FORWARDER_FILENAME = "hook-forwarder.mjs";
4
+ export declare const REQUIRED_HOOK_FIELDS: readonly ["conversation_id", "model", "workspace_roots"];
5
+ export declare const P0_HOOK_EVENTS: readonly ["sessionEnd", "postToolUse"];
6
+ export type RequiredHookField = (typeof REQUIRED_HOOK_FIELDS)[number];
7
+ export interface HooksJsonEntry {
8
+ /**
9
+ * Cursor executes this as a single shell command string. Cursor's hooks.json
10
+ * schema does NOT support `args` or `env` — everything (interpreter, script
11
+ * path, flags) must live in `command`. We keep `args`/`env` here only so the
12
+ * parser tolerates legacy/foreign configs; the installer never emits them.
13
+ */
14
+ command?: string;
15
+ args?: string[];
16
+ env?: Record<string, string>;
17
+ }
18
+ export interface HooksJsonConfig {
19
+ version?: number;
20
+ hooks?: Record<string, Array<HooksJsonEntry>>;
21
+ }
22
+ export interface HookLogEvent {
23
+ captured_at?: string;
24
+ hook_event_name?: string;
25
+ conversation_id?: string;
26
+ generation_id?: string;
27
+ model?: string;
28
+ workspace_roots?: unknown;
29
+ tool_name?: string;
30
+ duration_ms?: unknown;
31
+ cursor_version?: string;
32
+ [key: string]: unknown;
33
+ }
34
+ export interface HookEventFieldCheck {
35
+ field: RequiredHookField;
36
+ present: boolean;
37
+ populated: boolean;
38
+ note?: string;
39
+ }
40
+ export interface HookEventAnalysis {
41
+ captured_at: string | null;
42
+ hook_event_name: string | null;
43
+ field_checks: HookEventFieldCheck[];
44
+ passes_required_fields: boolean;
45
+ }
46
+ export interface HookFeasibilityReport {
47
+ captured_at: string;
48
+ platform: NodeJS.Platform;
49
+ hooks_json_installed: boolean;
50
+ backup_exists: boolean;
51
+ queue_path_redacted: string;
52
+ queue_depth: number;
53
+ required_fields_verified: boolean;
54
+ sample_events: HookEventAnalysis[];
55
+ next_steps: string[];
56
+ }
57
+ export declare function redactHomePath(p: string): string;
58
+ export declare function parseHooksJson(raw: string): HooksJsonConfig | null;
59
+ export declare function hooksConfigUsesForwarder(config: HooksJsonConfig, forwarderPath: string): boolean;
60
+ /**
61
+ * Build the single-string `command` Cursor runs for each hook. Cursor's schema
62
+ * has no `args`/`env`, so the interpreter, script path, and appDir flag are all
63
+ * encoded inline. The forwarder reads `--app-dir` to resolve the queue path,
64
+ * since it runs as a Cursor subprocess outside the MCP process.
65
+ */
66
+ export declare function buildForwarderCommand(forwarderPath: string, appDir: string): string;
67
+ /**
68
+ * Build the hooks.json config that routes P0 events to the forwarder.
69
+ */
70
+ export declare function buildUserHooksConfig(forwarderPath: string, appDir: string): HooksJsonConfig;
71
+ /**
72
+ * Install the db90 hook-forwarder:
73
+ * 1. Copy hook-forwarder.mjs from srcForwarderPath → {appDir}/hook-forwarder.mjs
74
+ * 2. Backup existing ~/.cursor/hooks.json if present
75
+ * 3. Write new hooks.json pointing at the installed forwarder
76
+ */
77
+ export declare function installHooksConfig(srcForwarderPath: string, appDir: string): {
78
+ forwarderInstalled: string;
79
+ backupPath: string | null;
80
+ };
81
+ /**
82
+ * Restore the backed-up hooks.json (or remove the current one if no backup existed).
83
+ * Warns (does not throw) if a queue file with unprocessed events exists.
84
+ */
85
+ export declare function uninstallHooksConfig(appDir: string, queuePath?: string): {
86
+ restored: boolean;
87
+ backupPath: string | null;
88
+ queueWarning: string | null;
89
+ };
90
+ export declare function readHooksQueue(queuePath: string): HookLogEvent[];
91
+ export declare function analyzeHookEvent(event: HookLogEvent): HookEventAnalysis;
92
+ export declare function verifyHooksConfig(appDir: string): HookFeasibilityReport;