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,115 @@
1
+ // Track which Antigravity sessions have been synced to cloud
2
+ // File: ~/.grov/antigravity_synced.json
3
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
4
+ import { homedir } from 'os';
5
+ import { join } from 'path';
6
+ const GROV_DIR = join(homedir(), '.grov');
7
+ const SYNCED_FILE = join(GROV_DIR, 'antigravity_synced.json');
8
+ // ─────────────────────────────────────────────────────────────
9
+ // File Operations
10
+ // ─────────────────────────────────────────────────────────────
11
+ function ensureDir() {
12
+ if (!existsSync(GROV_DIR)) {
13
+ mkdirSync(GROV_DIR, { recursive: true });
14
+ }
15
+ }
16
+ function readState() {
17
+ if (!existsSync(SYNCED_FILE)) {
18
+ return { entries: [] };
19
+ }
20
+ try {
21
+ const content = readFileSync(SYNCED_FILE, 'utf-8');
22
+ return JSON.parse(content);
23
+ }
24
+ catch {
25
+ return { entries: [] };
26
+ }
27
+ }
28
+ function writeState(state) {
29
+ ensureDir();
30
+ writeFileSync(SYNCED_FILE, JSON.stringify(state, null, 2));
31
+ }
32
+ // ─────────────────────────────────────────────────────────────
33
+ // Hash Helper
34
+ // ─────────────────────────────────────────────────────────────
35
+ /**
36
+ * Simple hash for detecting plan content changes
37
+ */
38
+ export function hashContent(content) {
39
+ let hash = 0;
40
+ for (let i = 0; i < content.length; i++) {
41
+ const char = content.charCodeAt(i);
42
+ hash = ((hash << 5) - hash) + char;
43
+ hash = hash & hash; // Convert to 32bit integer
44
+ }
45
+ return hash.toString(16);
46
+ }
47
+ // ─────────────────────────────────────────────────────────────
48
+ // Sync Tracking
49
+ // ─────────────────────────────────────────────────────────────
50
+ /**
51
+ * Check if a session has been synced
52
+ */
53
+ export function isSynced(sessionId) {
54
+ const state = readState();
55
+ return state.entries.some(e => e.sessionId === sessionId);
56
+ }
57
+ /**
58
+ * Check if a session needs update (plan content changed)
59
+ */
60
+ export function needsUpdate(sessionId, planContent) {
61
+ const state = readState();
62
+ const entry = state.entries.find(e => e.sessionId === sessionId);
63
+ if (!entry)
64
+ return true; // Not synced yet
65
+ const currentHash = hashContent(planContent);
66
+ return entry.planHash !== currentHash;
67
+ }
68
+ /**
69
+ * Mark a session as synced
70
+ */
71
+ export function markSynced(sessionId, planContent) {
72
+ const state = readState();
73
+ const existingIdx = state.entries.findIndex(e => e.sessionId === sessionId);
74
+ const entry = {
75
+ sessionId,
76
+ syncedAt: new Date().toISOString(),
77
+ planHash: planContent ? hashContent(planContent) : undefined,
78
+ };
79
+ if (existingIdx >= 0) {
80
+ state.entries[existingIdx] = entry;
81
+ }
82
+ else {
83
+ state.entries.push(entry);
84
+ }
85
+ writeState(state);
86
+ }
87
+ /**
88
+ * Get all synced session IDs
89
+ */
90
+ export function getSyncedSessionIds() {
91
+ const state = readState();
92
+ return state.entries.map(e => e.sessionId);
93
+ }
94
+ /**
95
+ * Get sessions that haven't been synced yet
96
+ */
97
+ export function getUnsynced(allSessionIds) {
98
+ const synced = new Set(getSyncedSessionIds());
99
+ return allSessionIds.filter(id => !synced.has(id));
100
+ }
101
+ /**
102
+ * Remove old entries to keep file size manageable
103
+ * Keeps most recent 500 entries
104
+ */
105
+ export function pruneOldEntries(keepCount = 500) {
106
+ const state = readState();
107
+ if (state.entries.length <= keepCount)
108
+ return;
109
+ // Sort by syncedAt descending and keep most recent
110
+ state.entries.sort((a, b) => {
111
+ return new Date(b.syncedAt).getTime() - new Date(a.syncedAt).getTime();
112
+ });
113
+ state.entries = state.entries.slice(0, keepCount);
114
+ writeState(state);
115
+ }
@@ -0,0 +1,18 @@
1
+ export interface Turn {
2
+ usageUuid: string;
3
+ agentId: string;
4
+ userPrompt: string;
5
+ assistantTexts: string[];
6
+ reasoningBlocks: string[];
7
+ toolCalls: Array<{
8
+ toolName: string;
9
+ args: Record<string, unknown>;
10
+ }>;
11
+ projectPath: string;
12
+ model: string;
13
+ isComplete: boolean;
14
+ }
15
+ /**
16
+ * Main polling function - captures all completed turns from all CLI databases
17
+ */
18
+ export declare function pollAndCaptureAll(): Promise<void>;
@@ -0,0 +1,258 @@
1
+ // CLI Extractor - Parse SQLite blobs, identify turns, extract conversation data
2
+ // Cursor CLI stores conversations in protobuf-like blobs with embedded JSON
3
+ import Database from 'better-sqlite3';
4
+ import { findAllCLIDatabases, getConnectTime } from './cli-watcher.js';
5
+ import { isAlreadyCaptured, markAsCaptured, cleanupOldChats } from './cli-synced.js';
6
+ import { transformToApiFormat, postToApi } from './cli-transform.js';
7
+ /**
8
+ * Main polling function - captures all completed turns from all CLI databases
9
+ */
10
+ export async function pollAndCaptureAll() {
11
+ const databases = findAllCLIDatabases();
12
+ if (databases.length === 0)
13
+ return;
14
+ const startTime = getConnectTime();
15
+ for (const { dbPath, agentId, mtime } of databases) {
16
+ // Skip DBs not modified since MCP connect
17
+ if (mtime < startTime)
18
+ continue;
19
+ try {
20
+ await captureFromDatabase(dbPath, agentId);
21
+ }
22
+ catch {
23
+ // Ignore individual database errors
24
+ }
25
+ }
26
+ cleanupOldChats();
27
+ }
28
+ /**
29
+ * Capture turns from a single CLI database
30
+ */
31
+ async function captureFromDatabase(dbPath, agentId) {
32
+ const db = new Database(dbPath, { readonly: true });
33
+ try {
34
+ const meta = getMetaData(db);
35
+ if (!meta)
36
+ return;
37
+ const orderedBlobIds = parseRootBlob(db, meta.latestRootBlobId);
38
+ if (orderedBlobIds.length === 0)
39
+ return;
40
+ const turns = identifyAllTurns(db, orderedBlobIds, meta);
41
+ const completedTurns = turns.filter(turn => turn.isComplete);
42
+ const newTurns = completedTurns.filter(turn => !isAlreadyCaptured(meta.agentId, turn.usageUuid));
43
+ if (newTurns.length === 0)
44
+ return;
45
+ for (const turn of newTurns) {
46
+ try {
47
+ const success = await postToApi(transformToApiFormat(turn, meta));
48
+ if (success) {
49
+ markAsCaptured(meta.agentId, turn.usageUuid);
50
+ }
51
+ }
52
+ catch {
53
+ // Ignore individual capture errors
54
+ }
55
+ }
56
+ }
57
+ finally {
58
+ db.close();
59
+ }
60
+ }
61
+ /**
62
+ * Get meta data from CLI database
63
+ */
64
+ function getMetaData(db) {
65
+ try {
66
+ const row = db.prepare("SELECT value FROM meta WHERE key='0'").get();
67
+ if (!row)
68
+ return null;
69
+ const json = Buffer.from(row.value, 'hex').toString('utf8');
70
+ return JSON.parse(json);
71
+ }
72
+ catch {
73
+ return null;
74
+ }
75
+ }
76
+ /**
77
+ * Parse root blob to get ordered list of child blob IDs
78
+ * Root blob contains protobuf-encoded list with 0a20 markers followed by 32-byte hashes
79
+ */
80
+ function parseRootBlob(db, rootId) {
81
+ try {
82
+ const row = db.prepare("SELECT data FROM blobs WHERE id=?").get(rootId);
83
+ if (!row)
84
+ return [];
85
+ const data = row.data;
86
+ const ids = [];
87
+ for (let i = 0; i < data.length - 33; i++) {
88
+ if (data[i] === 0x0a && data[i + 1] === 0x20) {
89
+ const hash = data.slice(i + 2, i + 34).toString('hex');
90
+ ids.push(hash);
91
+ i += 33;
92
+ }
93
+ }
94
+ return ids;
95
+ }
96
+ catch {
97
+ return [];
98
+ }
99
+ }
100
+ /**
101
+ * Parse a single blob to extract message data
102
+ * CLI blobs are JSON with structure: {"role":"...", "content": string | ContentBlock[]}
103
+ */
104
+ function parseBlob(db, blobId) {
105
+ try {
106
+ const row = db.prepare("SELECT data FROM blobs WHERE id=?").get(blobId);
107
+ if (!row)
108
+ return null;
109
+ const dataStr = row.data.toString('utf8');
110
+ // Skip non-JSON blobs (protobuf/binary)
111
+ if (!dataStr.startsWith('{'))
112
+ return null;
113
+ let json;
114
+ try {
115
+ json = JSON.parse(dataStr);
116
+ }
117
+ catch {
118
+ return null; // Not valid JSON
119
+ }
120
+ // Must have role (user, assistant, system - skip 'tool')
121
+ if (!json.role || json.role === 'tool')
122
+ return null;
123
+ if (json.role !== 'user' && json.role !== 'assistant' && json.role !== 'system')
124
+ return null;
125
+ const content = [];
126
+ // Handle content field - can be string or array
127
+ if (typeof json.content === 'string') {
128
+ // System/context messages have content as string
129
+ content.push({ type: 'text', text: json.content });
130
+ }
131
+ else if (Array.isArray(json.content)) {
132
+ // User queries and assistant responses have content as array
133
+ for (const block of json.content) {
134
+ if (block.type === 'text' && block.text) {
135
+ content.push({ type: 'text', text: block.text });
136
+ }
137
+ else if (block.type === 'reasoning' && block.text) {
138
+ content.push({ type: 'reasoning', text: block.text });
139
+ }
140
+ else if (block.type === 'tool-call' && block.toolName) {
141
+ content.push({
142
+ type: 'tool-call',
143
+ toolName: block.toolName,
144
+ args: block.args || {}
145
+ });
146
+ }
147
+ }
148
+ }
149
+ // Extract requestId from providerOptions
150
+ const requestId = json.providerOptions?.cursor?.requestId;
151
+ return {
152
+ role: json.role,
153
+ content,
154
+ requestId
155
+ };
156
+ }
157
+ catch {
158
+ return null;
159
+ }
160
+ }
161
+ /**
162
+ * Identify all turns from ordered blob IDs
163
+ * A turn starts with a user message and includes all following assistant messages
164
+ */
165
+ function identifyAllTurns(db, orderedBlobIds, meta) {
166
+ const turns = [];
167
+ let currentTurn = null;
168
+ for (const blobId of orderedBlobIds) {
169
+ const message = parseBlob(db, blobId);
170
+ if (!message)
171
+ continue;
172
+ if (message.role === 'user' && hasTextContent(message)) {
173
+ // New turn starts
174
+ if (currentTurn && currentTurn.usageUuid) {
175
+ // Finalize previous turn
176
+ currentTurn.isComplete = isTurnComplete(currentTurn);
177
+ turns.push(currentTurn);
178
+ }
179
+ // Start new turn
180
+ currentTurn = {
181
+ usageUuid: message.requestId || blobId,
182
+ agentId: meta.agentId,
183
+ userPrompt: extractTextContent(message),
184
+ assistantTexts: [],
185
+ reasoningBlocks: [],
186
+ toolCalls: [],
187
+ projectPath: extractProjectPath(message) || '',
188
+ model: meta.lastUsedModel || 'unknown',
189
+ isComplete: false
190
+ };
191
+ }
192
+ else if (currentTurn && message.role === 'assistant') {
193
+ // Add to current turn
194
+ for (const block of message.content) {
195
+ if (block.type === 'text' && block.text) {
196
+ currentTurn.assistantTexts.push(block.text);
197
+ }
198
+ else if (block.type === 'reasoning' && block.text) {
199
+ currentTurn.reasoningBlocks.push(block.text);
200
+ }
201
+ else if (block.type === 'tool-call' && block.toolName) {
202
+ currentTurn.toolCalls.push({
203
+ toolName: block.toolName,
204
+ args: block.args || {}
205
+ });
206
+ }
207
+ }
208
+ }
209
+ }
210
+ // Don't forget last turn
211
+ if (currentTurn && currentTurn.usageUuid) {
212
+ currentTurn.isComplete = isTurnComplete(currentTurn);
213
+ turns.push(currentTurn);
214
+ }
215
+ return turns;
216
+ }
217
+ /**
218
+ * Check if message has text content
219
+ */
220
+ function hasTextContent(message) {
221
+ return message.content.some(c => c.type === 'text' && c.text && c.text.length > 0);
222
+ }
223
+ /**
224
+ * Extract text content from message
225
+ */
226
+ function extractTextContent(message) {
227
+ return message.content
228
+ .filter(c => c.type === 'text' && c.text)
229
+ .map(c => c.text)
230
+ .join('\n');
231
+ }
232
+ /**
233
+ * Extract project path from user context message
234
+ * Looks for "Workspace Path: /path/to/project" pattern
235
+ */
236
+ function extractProjectPath(message) {
237
+ const fullText = message.content
238
+ .filter(c => c.type === 'text' && c.text)
239
+ .map(c => c.text)
240
+ .join('\n');
241
+ const match = fullText.match(/Workspace Path:\s*([^\n]+)/);
242
+ return match?.[1]?.trim() || null;
243
+ }
244
+ /**
245
+ * Check if turn is complete
246
+ * Complete = has text response AND no pending tool calls in final state
247
+ *
248
+ * Key insight: By LLM API design, if assistant has text AND wants to call tools,
249
+ * both MUST be in the SAME message. So text without tool-call = turn complete.
250
+ */
251
+ function isTurnComplete(turn) {
252
+ // Must have assistant text response
253
+ if (!turn.assistantTexts || turn.assistantTexts.length === 0) {
254
+ return false;
255
+ }
256
+ // If has text, turn is complete (tool calls would be in same message)
257
+ return true;
258
+ }
@@ -0,0 +1,4 @@
1
+ export declare function isAlreadyCaptured(agentId: string, usageUuid: string): boolean;
2
+ export declare function markAsCaptured(agentId: string, usageUuid: string): void;
3
+ export declare function cleanupOldChats(): void;
4
+ export declare function getCapturedForAgent(agentId: string): string[];
@@ -0,0 +1,62 @@
1
+ // CLI Sync Tracker - Deduplication + 6h cleanup
2
+ // File: ~/.grov/cli_synced.json
3
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
4
+ import { homedir } from 'os';
5
+ import { join } from 'path';
6
+ const GROV_DIR = join(homedir(), '.grov');
7
+ const SYNCED_FILE = join(GROV_DIR, 'cli_synced.json');
8
+ const SIX_HOURS = 6 * 60 * 60 * 1000;
9
+ function ensureDir() {
10
+ if (!existsSync(GROV_DIR)) {
11
+ mkdirSync(GROV_DIR, { recursive: true });
12
+ }
13
+ }
14
+ function readSyncedFile() {
15
+ if (!existsSync(SYNCED_FILE)) {
16
+ return { chats: {} };
17
+ }
18
+ try {
19
+ return JSON.parse(readFileSync(SYNCED_FILE, 'utf-8'));
20
+ }
21
+ catch {
22
+ return { chats: {} };
23
+ }
24
+ }
25
+ function writeSyncedFile(data) {
26
+ ensureDir();
27
+ writeFileSync(SYNCED_FILE, JSON.stringify(data, null, 2));
28
+ }
29
+ export function isAlreadyCaptured(agentId, usageUuid) {
30
+ const synced = readSyncedFile();
31
+ return synced.chats[agentId]?.captured.includes(usageUuid) || false;
32
+ }
33
+ export function markAsCaptured(agentId, usageUuid) {
34
+ const synced = readSyncedFile();
35
+ if (!synced.chats[agentId]) {
36
+ synced.chats[agentId] = { captured: [], lastActivity: '' };
37
+ }
38
+ if (!synced.chats[agentId].captured.includes(usageUuid)) {
39
+ synced.chats[agentId].captured.push(usageUuid);
40
+ }
41
+ synced.chats[agentId].lastActivity = new Date().toISOString();
42
+ writeSyncedFile(synced);
43
+ }
44
+ export function cleanupOldChats() {
45
+ const synced = readSyncedFile();
46
+ const cutoff = Date.now() - SIX_HOURS;
47
+ let changed = false;
48
+ for (const [agentId, data] of Object.entries(synced.chats)) {
49
+ if (new Date(data.lastActivity).getTime() < cutoff) {
50
+ delete synced.chats[agentId];
51
+ changed = true;
52
+ }
53
+ }
54
+ if (changed) {
55
+ writeSyncedFile(synced);
56
+ }
57
+ }
58
+ // Get all captured usageUuids for an agent (for debugging)
59
+ export function getCapturedForAgent(agentId) {
60
+ const synced = readSyncedFile();
61
+ return synced.chats[agentId]?.captured || [];
62
+ }
@@ -0,0 +1,30 @@
1
+ import type { Turn } from './cli-extractor.js';
2
+ interface ExtractPayload {
3
+ composerId: string;
4
+ usageUuid: string;
5
+ mode: 'ask' | 'plan' | 'agent';
6
+ projectPath: string;
7
+ original_query: string;
8
+ text: string;
9
+ thinking: string;
10
+ toolCalls: Array<{
11
+ name: string;
12
+ params: Record<string, unknown>;
13
+ }>;
14
+ }
15
+ interface MetaData {
16
+ agentId: string;
17
+ lastUsedModel: string;
18
+ mode?: string;
19
+ }
20
+ export declare function transformToApiFormat(turn: Turn, meta: MetaData): ExtractPayload;
21
+ /**
22
+ * Post extracted turn to API
23
+ * Returns true on success, false on failure
24
+ */
25
+ export declare function postToApi(payload: ExtractPayload): Promise<boolean>;
26
+ /**
27
+ * Check if CLI capture is enabled (sync enabled + team ID set)
28
+ */
29
+ export declare function isCLICaptureEnabled(): boolean;
30
+ export {};
@@ -0,0 +1,62 @@
1
+ // CLI Transform - Convert CLI Turn data to API format and POST to extraction endpoint
2
+ // Reuses same /cursor/extract endpoint as IDE capture
3
+ import { request } from 'undici';
4
+ import { getAccessToken, getSyncStatus } from '../../../core/cloud/credentials.js';
5
+ const API_URL = process.env.GROV_API_URL || 'https://api.grov.dev';
6
+ /**
7
+ * Transform CLI Turn to API ExtractPayload format
8
+ * CLI always uses 'agent' mode (no ask/plan distinction in CLI)
9
+ */
10
+ function stripUserQueryTags(text) {
11
+ return text.replace(/<\/?user_query>/g, '').trim();
12
+ }
13
+ export function transformToApiFormat(turn, meta) {
14
+ return {
15
+ composerId: meta.agentId,
16
+ usageUuid: turn.usageUuid,
17
+ mode: 'agent', // CLI always agent mode
18
+ projectPath: turn.projectPath || 'unknown',
19
+ original_query: stripUserQueryTags(turn.userPrompt),
20
+ text: turn.assistantTexts.join('\n'),
21
+ thinking: turn.reasoningBlocks.join('\n\n'),
22
+ toolCalls: turn.toolCalls.map(tc => ({
23
+ name: tc.toolName,
24
+ params: tc.args
25
+ }))
26
+ };
27
+ }
28
+ /**
29
+ * Post extracted turn to API
30
+ * Returns true on success, false on failure
31
+ */
32
+ export async function postToApi(payload) {
33
+ const syncStatus = getSyncStatus();
34
+ if (!syncStatus?.enabled || !syncStatus.teamId)
35
+ return false;
36
+ const token = await getAccessToken();
37
+ if (!token)
38
+ return false;
39
+ const teamId = syncStatus.teamId;
40
+ const url = `${API_URL}/teams/${teamId}/cursor/extract`;
41
+ try {
42
+ const res = await request(url, {
43
+ method: 'POST',
44
+ headers: {
45
+ 'Content-Type': 'application/json',
46
+ 'Authorization': `Bearer ${token}`,
47
+ },
48
+ body: JSON.stringify(payload),
49
+ });
50
+ return res.statusCode === 200;
51
+ }
52
+ catch {
53
+ return false;
54
+ }
55
+ }
56
+ /**
57
+ * Check if CLI capture is enabled (sync enabled + team ID set)
58
+ */
59
+ export function isCLICaptureEnabled() {
60
+ const syncStatus = getSyncStatus();
61
+ return syncStatus?.enabled === true && !!syncStatus.teamId;
62
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Start CLI capture polling
3
+ * Returns cleanup function to stop polling
4
+ */
5
+ export declare function startCLICapture(pollAndCapture: () => Promise<void>): () => void;
6
+ export declare function getConnectTime(): number;
7
+ /**
8
+ * Find all CLI databases (not just most recent)
9
+ * Returns array of { dbPath, agentId, mtime }
10
+ */
11
+ export declare function findAllCLIDatabases(): Array<{
12
+ dbPath: string;
13
+ workspaceHash: string;
14
+ agentId: string;
15
+ mtime: number;
16
+ }>;
17
+ /**
18
+ * Find most recently modified CLI database
19
+ */
20
+ export declare function findMostRecentCLIDatabase(): {
21
+ dbPath: string;
22
+ agentId: string;
23
+ } | null;
24
+ /**
25
+ * Check if CLI chats directory exists
26
+ */
27
+ export declare function cliChatsExist(): boolean;
28
+ /**
29
+ * Get the CLI chats path (for debugging/logging)
30
+ */
31
+ export declare function getCLIChatsPath(): string;
@@ -0,0 +1,106 @@
1
+ // CLI Watcher - Polling orchestration for Cursor CLI capture
2
+ // Polls every 3 minutes while MCP connection is active
3
+ import { homedir } from 'os';
4
+ import { join } from 'path';
5
+ import { readdirSync, existsSync, statSync } from 'fs';
6
+ const CLI_CHATS_PATH = join(homedir(), '.cursor', 'chats');
7
+ const POLL_INTERVAL = 3 * 60 * 1000; // 3 minutes
8
+ let pollingInterval = null;
9
+ let pollFunction = null;
10
+ let connectTime = 0;
11
+ /**
12
+ * Start CLI capture polling
13
+ * Returns cleanup function to stop polling
14
+ */
15
+ export function startCLICapture(pollAndCapture) {
16
+ pollFunction = pollAndCapture;
17
+ connectTime = Date.now();
18
+ // Initial poll (delayed slightly to let MCP fully connect)
19
+ setTimeout(() => {
20
+ pollAndCapture().catch(() => { });
21
+ }, 5000);
22
+ // Start interval
23
+ pollingInterval = setInterval(() => {
24
+ pollAndCapture().catch(() => { });
25
+ }, POLL_INTERVAL);
26
+ // Return cleanup function
27
+ return () => {
28
+ if (pollFunction) {
29
+ pollFunction().catch(() => { });
30
+ }
31
+ if (pollingInterval) {
32
+ clearInterval(pollingInterval);
33
+ pollingInterval = null;
34
+ }
35
+ pollFunction = null;
36
+ };
37
+ }
38
+ export function getConnectTime() {
39
+ return connectTime;
40
+ }
41
+ /**
42
+ * Find all CLI databases (not just most recent)
43
+ * Returns array of { dbPath, agentId, mtime }
44
+ */
45
+ export function findAllCLIDatabases() {
46
+ if (!existsSync(CLI_CHATS_PATH)) {
47
+ return [];
48
+ }
49
+ const databases = [];
50
+ try {
51
+ for (const workspaceHash of readdirSync(CLI_CHATS_PATH)) {
52
+ const wsPath = join(CLI_CHATS_PATH, workspaceHash);
53
+ // Skip if not a directory
54
+ try {
55
+ if (!statSync(wsPath).isDirectory())
56
+ continue;
57
+ }
58
+ catch {
59
+ continue;
60
+ }
61
+ for (const agentId of readdirSync(wsPath)) {
62
+ const dbPath = join(wsPath, agentId, 'store.db');
63
+ if (existsSync(dbPath)) {
64
+ try {
65
+ const stat = statSync(dbPath);
66
+ databases.push({
67
+ dbPath,
68
+ workspaceHash,
69
+ agentId,
70
+ mtime: stat.mtimeMs
71
+ });
72
+ }
73
+ catch {
74
+ // Skip inaccessible files
75
+ }
76
+ }
77
+ }
78
+ }
79
+ }
80
+ catch {
81
+ // Ignore scan errors
82
+ }
83
+ // Sort by most recent first
84
+ return databases.sort((a, b) => b.mtime - a.mtime);
85
+ }
86
+ /**
87
+ * Find most recently modified CLI database
88
+ */
89
+ export function findMostRecentCLIDatabase() {
90
+ const all = findAllCLIDatabases();
91
+ if (all.length === 0)
92
+ return null;
93
+ return { dbPath: all[0].dbPath, agentId: all[0].agentId };
94
+ }
95
+ /**
96
+ * Check if CLI chats directory exists
97
+ */
98
+ export function cliChatsExist() {
99
+ return existsSync(CLI_CHATS_PATH);
100
+ }
101
+ /**
102
+ * Get the CLI chats path (for debugging/logging)
103
+ */
104
+ export function getCLIChatsPath() {
105
+ return CLI_CHATS_PATH;
106
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};