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.
Files changed (190) hide show
  1. package/dist/cli/agents/registry.d.ts +17 -0
  2. package/dist/cli/agents/registry.js +132 -0
  3. package/dist/cli/commands/agents.d.ts +1 -0
  4. package/dist/cli/commands/agents.js +48 -0
  5. package/dist/cli/commands/disable.d.ts +1 -0
  6. package/dist/cli/commands/disable.js +179 -0
  7. package/dist/cli/commands/doctor.d.ts +1 -0
  8. package/dist/cli/commands/doctor.js +157 -0
  9. package/dist/{commands → cli/commands}/drift-test.js +39 -26
  10. package/dist/cli/commands/init.d.ts +1 -0
  11. package/dist/cli/commands/init.js +90 -0
  12. package/dist/{commands → cli/commands}/login.js +19 -18
  13. package/dist/{commands → cli/commands}/logout.js +1 -1
  14. package/dist/{commands → cli/commands}/proxy-status.js +1 -1
  15. package/dist/cli/commands/setup.d.ts +6 -0
  16. package/dist/cli/commands/setup.js +309 -0
  17. package/dist/{commands → cli/commands}/status.js +1 -1
  18. package/dist/{commands → cli/commands}/sync.d.ts +1 -0
  19. package/dist/{commands → cli/commands}/sync.js +59 -4
  20. package/dist/{commands → cli/commands}/uninstall.js +2 -2
  21. package/dist/cli/index.js +270 -0
  22. package/dist/{lib → core/cloud}/cloud-sync.d.ts +3 -3
  23. package/dist/{lib → core/cloud}/cloud-sync.js +10 -10
  24. package/dist/{lib → core/extraction}/correction-builder-proxy.d.ts +1 -1
  25. package/dist/{lib → core/extraction}/correction-builder-proxy.js +0 -4
  26. package/dist/{lib → core/extraction}/drift-checker-proxy.d.ts +13 -9
  27. package/dist/core/extraction/drift-checker-proxy.js +510 -0
  28. package/dist/{lib → core/extraction}/llm-extractor.d.ts +8 -38
  29. package/dist/{lib → core/extraction}/llm-extractor.js +132 -220
  30. package/dist/{lib → core}/store/sessions.js +3 -19
  31. package/dist/core/store/store.d.ts +1 -0
  32. package/dist/{lib → core/store}/store.js +1 -1
  33. package/dist/{lib → core}/store/types.d.ts +0 -4
  34. package/dist/integrations/mcp/cache.d.ts +27 -0
  35. package/dist/integrations/mcp/cache.js +106 -0
  36. package/dist/integrations/mcp/capture/antigravity-parser.d.ts +26 -0
  37. package/dist/integrations/mcp/capture/antigravity-parser.js +272 -0
  38. package/dist/integrations/mcp/capture/antigravity-scanner.d.ts +24 -0
  39. package/dist/integrations/mcp/capture/antigravity-scanner.js +153 -0
  40. package/dist/integrations/mcp/capture/antigravity-sync-tracker.d.ts +29 -0
  41. package/dist/integrations/mcp/capture/antigravity-sync-tracker.js +115 -0
  42. package/dist/integrations/mcp/capture/cli-extractor.d.ts +18 -0
  43. package/dist/integrations/mcp/capture/cli-extractor.js +258 -0
  44. package/dist/integrations/mcp/capture/cli-synced.d.ts +4 -0
  45. package/dist/integrations/mcp/capture/cli-synced.js +62 -0
  46. package/dist/integrations/mcp/capture/cli-transform.d.ts +30 -0
  47. package/dist/integrations/mcp/capture/cli-transform.js +62 -0
  48. package/dist/integrations/mcp/capture/cli-watcher.d.ts +31 -0
  49. package/dist/integrations/mcp/capture/cli-watcher.js +106 -0
  50. package/dist/integrations/mcp/capture/hook-handler.d.ts +2 -0
  51. package/dist/integrations/mcp/capture/hook-handler.js +157 -0
  52. package/dist/integrations/mcp/capture/sqlite-reader.d.ts +35 -0
  53. package/dist/integrations/mcp/capture/sqlite-reader.js +388 -0
  54. package/dist/integrations/mcp/capture/sync-tracker.d.ts +16 -0
  55. package/dist/integrations/mcp/capture/sync-tracker.js +102 -0
  56. package/dist/integrations/mcp/clients/cursor/rules-installer.d.ts +19 -0
  57. package/dist/integrations/mcp/clients/cursor/rules-installer.js +123 -0
  58. package/dist/integrations/mcp/index.d.ts +1 -0
  59. package/dist/integrations/mcp/index.js +94 -0
  60. package/dist/integrations/mcp/logger.d.ts +8 -0
  61. package/dist/integrations/mcp/logger.js +50 -0
  62. package/dist/integrations/mcp/server.d.ts +5 -0
  63. package/dist/integrations/mcp/server.js +58 -0
  64. package/dist/integrations/mcp/tools/expand.d.ts +1 -0
  65. package/dist/integrations/mcp/tools/expand.js +53 -0
  66. package/dist/integrations/mcp/tools/preview.d.ts +1 -0
  67. package/dist/integrations/mcp/tools/preview.js +64 -0
  68. package/dist/integrations/proxy/agents/base.d.ts +43 -0
  69. package/dist/integrations/proxy/agents/base.js +13 -0
  70. package/dist/{proxy/utils → integrations/proxy/agents/claude}/extractors.d.ts +4 -8
  71. package/dist/{proxy/utils → integrations/proxy/agents/claude}/extractors.js +4 -33
  72. package/dist/{proxy → integrations/proxy/agents/claude}/forwarder.d.ts +1 -1
  73. package/dist/{proxy → integrations/proxy/agents/claude}/forwarder.js +22 -6
  74. package/dist/integrations/proxy/agents/claude/index.d.ts +43 -0
  75. package/dist/integrations/proxy/agents/claude/index.js +386 -0
  76. package/dist/{proxy/action-parser.d.ts → integrations/proxy/agents/claude/parser.d.ts} +1 -1
  77. package/dist/integrations/proxy/agents/codex/extractors.d.ts +6 -0
  78. package/dist/integrations/proxy/agents/codex/extractors.js +49 -0
  79. package/dist/integrations/proxy/agents/codex/forwarder.d.ts +9 -0
  80. package/dist/integrations/proxy/agents/codex/forwarder.js +125 -0
  81. package/dist/integrations/proxy/agents/codex/index.d.ts +44 -0
  82. package/dist/integrations/proxy/agents/codex/index.js +371 -0
  83. package/dist/integrations/proxy/agents/codex/parser.d.ts +11 -0
  84. package/dist/integrations/proxy/agents/codex/parser.js +104 -0
  85. package/dist/integrations/proxy/agents/codex/patch.d.ts +12 -0
  86. package/dist/integrations/proxy/agents/codex/patch.js +40 -0
  87. package/dist/integrations/proxy/agents/codex/settings.d.ts +18 -0
  88. package/dist/integrations/proxy/agents/codex/settings.js +73 -0
  89. package/dist/integrations/proxy/agents/codex/types.d.ts +59 -0
  90. package/dist/integrations/proxy/agents/codex/types.js +2 -0
  91. package/dist/integrations/proxy/agents/index.d.ts +11 -0
  92. package/dist/integrations/proxy/agents/index.js +25 -0
  93. package/dist/integrations/proxy/agents/types.d.ts +77 -0
  94. package/dist/integrations/proxy/agents/types.js +2 -0
  95. package/dist/{proxy → integrations/proxy/cache}/extended-cache.js +2 -6
  96. package/dist/{proxy → integrations/proxy}/config.js +1 -1
  97. package/dist/{proxy → integrations/proxy}/handlers/preprocess.d.ts +3 -3
  98. package/dist/integrations/proxy/handlers/preprocess.js +194 -0
  99. package/dist/integrations/proxy/index.js +20 -0
  100. package/dist/integrations/proxy/injection/memory-injection.d.ts +56 -0
  101. package/dist/integrations/proxy/injection/memory-injection.js +252 -0
  102. package/dist/integrations/proxy/orchestrator.d.ts +30 -0
  103. package/dist/integrations/proxy/orchestrator.js +954 -0
  104. package/dist/integrations/proxy/request-processor.d.ts +14 -0
  105. package/dist/integrations/proxy/request-processor.js +68 -0
  106. package/dist/{proxy → integrations/proxy}/response-processor.d.ts +4 -3
  107. package/dist/{proxy → integrations/proxy}/response-processor.js +51 -43
  108. package/dist/{proxy → integrations/proxy}/server.d.ts +0 -1
  109. package/dist/integrations/proxy/server.js +146 -0
  110. package/dist/{proxy → integrations/proxy}/types.d.ts +4 -0
  111. package/dist/{proxy → integrations/proxy}/utils/logging.d.ts +1 -0
  112. package/dist/{proxy → integrations/proxy}/utils/logging.js +5 -0
  113. package/package.json +31 -10
  114. package/postinstall.js +62 -6
  115. package/dist/cli.js +0 -149
  116. package/dist/commands/capture.d.ts +0 -6
  117. package/dist/commands/capture.js +0 -324
  118. package/dist/commands/disable.d.ts +0 -1
  119. package/dist/commands/disable.js +0 -14
  120. package/dist/commands/doctor.d.ts +0 -1
  121. package/dist/commands/doctor.js +0 -89
  122. package/dist/commands/init.d.ts +0 -1
  123. package/dist/commands/init.js +0 -52
  124. package/dist/commands/inject.d.ts +0 -5
  125. package/dist/commands/inject.js +0 -88
  126. package/dist/commands/prompt-inject.d.ts +0 -4
  127. package/dist/commands/prompt-inject.js +0 -451
  128. package/dist/commands/unregister.d.ts +0 -1
  129. package/dist/commands/unregister.js +0 -28
  130. package/dist/lib/anchor-extractor.d.ts +0 -30
  131. package/dist/lib/anchor-extractor.js +0 -296
  132. package/dist/lib/correction-builder.d.ts +0 -10
  133. package/dist/lib/correction-builder.js +0 -226
  134. package/dist/lib/drift-checker-proxy.js +0 -373
  135. package/dist/lib/drift-checker.d.ts +0 -66
  136. package/dist/lib/drift-checker.js +0 -341
  137. package/dist/lib/hooks.d.ts +0 -38
  138. package/dist/lib/hooks.js +0 -291
  139. package/dist/lib/jsonl-parser.d.ts +0 -87
  140. package/dist/lib/jsonl-parser.js +0 -281
  141. package/dist/lib/session-parser.d.ts +0 -44
  142. package/dist/lib/session-parser.js +0 -256
  143. package/dist/lib/store.d.ts +0 -1
  144. package/dist/proxy/cache.d.ts +0 -32
  145. package/dist/proxy/cache.js +0 -47
  146. package/dist/proxy/handlers/preprocess.js +0 -186
  147. package/dist/proxy/index.js +0 -30
  148. package/dist/proxy/injection/delta-tracking.d.ts +0 -11
  149. package/dist/proxy/injection/delta-tracking.js +0 -94
  150. package/dist/proxy/injection/injectors.d.ts +0 -7
  151. package/dist/proxy/injection/injectors.js +0 -139
  152. package/dist/proxy/request-processor.d.ts +0 -27
  153. package/dist/proxy/request-processor.js +0 -233
  154. package/dist/proxy/server.js +0 -1289
  155. /package/dist/{commands → cli/commands}/drift-test.d.ts +0 -0
  156. /package/dist/{commands → cli/commands}/login.d.ts +0 -0
  157. /package/dist/{commands → cli/commands}/logout.d.ts +0 -0
  158. /package/dist/{commands → cli/commands}/proxy-status.d.ts +0 -0
  159. /package/dist/{commands → cli/commands}/status.d.ts +0 -0
  160. /package/dist/{commands → cli/commands}/uninstall.d.ts +0 -0
  161. /package/dist/{cli.d.ts → cli/index.d.ts} +0 -0
  162. /package/dist/{lib → core/cloud}/api-client.d.ts +0 -0
  163. /package/dist/{lib → core/cloud}/api-client.js +0 -0
  164. /package/dist/{lib → core/cloud}/credentials.d.ts +0 -0
  165. /package/dist/{lib → core/cloud}/credentials.js +0 -0
  166. /package/dist/{lib → core}/store/convenience.d.ts +0 -0
  167. /package/dist/{lib → core}/store/convenience.js +0 -0
  168. /package/dist/{lib → core}/store/database.d.ts +0 -0
  169. /package/dist/{lib → core}/store/database.js +0 -0
  170. /package/dist/{lib → core}/store/drift.d.ts +0 -0
  171. /package/dist/{lib → core}/store/drift.js +0 -0
  172. /package/dist/{lib → core}/store/index.d.ts +0 -0
  173. /package/dist/{lib → core}/store/index.js +0 -0
  174. /package/dist/{lib → core}/store/sessions.d.ts +0 -0
  175. /package/dist/{lib → core}/store/steps.d.ts +0 -0
  176. /package/dist/{lib → core}/store/steps.js +0 -0
  177. /package/dist/{lib → core}/store/tasks.d.ts +0 -0
  178. /package/dist/{lib → core}/store/tasks.js +0 -0
  179. /package/dist/{lib → core}/store/types.js +0 -0
  180. /package/dist/{proxy/action-parser.js → integrations/proxy/agents/claude/parser.js} +0 -0
  181. /package/dist/{lib → integrations/proxy/agents/claude}/settings.d.ts +0 -0
  182. /package/dist/{lib → integrations/proxy/agents/claude}/settings.js +0 -0
  183. /package/dist/{proxy → integrations/proxy/cache}/extended-cache.d.ts +0 -0
  184. /package/dist/{proxy → integrations/proxy}/config.d.ts +0 -0
  185. /package/dist/{proxy → integrations/proxy}/index.d.ts +0 -0
  186. /package/dist/{proxy → integrations/proxy}/types.js +0 -0
  187. /package/dist/{lib → utils}/debug.d.ts +0 -0
  188. /package/dist/{lib → utils}/debug.js +0 -0
  189. /package/dist/{lib → utils}/utils.d.ts +0 -0
  190. /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 {};