grov 0.5.11 → 0.6.13
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/dist/cli/agents/registry.d.ts +17 -0
- package/dist/cli/agents/registry.js +132 -0
- package/dist/cli/commands/agents.d.ts +1 -0
- package/dist/cli/commands/agents.js +48 -0
- package/dist/cli/commands/disable.d.ts +1 -0
- package/dist/cli/commands/disable.js +179 -0
- package/dist/cli/commands/doctor.d.ts +1 -0
- package/dist/cli/commands/doctor.js +157 -0
- package/dist/{commands → cli/commands}/drift-test.js +39 -26
- package/dist/cli/commands/init.d.ts +1 -0
- package/dist/cli/commands/init.js +90 -0
- package/dist/{commands → cli/commands}/login.js +19 -18
- package/dist/{commands → cli/commands}/logout.js +1 -1
- package/dist/{commands → cli/commands}/proxy-status.js +1 -1
- package/dist/cli/commands/setup.d.ts +6 -0
- package/dist/cli/commands/setup.js +309 -0
- package/dist/{commands → cli/commands}/status.js +1 -1
- package/dist/{commands → cli/commands}/sync.d.ts +1 -0
- package/dist/{commands → cli/commands}/sync.js +59 -4
- package/dist/{commands → cli/commands}/uninstall.js +2 -2
- package/dist/cli/index.js +270 -0
- package/dist/{lib → core/cloud}/cloud-sync.d.ts +3 -3
- package/dist/{lib → core/cloud}/cloud-sync.js +10 -10
- package/dist/{lib → core/extraction}/correction-builder-proxy.d.ts +1 -1
- package/dist/{lib → core/extraction}/correction-builder-proxy.js +0 -4
- package/dist/{lib → core/extraction}/drift-checker-proxy.d.ts +13 -9
- package/dist/core/extraction/drift-checker-proxy.js +510 -0
- package/dist/{lib → core/extraction}/llm-extractor.d.ts +8 -38
- package/dist/{lib → core/extraction}/llm-extractor.js +132 -220
- package/dist/{lib → core}/store/sessions.js +3 -19
- package/dist/core/store/store.d.ts +1 -0
- package/dist/{lib → core/store}/store.js +1 -1
- package/dist/{lib → core}/store/types.d.ts +0 -4
- package/dist/integrations/mcp/cache.d.ts +27 -0
- package/dist/integrations/mcp/cache.js +106 -0
- package/dist/integrations/mcp/capture/antigravity-parser.d.ts +26 -0
- package/dist/integrations/mcp/capture/antigravity-parser.js +272 -0
- package/dist/integrations/mcp/capture/antigravity-scanner.d.ts +24 -0
- package/dist/integrations/mcp/capture/antigravity-scanner.js +153 -0
- package/dist/integrations/mcp/capture/antigravity-sync-tracker.d.ts +29 -0
- package/dist/integrations/mcp/capture/antigravity-sync-tracker.js +115 -0
- package/dist/integrations/mcp/capture/cli-extractor.d.ts +18 -0
- package/dist/integrations/mcp/capture/cli-extractor.js +258 -0
- package/dist/integrations/mcp/capture/cli-synced.d.ts +4 -0
- package/dist/integrations/mcp/capture/cli-synced.js +62 -0
- package/dist/integrations/mcp/capture/cli-transform.d.ts +30 -0
- package/dist/integrations/mcp/capture/cli-transform.js +62 -0
- package/dist/integrations/mcp/capture/cli-watcher.d.ts +31 -0
- package/dist/integrations/mcp/capture/cli-watcher.js +106 -0
- package/dist/integrations/mcp/capture/hook-handler.d.ts +2 -0
- package/dist/integrations/mcp/capture/hook-handler.js +157 -0
- package/dist/integrations/mcp/capture/sqlite-reader.d.ts +35 -0
- package/dist/integrations/mcp/capture/sqlite-reader.js +388 -0
- package/dist/integrations/mcp/capture/sync-tracker.d.ts +16 -0
- package/dist/integrations/mcp/capture/sync-tracker.js +102 -0
- package/dist/integrations/mcp/clients/cursor/rules-installer.d.ts +19 -0
- package/dist/integrations/mcp/clients/cursor/rules-installer.js +123 -0
- package/dist/integrations/mcp/index.d.ts +1 -0
- package/dist/integrations/mcp/index.js +94 -0
- package/dist/integrations/mcp/logger.d.ts +8 -0
- package/dist/integrations/mcp/logger.js +50 -0
- package/dist/integrations/mcp/server.d.ts +5 -0
- package/dist/integrations/mcp/server.js +58 -0
- package/dist/integrations/mcp/tools/expand.d.ts +1 -0
- package/dist/integrations/mcp/tools/expand.js +53 -0
- package/dist/integrations/mcp/tools/preview.d.ts +1 -0
- package/dist/integrations/mcp/tools/preview.js +64 -0
- package/dist/integrations/proxy/agents/base.d.ts +43 -0
- package/dist/integrations/proxy/agents/base.js +13 -0
- package/dist/{proxy/utils → integrations/proxy/agents/claude}/extractors.d.ts +4 -8
- package/dist/{proxy/utils → integrations/proxy/agents/claude}/extractors.js +4 -33
- package/dist/{proxy → integrations/proxy/agents/claude}/forwarder.d.ts +1 -1
- package/dist/{proxy → integrations/proxy/agents/claude}/forwarder.js +22 -6
- package/dist/integrations/proxy/agents/claude/index.d.ts +43 -0
- package/dist/integrations/proxy/agents/claude/index.js +386 -0
- package/dist/{proxy/action-parser.d.ts → integrations/proxy/agents/claude/parser.d.ts} +1 -1
- package/dist/integrations/proxy/agents/codex/extractors.d.ts +6 -0
- package/dist/integrations/proxy/agents/codex/extractors.js +49 -0
- package/dist/integrations/proxy/agents/codex/forwarder.d.ts +9 -0
- package/dist/integrations/proxy/agents/codex/forwarder.js +125 -0
- package/dist/integrations/proxy/agents/codex/index.d.ts +44 -0
- package/dist/integrations/proxy/agents/codex/index.js +371 -0
- package/dist/integrations/proxy/agents/codex/parser.d.ts +11 -0
- package/dist/integrations/proxy/agents/codex/parser.js +104 -0
- package/dist/integrations/proxy/agents/codex/patch.d.ts +12 -0
- package/dist/integrations/proxy/agents/codex/patch.js +40 -0
- package/dist/integrations/proxy/agents/codex/settings.d.ts +18 -0
- package/dist/integrations/proxy/agents/codex/settings.js +73 -0
- package/dist/integrations/proxy/agents/codex/types.d.ts +59 -0
- package/dist/integrations/proxy/agents/codex/types.js +2 -0
- package/dist/integrations/proxy/agents/index.d.ts +11 -0
- package/dist/integrations/proxy/agents/index.js +25 -0
- package/dist/integrations/proxy/agents/types.d.ts +77 -0
- package/dist/integrations/proxy/agents/types.js +2 -0
- package/dist/{proxy → integrations/proxy/cache}/extended-cache.js +2 -6
- package/dist/{proxy → integrations/proxy}/config.js +1 -1
- package/dist/{proxy → integrations/proxy}/handlers/preprocess.d.ts +3 -3
- package/dist/integrations/proxy/handlers/preprocess.js +194 -0
- package/dist/integrations/proxy/index.js +20 -0
- package/dist/integrations/proxy/injection/memory-injection.d.ts +56 -0
- package/dist/integrations/proxy/injection/memory-injection.js +252 -0
- package/dist/integrations/proxy/orchestrator.d.ts +30 -0
- package/dist/integrations/proxy/orchestrator.js +954 -0
- package/dist/integrations/proxy/request-processor.d.ts +14 -0
- package/dist/integrations/proxy/request-processor.js +68 -0
- package/dist/{proxy → integrations/proxy}/response-processor.d.ts +4 -3
- package/dist/{proxy → integrations/proxy}/response-processor.js +51 -43
- package/dist/{proxy → integrations/proxy}/server.d.ts +0 -1
- package/dist/integrations/proxy/server.js +146 -0
- package/dist/{proxy → integrations/proxy}/types.d.ts +4 -0
- package/dist/{proxy → integrations/proxy}/utils/logging.d.ts +1 -0
- package/dist/{proxy → integrations/proxy}/utils/logging.js +5 -0
- package/package.json +31 -10
- package/postinstall.js +62 -6
- package/dist/cli.js +0 -149
- package/dist/commands/capture.d.ts +0 -6
- package/dist/commands/capture.js +0 -324
- package/dist/commands/disable.d.ts +0 -1
- package/dist/commands/disable.js +0 -14
- package/dist/commands/doctor.d.ts +0 -1
- package/dist/commands/doctor.js +0 -89
- package/dist/commands/init.d.ts +0 -1
- package/dist/commands/init.js +0 -52
- package/dist/commands/inject.d.ts +0 -5
- package/dist/commands/inject.js +0 -88
- package/dist/commands/prompt-inject.d.ts +0 -4
- package/dist/commands/prompt-inject.js +0 -451
- package/dist/commands/unregister.d.ts +0 -1
- package/dist/commands/unregister.js +0 -28
- package/dist/lib/anchor-extractor.d.ts +0 -30
- package/dist/lib/anchor-extractor.js +0 -296
- package/dist/lib/correction-builder.d.ts +0 -10
- package/dist/lib/correction-builder.js +0 -226
- package/dist/lib/drift-checker-proxy.js +0 -373
- package/dist/lib/drift-checker.d.ts +0 -66
- package/dist/lib/drift-checker.js +0 -341
- package/dist/lib/hooks.d.ts +0 -38
- package/dist/lib/hooks.js +0 -291
- package/dist/lib/jsonl-parser.d.ts +0 -87
- package/dist/lib/jsonl-parser.js +0 -281
- package/dist/lib/session-parser.d.ts +0 -44
- package/dist/lib/session-parser.js +0 -256
- package/dist/lib/store.d.ts +0 -1
- package/dist/proxy/cache.d.ts +0 -32
- package/dist/proxy/cache.js +0 -47
- package/dist/proxy/handlers/preprocess.js +0 -186
- package/dist/proxy/index.js +0 -30
- package/dist/proxy/injection/delta-tracking.d.ts +0 -11
- package/dist/proxy/injection/delta-tracking.js +0 -94
- package/dist/proxy/injection/injectors.d.ts +0 -7
- package/dist/proxy/injection/injectors.js +0 -139
- package/dist/proxy/request-processor.d.ts +0 -27
- package/dist/proxy/request-processor.js +0 -233
- package/dist/proxy/server.js +0 -1289
- /package/dist/{commands → cli/commands}/drift-test.d.ts +0 -0
- /package/dist/{commands → cli/commands}/login.d.ts +0 -0
- /package/dist/{commands → cli/commands}/logout.d.ts +0 -0
- /package/dist/{commands → cli/commands}/proxy-status.d.ts +0 -0
- /package/dist/{commands → cli/commands}/status.d.ts +0 -0
- /package/dist/{commands → cli/commands}/uninstall.d.ts +0 -0
- /package/dist/{cli.d.ts → cli/index.d.ts} +0 -0
- /package/dist/{lib → core/cloud}/api-client.d.ts +0 -0
- /package/dist/{lib → core/cloud}/api-client.js +0 -0
- /package/dist/{lib → core/cloud}/credentials.d.ts +0 -0
- /package/dist/{lib → core/cloud}/credentials.js +0 -0
- /package/dist/{lib → core}/store/convenience.d.ts +0 -0
- /package/dist/{lib → core}/store/convenience.js +0 -0
- /package/dist/{lib → core}/store/database.d.ts +0 -0
- /package/dist/{lib → core}/store/database.js +0 -0
- /package/dist/{lib → core}/store/drift.d.ts +0 -0
- /package/dist/{lib → core}/store/drift.js +0 -0
- /package/dist/{lib → core}/store/index.d.ts +0 -0
- /package/dist/{lib → core}/store/index.js +0 -0
- /package/dist/{lib → core}/store/sessions.d.ts +0 -0
- /package/dist/{lib → core}/store/steps.d.ts +0 -0
- /package/dist/{lib → core}/store/steps.js +0 -0
- /package/dist/{lib → core}/store/tasks.d.ts +0 -0
- /package/dist/{lib → core}/store/tasks.js +0 -0
- /package/dist/{lib → core}/store/types.js +0 -0
- /package/dist/{proxy/action-parser.js → integrations/proxy/agents/claude/parser.js} +0 -0
- /package/dist/{lib → integrations/proxy/agents/claude}/settings.d.ts +0 -0
- /package/dist/{lib → integrations/proxy/agents/claude}/settings.js +0 -0
- /package/dist/{proxy → integrations/proxy/cache}/extended-cache.d.ts +0 -0
- /package/dist/{proxy → integrations/proxy}/config.d.ts +0 -0
- /package/dist/{proxy → integrations/proxy}/index.d.ts +0 -0
- /package/dist/{proxy → integrations/proxy}/types.js +0 -0
- /package/dist/{lib → utils}/debug.d.ts +0 -0
- /package/dist/{lib → utils}/debug.js +0 -0
- /package/dist/{lib → utils}/utils.d.ts +0 -0
- /package/dist/{lib → utils}/utils.js +0 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Cursor Stop Hook Handler
|
|
3
|
+
// Called by Cursor after each LLM response
|
|
4
|
+
// Reads from SQLite, handles mode logic, POSTs to API
|
|
5
|
+
import { getLatestComposerId, getComposerData, getLatestPromptId, getConversationPair, getCurrentWorkspace, dbExists, } from './sqlite-reader.js';
|
|
6
|
+
import { isSynced, markSynced, getPlanState, addToPlanState, clearPlanState, isPlanTimedOut, } from './sync-tracker.js';
|
|
7
|
+
import { getAccessToken, getSyncStatus } from '../../../core/cloud/credentials.js';
|
|
8
|
+
import { request } from 'undici';
|
|
9
|
+
import { mcpLog } from '../logger.js';
|
|
10
|
+
const API_URL = process.env.GROV_API_URL || 'https://api.grov.dev';
|
|
11
|
+
const PLAN_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
|
|
12
|
+
function modeNumToString(num) {
|
|
13
|
+
if (num === 1)
|
|
14
|
+
return 'ask';
|
|
15
|
+
if (num === 5)
|
|
16
|
+
return 'plan';
|
|
17
|
+
return 'agent';
|
|
18
|
+
}
|
|
19
|
+
async function postToApi(teamId, token, payload) {
|
|
20
|
+
try {
|
|
21
|
+
const res = await request(`${API_URL}/teams/${teamId}/cursor/extract`, {
|
|
22
|
+
method: 'POST',
|
|
23
|
+
headers: {
|
|
24
|
+
'Content-Type': 'application/json',
|
|
25
|
+
'Authorization': `Bearer ${token}`,
|
|
26
|
+
},
|
|
27
|
+
body: JSON.stringify(payload),
|
|
28
|
+
});
|
|
29
|
+
return res.statusCode === 200;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function buildPayload(pair, projectPath) {
|
|
36
|
+
return {
|
|
37
|
+
composerId: pair.assistant.composerId,
|
|
38
|
+
usageUuid: pair.assistant.usageUuid,
|
|
39
|
+
mode: modeNumToString(pair.assistant.unifiedMode),
|
|
40
|
+
projectPath,
|
|
41
|
+
original_query: pair.user.text,
|
|
42
|
+
text: pair.assistant.text,
|
|
43
|
+
thinking: pair.assistant.thinking,
|
|
44
|
+
toolCalls: pair.assistant.toolCalls,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
// Syncs accumulated plan messages if the plan session has been idle too long.
|
|
48
|
+
async function handlePlanTimeout(teamId, token) {
|
|
49
|
+
const planState = getPlanState();
|
|
50
|
+
if (!planState || !isPlanTimedOut(PLAN_TIMEOUT_MS))
|
|
51
|
+
return;
|
|
52
|
+
const composer = getComposerData(planState.composerId);
|
|
53
|
+
if (!composer) {
|
|
54
|
+
clearPlanState();
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
for (const usageUuid of planState.usageUuids) {
|
|
58
|
+
if (isSynced(planState.composerId, usageUuid))
|
|
59
|
+
continue;
|
|
60
|
+
const pair = getConversationPair(planState.composerId, usageUuid);
|
|
61
|
+
if (!pair)
|
|
62
|
+
continue;
|
|
63
|
+
const payload = buildPayload(pair, composer.projectPath);
|
|
64
|
+
const success = await postToApi(teamId, token, payload);
|
|
65
|
+
if (success) {
|
|
66
|
+
markSynced(planState.composerId, usageUuid);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
clearPlanState();
|
|
70
|
+
}
|
|
71
|
+
async function handleCurrentPrompt(teamId, token, composerId, usageUuid, projectPath) {
|
|
72
|
+
if (isSynced(composerId, usageUuid))
|
|
73
|
+
return;
|
|
74
|
+
const pair = getConversationPair(composerId, usageUuid);
|
|
75
|
+
if (!pair)
|
|
76
|
+
return;
|
|
77
|
+
const mode = pair.assistant.unifiedMode;
|
|
78
|
+
// Warn if text is empty (helps debug text=0 issue)
|
|
79
|
+
if (pair.assistant.text.length === 0) {
|
|
80
|
+
mcpLog(`[hook] WARNING: Assistant text is EMPTY for ${usageUuid.substring(0, 8)}...`);
|
|
81
|
+
}
|
|
82
|
+
// Ask mode (1): skip
|
|
83
|
+
if (mode === 1) {
|
|
84
|
+
markSynced(composerId, usageUuid);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
// Plan mode (5): accumulate
|
|
88
|
+
if (mode === 5) {
|
|
89
|
+
addToPlanState(composerId, usageUuid);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
// Agent mode (2): check if we have accumulated plan to send first
|
|
93
|
+
const planState = getPlanState();
|
|
94
|
+
if (planState && planState.composerId === composerId) {
|
|
95
|
+
for (const planUuid of planState.usageUuids) {
|
|
96
|
+
if (isSynced(composerId, planUuid))
|
|
97
|
+
continue;
|
|
98
|
+
const planPair = getConversationPair(composerId, planUuid);
|
|
99
|
+
if (!planPair)
|
|
100
|
+
continue;
|
|
101
|
+
const planPayload = buildPayload(planPair, projectPath);
|
|
102
|
+
const success = await postToApi(teamId, token, planPayload);
|
|
103
|
+
if (success)
|
|
104
|
+
markSynced(composerId, planUuid);
|
|
105
|
+
}
|
|
106
|
+
clearPlanState();
|
|
107
|
+
}
|
|
108
|
+
// Send current agent message
|
|
109
|
+
const payload = buildPayload(pair, projectPath);
|
|
110
|
+
const success = await postToApi(teamId, token, payload);
|
|
111
|
+
if (success) {
|
|
112
|
+
markSynced(composerId, usageUuid);
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
mcpLog(`[hook] Failed to sync ${usageUuid.substring(0, 8)}...`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
// Helper to sleep
|
|
119
|
+
function sleep(ms) {
|
|
120
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
121
|
+
}
|
|
122
|
+
async function main() {
|
|
123
|
+
mcpLog(`[hook] started`);
|
|
124
|
+
// Wait 3 seconds to let Cursor finish writing to SQLite
|
|
125
|
+
await sleep(3000);
|
|
126
|
+
// Check prerequisites
|
|
127
|
+
if (!dbExists())
|
|
128
|
+
process.exit(0);
|
|
129
|
+
const syncStatus = getSyncStatus();
|
|
130
|
+
if (!syncStatus?.enabled || !syncStatus.teamId)
|
|
131
|
+
process.exit(0);
|
|
132
|
+
const token = await getAccessToken();
|
|
133
|
+
if (!token)
|
|
134
|
+
process.exit(0);
|
|
135
|
+
const teamId = syncStatus.teamId;
|
|
136
|
+
// Handle any timed-out plan from a DIFFERENT conversation
|
|
137
|
+
await handlePlanTimeout(teamId, token);
|
|
138
|
+
// Get latest composer (skips empty ones)
|
|
139
|
+
const composerId = getLatestComposerId();
|
|
140
|
+
if (!composerId)
|
|
141
|
+
process.exit(0);
|
|
142
|
+
const composer = getComposerData(composerId);
|
|
143
|
+
if (!composer)
|
|
144
|
+
process.exit(0);
|
|
145
|
+
// Get latest prompt (usageUuid) with content
|
|
146
|
+
const usageUuid = getLatestPromptId(composerId);
|
|
147
|
+
if (!usageUuid)
|
|
148
|
+
process.exit(0);
|
|
149
|
+
// Get project path from current workspace (MRU list)
|
|
150
|
+
const projectPath = getCurrentWorkspace() || composer.projectPath;
|
|
151
|
+
await handleCurrentPrompt(teamId, token, composerId, usageUuid, projectPath);
|
|
152
|
+
mcpLog(`[hook] finished`);
|
|
153
|
+
}
|
|
154
|
+
main().catch((err) => {
|
|
155
|
+
mcpLog(`[main] Fatal error: ${err instanceof Error ? err.message : 'unknown'}`);
|
|
156
|
+
process.exit(1);
|
|
157
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export interface ToolCall {
|
|
2
|
+
name: string;
|
|
3
|
+
params: Record<string, unknown>;
|
|
4
|
+
}
|
|
5
|
+
export interface ComposerData {
|
|
6
|
+
composerId: string;
|
|
7
|
+
projectPath: string;
|
|
8
|
+
createdAt: number;
|
|
9
|
+
}
|
|
10
|
+
export interface AggregatedAssistant {
|
|
11
|
+
composerId: string;
|
|
12
|
+
usageUuid: string;
|
|
13
|
+
unifiedMode: 1 | 2 | 5;
|
|
14
|
+
text: string;
|
|
15
|
+
thinking: string;
|
|
16
|
+
toolCalls: ToolCall[];
|
|
17
|
+
bubbleCount: number;
|
|
18
|
+
}
|
|
19
|
+
export interface ConversationPair {
|
|
20
|
+
user: {
|
|
21
|
+
text: string;
|
|
22
|
+
timestamp: number;
|
|
23
|
+
};
|
|
24
|
+
assistant: AggregatedAssistant;
|
|
25
|
+
}
|
|
26
|
+
export declare function getLatestComposerId(): string | null;
|
|
27
|
+
export declare function getComposerData(composerId: string): ComposerData | null;
|
|
28
|
+
export declare function getLatestPromptId(composerId: string): string | null;
|
|
29
|
+
export declare function getConversationPair(composerId: string, usageUuid: string): ConversationPair | null;
|
|
30
|
+
export declare function dbExists(): boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Get current workspace from Cursor's recently opened list.
|
|
33
|
+
* Index 0 = most recently accessed = current workspace.
|
|
34
|
+
*/
|
|
35
|
+
export declare function getCurrentWorkspace(): string | null;
|
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
// Read messages from Cursor SQLite
|
|
2
|
+
// Location varies by OS:
|
|
3
|
+
// Linux: ~/.config/Cursor/User/globalStorage/state.vscdb
|
|
4
|
+
// macOS: ~/Library/Application Support/Cursor/User/globalStorage/state.vscdb
|
|
5
|
+
// Windows: %APPDATA%/Cursor/User/globalStorage/state.vscdb
|
|
6
|
+
import Database from 'better-sqlite3';
|
|
7
|
+
import { homedir, platform } from 'os';
|
|
8
|
+
import { join } from 'path';
|
|
9
|
+
import { existsSync } from 'fs';
|
|
10
|
+
import { mcpLog } from '../logger.js';
|
|
11
|
+
function getCursorDbPath() {
|
|
12
|
+
const home = homedir();
|
|
13
|
+
switch (platform()) {
|
|
14
|
+
case 'darwin':
|
|
15
|
+
return join(home, 'Library/Application Support/Cursor/User/globalStorage/state.vscdb');
|
|
16
|
+
case 'win32':
|
|
17
|
+
return join(process.env.APPDATA || join(home, 'AppData/Roaming'), 'Cursor/User/globalStorage/state.vscdb');
|
|
18
|
+
default:
|
|
19
|
+
return join(home, '.config/Cursor/User/globalStorage/state.vscdb');
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
const CURSOR_DB_PATH = getCursorDbPath();
|
|
23
|
+
const TABLE = 'cursorDiskKV';
|
|
24
|
+
function getDb() {
|
|
25
|
+
if (!existsSync(CURSOR_DB_PATH))
|
|
26
|
+
return null;
|
|
27
|
+
try {
|
|
28
|
+
return new Database(CURSOR_DB_PATH, { readonly: true });
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function getValue(db, key) {
|
|
35
|
+
const row = db.prepare(`SELECT value FROM ${TABLE} WHERE key = ?`).get(key);
|
|
36
|
+
if (!row)
|
|
37
|
+
return null;
|
|
38
|
+
try {
|
|
39
|
+
return JSON.parse(row.value);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export function getLatestComposerId() {
|
|
46
|
+
const db = getDb();
|
|
47
|
+
if (!db)
|
|
48
|
+
return null;
|
|
49
|
+
try {
|
|
50
|
+
// Get all composer keys with their createdAt
|
|
51
|
+
const rows = db.prepare(`SELECT key FROM ${TABLE} WHERE key LIKE 'composerData:%'`).all();
|
|
52
|
+
mcpLog(`[getLatestComposerId] Found ${rows.length} composers, checking for bubbles...`);
|
|
53
|
+
// Build list of composers with their timestamps
|
|
54
|
+
const composers = [];
|
|
55
|
+
for (const row of rows) {
|
|
56
|
+
const composerId = row.key.replace('composerData:', '');
|
|
57
|
+
const data = getValue(db, row.key);
|
|
58
|
+
if (data?.createdAt) {
|
|
59
|
+
composers.push({ id: composerId, createdAt: data.createdAt });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// Sort by createdAt descending (newest first)
|
|
63
|
+
composers.sort((a, b) => b.createdAt - a.createdAt);
|
|
64
|
+
// Find first composer that has at least one bubble
|
|
65
|
+
for (const composer of composers) {
|
|
66
|
+
const bubbleCount = db.prepare(`SELECT COUNT(*) as count FROM ${TABLE} WHERE key LIKE ?`).get(`bubbleId:${composer.id}:%`);
|
|
67
|
+
if (bubbleCount.count > 0) {
|
|
68
|
+
mcpLog(`[getLatestComposerId] Selected ${composer.id.substring(0, 8)}... (${bubbleCount.count} bubbles)`);
|
|
69
|
+
return composer.id;
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
mcpLog(`[getLatestComposerId] Skipping ${composer.id.substring(0, 8)}... (0 bubbles)`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
mcpLog(`[getLatestComposerId] No composer with bubbles found`);
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
db.close();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
export function getComposerData(composerId) {
|
|
83
|
+
const db = getDb();
|
|
84
|
+
if (!db)
|
|
85
|
+
return null;
|
|
86
|
+
try {
|
|
87
|
+
const data = getValue(db, `composerData:${composerId}`);
|
|
88
|
+
if (!data)
|
|
89
|
+
return null;
|
|
90
|
+
let projectPath = '';
|
|
91
|
+
// Primary: Extract from messageRequestContext.ideEditorsState
|
|
92
|
+
// Find a user bubble (type=1) to get its bubbleId
|
|
93
|
+
const userBubble = db.prepare(`
|
|
94
|
+
SELECT json_extract(value, '$.bubbleId') as bubbleId
|
|
95
|
+
FROM ${TABLE}
|
|
96
|
+
WHERE key LIKE ? AND json_extract(value, '$.type') = 1
|
|
97
|
+
LIMIT 1
|
|
98
|
+
`).get(`bubbleId:${composerId}:%`);
|
|
99
|
+
if (userBubble?.bubbleId) {
|
|
100
|
+
const msgCtx = getValue(db, `messageRequestContext:${composerId}:${userBubble.bubbleId}`);
|
|
101
|
+
if (msgCtx?.ideEditorsState && typeof msgCtx.ideEditorsState === 'string') {
|
|
102
|
+
try {
|
|
103
|
+
const ide = JSON.parse(msgCtx.ideEditorsState);
|
|
104
|
+
const file = ide.visibleFiles?.[0];
|
|
105
|
+
if (file?.relativePath && file?.absolutePath && file.absolutePath.endsWith(file.relativePath)) {
|
|
106
|
+
projectPath = file.absolutePath.slice(0, -(file.relativePath.length + 1));
|
|
107
|
+
mcpLog(`[getComposerData] Project from ideEditorsState: ${projectPath}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
// ignore parse errors
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// Fallback: Extract from toolFormerData file paths
|
|
116
|
+
if (!projectPath) {
|
|
117
|
+
const toolBubbles = db.prepare(`
|
|
118
|
+
SELECT json_extract(value, '$.toolFormerData') as toolData
|
|
119
|
+
FROM ${TABLE}
|
|
120
|
+
WHERE key LIKE ? AND json_extract(value, '$.toolFormerData') IS NOT NULL
|
|
121
|
+
LIMIT 5
|
|
122
|
+
`).all(`bubbleId:${composerId}:%`);
|
|
123
|
+
for (const row of toolBubbles) {
|
|
124
|
+
if (projectPath)
|
|
125
|
+
break;
|
|
126
|
+
try {
|
|
127
|
+
const toolData = JSON.parse(row.toolData);
|
|
128
|
+
if (toolData.params) {
|
|
129
|
+
const params = JSON.parse(toolData.params);
|
|
130
|
+
// Look for file paths in common param names
|
|
131
|
+
const filePath = params.targetFile || params.relativeWorkspacePath || params.file_path || params.path;
|
|
132
|
+
if (typeof filePath === 'string' && filePath.startsWith('/')) {
|
|
133
|
+
// Extract project root from absolute path (assume src/, lib/, etc. are inside project)
|
|
134
|
+
const match = filePath.match(/^(\/[^/]+(?:\/[^/]+)*?)\/(?:src|lib|test|tests|app|packages|node_modules)\//);
|
|
135
|
+
if (match) {
|
|
136
|
+
projectPath = match[1];
|
|
137
|
+
mcpLog(`[getComposerData] Project from toolFormerData: ${projectPath}`);
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
// ignore parse errors
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
composerId,
|
|
150
|
+
projectPath,
|
|
151
|
+
createdAt: typeof data.createdAt === 'number' ? data.createdAt : 0,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
finally {
|
|
155
|
+
db.close();
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
export function getLatestPromptId(composerId) {
|
|
159
|
+
const db = getDb();
|
|
160
|
+
if (!db)
|
|
161
|
+
return null;
|
|
162
|
+
try {
|
|
163
|
+
// Get all bubbles for this composer with their usageUuid and content info
|
|
164
|
+
const rows = db.prepare(`
|
|
165
|
+
SELECT
|
|
166
|
+
key,
|
|
167
|
+
json_extract(value, '$.usageUuid') as usageUuid,
|
|
168
|
+
json_extract(value, '$.requestId') as requestId,
|
|
169
|
+
json_extract(value, '$.type') as type,
|
|
170
|
+
json_extract(value, '$.text') as text,
|
|
171
|
+
json_extract(value, '$.thinking.text') as thinking,
|
|
172
|
+
json_extract(value, '$.createdAt') as createdAt
|
|
173
|
+
FROM ${TABLE}
|
|
174
|
+
WHERE key LIKE ?
|
|
175
|
+
ORDER BY json_extract(value, '$.createdAt') DESC
|
|
176
|
+
`).all(`bubbleId:${composerId}:%`);
|
|
177
|
+
mcpLog(`[getLatestPromptId] Composer ${composerId.substring(0, 8)}: found ${rows.length} bubbles total`);
|
|
178
|
+
if (rows.length === 0)
|
|
179
|
+
return null;
|
|
180
|
+
// First: collect all usageUuids that have a user bubble (type=1)
|
|
181
|
+
// These are the only valid ones we can use
|
|
182
|
+
const uuidsWithUserBubble = new Set();
|
|
183
|
+
for (const row of rows) {
|
|
184
|
+
if (row.type === 1 && row.requestId) {
|
|
185
|
+
uuidsWithUserBubble.add(row.requestId);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
mcpLog(`[getLatestPromptId] usageUuids with user bubble: ${uuidsWithUserBubble.size}`);
|
|
189
|
+
// Group by usageUuid, track latest timestamp and whether it has content
|
|
190
|
+
const promptMap = new Map();
|
|
191
|
+
for (const row of rows) {
|
|
192
|
+
// usageUuid is on assistant messages, requestId is on user messages (same value)
|
|
193
|
+
const uuid = row.usageUuid || row.requestId;
|
|
194
|
+
if (!uuid)
|
|
195
|
+
continue;
|
|
196
|
+
// Skip usageUuids that don't have a user bubble (continuations)
|
|
197
|
+
if (!uuidsWithUserBubble.has(uuid))
|
|
198
|
+
continue;
|
|
199
|
+
const hasContent = Boolean((row.text && row.text.length > 0) || (row.thinking && row.thinking.length > 0));
|
|
200
|
+
const timestamp = row.createdAt ? parseInt(row.createdAt, 10) : 0;
|
|
201
|
+
const existing = promptMap.get(uuid);
|
|
202
|
+
if (!existing) {
|
|
203
|
+
promptMap.set(uuid, { maxTimestamp: timestamp, hasContent });
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
if (timestamp > existing.maxTimestamp) {
|
|
207
|
+
existing.maxTimestamp = timestamp;
|
|
208
|
+
}
|
|
209
|
+
if (hasContent) {
|
|
210
|
+
existing.hasContent = true;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
mcpLog(`[getLatestPromptId] Distinct usageUuids (with user bubble): ${promptMap.size}`);
|
|
215
|
+
// Find the latest usageUuid that has content
|
|
216
|
+
let latestUuid = null;
|
|
217
|
+
let latestTime = 0;
|
|
218
|
+
for (const [uuid, info] of promptMap) {
|
|
219
|
+
if (info.hasContent && info.maxTimestamp > latestTime) {
|
|
220
|
+
latestTime = info.maxTimestamp;
|
|
221
|
+
latestUuid = uuid;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (latestUuid) {
|
|
225
|
+
mcpLog(`[getLatestPromptId] Latest with content: ${latestUuid.substring(0, 8)}... (timestamp: ${latestTime})`);
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
mcpLog(`[getLatestPromptId] No valid prompt found with content`);
|
|
229
|
+
}
|
|
230
|
+
return latestUuid;
|
|
231
|
+
}
|
|
232
|
+
finally {
|
|
233
|
+
db.close();
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
export function getConversationPair(composerId, usageUuid) {
|
|
237
|
+
const db = getDb();
|
|
238
|
+
if (!db)
|
|
239
|
+
return null;
|
|
240
|
+
try {
|
|
241
|
+
// Get all bubbles for this usageUuid
|
|
242
|
+
const rows = db.prepare(`
|
|
243
|
+
SELECT
|
|
244
|
+
json_extract(value, '$.type') as type,
|
|
245
|
+
json_extract(value, '$.usageUuid') as usageUuid,
|
|
246
|
+
json_extract(value, '$.requestId') as requestId,
|
|
247
|
+
json_extract(value, '$.text') as text,
|
|
248
|
+
json_extract(value, '$.thinking.text') as thinking,
|
|
249
|
+
json_extract(value, '$.unifiedMode') as unifiedMode,
|
|
250
|
+
json_extract(value, '$.createdAt') as createdAt,
|
|
251
|
+
json_extract(value, '$.toolFormerData') as toolFormerData
|
|
252
|
+
FROM ${TABLE}
|
|
253
|
+
WHERE key LIKE ?
|
|
254
|
+
ORDER BY json_extract(value, '$.createdAt') ASC
|
|
255
|
+
`).all(`bubbleId:${composerId}:%`);
|
|
256
|
+
// Find START: user bubble (type=1) with requestId = usageUuid
|
|
257
|
+
mcpLog(`[getConversationPair] Total rows: ${rows.length}, looking for usageUuid=${usageUuid.substring(0, 8)}...`);
|
|
258
|
+
const startIdx = rows.findIndex(r => r.type === 1 && r.requestId === usageUuid);
|
|
259
|
+
if (startIdx === -1) {
|
|
260
|
+
mcpLog(`[getConversationPair] No user bubble found for usageUuid`);
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
const userBubble = rows[startIdx];
|
|
264
|
+
mcpLog(`[getConversationPair] Found user bubble at index ${startIdx}`);
|
|
265
|
+
// Collect ALL type=2 bubbles until next type=1 or end of array
|
|
266
|
+
// This includes continuation bubbles with NULL usageUuid
|
|
267
|
+
const assistantBubbles = [];
|
|
268
|
+
for (let i = startIdx + 1; i < rows.length; i++) {
|
|
269
|
+
if (rows[i].type === 1) {
|
|
270
|
+
mcpLog(`[getConversationPair] Stopping at next user bubble (index ${i})`);
|
|
271
|
+
break;
|
|
272
|
+
}
|
|
273
|
+
if (rows[i].type === 2) {
|
|
274
|
+
assistantBubbles.push(rows[i]);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
mcpLog(`[getConversationPair] Found ${assistantBubbles.length} assistant bubbles (including continuations)`);
|
|
278
|
+
// Debug: log each bubble's fields
|
|
279
|
+
for (let i = 0; i < assistantBubbles.length; i++) {
|
|
280
|
+
const b = assistantBubbles[i];
|
|
281
|
+
mcpLog(`[getConversationPair] Bubble ${i}: text=${b.text?.length || 0}, thinking=${b.thinking?.length || 0}, hasTool=${b.toolFormerData ? 'yes' : 'no'}`);
|
|
282
|
+
}
|
|
283
|
+
// Aggregate thinking from all bubbles
|
|
284
|
+
const thinkingParts = [];
|
|
285
|
+
let withThinking = 0;
|
|
286
|
+
let withText = 0;
|
|
287
|
+
for (const bubble of assistantBubbles) {
|
|
288
|
+
if (bubble.thinking && bubble.thinking.length > 0) {
|
|
289
|
+
thinkingParts.push(bubble.thinking);
|
|
290
|
+
withThinking++;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
// Get text from the last bubble that has text
|
|
294
|
+
let finalText = '';
|
|
295
|
+
for (let i = assistantBubbles.length - 1; i >= 0; i--) {
|
|
296
|
+
if (assistantBubbles[i].text && assistantBubbles[i].text.length > 0) {
|
|
297
|
+
finalText = assistantBubbles[i].text;
|
|
298
|
+
withText++;
|
|
299
|
+
break;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
// Aggregate tool calls from all bubbles (toolFormerData is a JSON object with name/params)
|
|
303
|
+
const allToolCalls = [];
|
|
304
|
+
for (const bubble of assistantBubbles) {
|
|
305
|
+
if (bubble.toolFormerData) {
|
|
306
|
+
try {
|
|
307
|
+
const toolData = JSON.parse(bubble.toolFormerData);
|
|
308
|
+
if (toolData.name) {
|
|
309
|
+
let params = {};
|
|
310
|
+
if (toolData.params) {
|
|
311
|
+
try {
|
|
312
|
+
params = JSON.parse(toolData.params);
|
|
313
|
+
}
|
|
314
|
+
catch {
|
|
315
|
+
// params might not be valid JSON
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
allToolCalls.push({
|
|
319
|
+
name: toolData.name,
|
|
320
|
+
params,
|
|
321
|
+
});
|
|
322
|
+
mcpLog(`[getConversationPair] Tool: ${toolData.name}, params keys: ${Object.keys(params).join(',')}`);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
catch {
|
|
326
|
+
// ignore parse errors
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
// Get unifiedMode from first assistant bubble
|
|
331
|
+
const unifiedMode = assistantBubbles[0]?.unifiedMode;
|
|
332
|
+
const mode = unifiedMode === 1 ? 1 : unifiedMode === 5 ? 5 : 2;
|
|
333
|
+
const aggregatedThinking = thinkingParts.join('\n\n');
|
|
334
|
+
mcpLog(`[getConversationPair] User: "${userBubble.text?.substring(0, 50)}..." (${userBubble.text?.length || 0} chars)`);
|
|
335
|
+
mcpLog(`[getConversationPair] Assistant bubbles: ${assistantBubbles.length} total, ${withThinking} with thinking, ${withText > 0 ? 1 : 0} with text`);
|
|
336
|
+
mcpLog(`[getConversationPair] Aggregated: thinking=${aggregatedThinking.length} chars, text=${finalText.length} chars, toolCalls=${allToolCalls.length}`);
|
|
337
|
+
return {
|
|
338
|
+
user: {
|
|
339
|
+
text: userBubble.text || '',
|
|
340
|
+
timestamp: userBubble.createdAt ? parseInt(userBubble.createdAt, 10) : 0,
|
|
341
|
+
},
|
|
342
|
+
assistant: {
|
|
343
|
+
composerId,
|
|
344
|
+
usageUuid,
|
|
345
|
+
unifiedMode: mode,
|
|
346
|
+
text: finalText,
|
|
347
|
+
thinking: aggregatedThinking,
|
|
348
|
+
toolCalls: allToolCalls,
|
|
349
|
+
bubbleCount: assistantBubbles.length,
|
|
350
|
+
},
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
finally {
|
|
354
|
+
db.close();
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
export function dbExists() {
|
|
358
|
+
return existsSync(CURSOR_DB_PATH);
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Get current workspace from Cursor's recently opened list.
|
|
362
|
+
* Index 0 = most recently accessed = current workspace.
|
|
363
|
+
*/
|
|
364
|
+
export function getCurrentWorkspace() {
|
|
365
|
+
const db = getDb();
|
|
366
|
+
if (!db)
|
|
367
|
+
return null;
|
|
368
|
+
try {
|
|
369
|
+
const row = db.prepare(`SELECT value FROM ItemTable WHERE key = ?`).get('history.recentlyOpenedPathsList');
|
|
370
|
+
if (!row)
|
|
371
|
+
return null;
|
|
372
|
+
const data = JSON.parse(row.value);
|
|
373
|
+
const folderUri = data.entries?.[0]?.folderUri;
|
|
374
|
+
if (!folderUri)
|
|
375
|
+
return null;
|
|
376
|
+
// Remove file:// prefix
|
|
377
|
+
const projectPath = folderUri.replace('file://', '');
|
|
378
|
+
mcpLog(`[getCurrentWorkspace] Current workspace: ${projectPath}`);
|
|
379
|
+
return projectPath;
|
|
380
|
+
}
|
|
381
|
+
catch (err) {
|
|
382
|
+
mcpLog(`[getCurrentWorkspace] Error: ${err}`);
|
|
383
|
+
return null;
|
|
384
|
+
}
|
|
385
|
+
finally {
|
|
386
|
+
db.close();
|
|
387
|
+
}
|
|
388
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
type SyncedId = string;
|
|
2
|
+
interface PlanState {
|
|
3
|
+
composerId: string;
|
|
4
|
+
usageUuids: string[];
|
|
5
|
+
lastActivity: number;
|
|
6
|
+
}
|
|
7
|
+
export declare function getSyncedIds(): Set<SyncedId>;
|
|
8
|
+
export declare function isSynced(composerId: string, usageUuid: string): boolean;
|
|
9
|
+
export declare function markSynced(composerId: string, usageUuid: string): void;
|
|
10
|
+
export declare function getPlanState(): PlanState | null;
|
|
11
|
+
export declare function setPlanState(composerId: string, usageUuids: string[]): void;
|
|
12
|
+
export declare function addToPlanState(composerId: string, usageUuid: string): void;
|
|
13
|
+
export declare function clearPlanState(): void;
|
|
14
|
+
export declare function isPlanTimedOut(timeoutMs?: number): boolean;
|
|
15
|
+
export declare function pruneSyncedOlderThan(days?: number): void;
|
|
16
|
+
export {};
|