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,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
|
+
}
|