grov 0.2.2 → 0.5.2

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/README.md CHANGED
@@ -14,8 +14,9 @@
14
14
 
15
15
  <p align="center">
16
16
  <a href="https://grov.dev">Website</a> •
17
+ <a href="https://app.grov.dev">Dashboard</a> •
17
18
  <a href="#quick-start">Quick Start</a> •
18
- <a href="#advanced-features">Advanced</a> •
19
+ <a href="#team-sync">Team Sync</a> •
19
20
  <a href="#contributing">Contributing</a>
20
21
  </p>
21
22
 
@@ -35,9 +36,15 @@ Every time you start a new Claude Code session:
35
36
 
36
37
  Grov captures what Claude learns and injects it back on the next session.
37
38
 
38
- **With grov:** Same task takes ~1-2 minutes, <2% tokens, 0 explore agents. Claude reads files directly because it already has context.
39
+ ![grov status](docs/grov-status.jpeg)
39
40
 
40
- <sub>*Based on controlled testing: Auth file modification task without grov launched 3+ subagents and read ~10 files for exploration. With grov (after initial memory capture), an adjacent task went directly to reading relevant files with full context.</sub>
41
+ ### What Gets Captured
42
+
43
+ Real reasoning, not just file lists:
44
+
45
+ ![captured reasoning](docs/reasoning-output.jpeg)
46
+
47
+ *Architectural decisions, patterns, and rationale - automatically extracted.*
41
48
 
42
49
  ## Quick Start
43
50
 
@@ -71,6 +78,8 @@ grov init # Configure proxy URL (one-time)
71
78
  grov proxy # Start the proxy (required)
72
79
  grov proxy-status # Show active sessions
73
80
  grov status # Show captured tasks
81
+ grov login # Login to cloud dashboard
82
+ grov sync # Sync memories to team dashboard
74
83
  grov disable # Disable grov
75
84
  ```
76
85
 
@@ -78,7 +87,24 @@ grov disable # Disable grov
78
87
 
79
88
  - **Database:** `~/.grov/memory.db` (SQLite)
80
89
  - **Per-project:** Context is filtered by project path
81
- - **Local only:** Nothing leaves your machine
90
+ - **Local by default:** Memories stay on your machine unless you enable team sync
91
+
92
+ ## Team Sync
93
+
94
+ Share memories across your engineering team with the cloud dashboard.
95
+
96
+ ```bash
97
+ grov login # Authenticate via GitHub
98
+ grov sync --enable --team ID # Enable sync for a team
99
+ ```
100
+
101
+ Once enabled, memories automatically sync to [app.grov.dev](https://app.grov.dev) where your team can:
102
+ - Browse all captured reasoning
103
+ - Search across sessions
104
+ - Invite team members
105
+ - See who learned what
106
+
107
+ Memories sync automatically when sessions complete - no manual intervention needed.
82
108
 
83
109
  ## Requirements
84
110
 
@@ -163,9 +189,10 @@ YOU MAY SKIP EXPLORE AGENTS for files mentioned above.
163
189
  - [x] LLM-powered extraction
164
190
  - [x] Local proxy with real-time monitoring
165
191
  - [x] Anti-drift detection & correction
166
- - [ ] Team sync (cloud backend)
167
- - [ ] Web dashboard
192
+ - [x] Team sync (cloud backend)
193
+ - [x] Web dashboard
168
194
  - [ ] Semantic search
195
+ - [ ] VS Code extension
169
196
 
170
197
  ## Contributing
171
198
 
package/dist/cli.js CHANGED
@@ -77,9 +77,10 @@ program
77
77
  program
78
78
  .command('proxy')
79
79
  .description('Start the Grov proxy server (intercepts Claude API calls)')
80
- .action(async () => {
80
+ .option('-d, --debug', 'Enable debug logging to grov-proxy.log')
81
+ .action(async (options) => {
81
82
  const { startServer } = await import('./proxy/server.js');
82
- await startServer();
83
+ await startServer({ debug: options.debug ?? false });
83
84
  });
84
85
  // grov proxy-status - Show active proxy sessions
85
86
  program
@@ -89,4 +90,33 @@ program
89
90
  const { proxyStatus } = await import('./commands/proxy-status.js');
90
91
  await proxyStatus();
91
92
  }));
93
+ // grov login - Authenticate with Grov cloud
94
+ program
95
+ .command('login')
96
+ .description('Login to Grov cloud (opens browser for authentication)')
97
+ .action(safeAction(async () => {
98
+ const { login } = await import('./commands/login.js');
99
+ await login();
100
+ }));
101
+ // grov logout - Clear stored credentials
102
+ program
103
+ .command('logout')
104
+ .description('Logout from Grov cloud')
105
+ .action(safeAction(async () => {
106
+ const { logout } = await import('./commands/logout.js');
107
+ await logout();
108
+ }));
109
+ // grov sync - Configure cloud sync
110
+ program
111
+ .command('sync')
112
+ .description('Configure cloud sync to team dashboard')
113
+ .option('--enable', 'Enable cloud sync')
114
+ .option('--disable', 'Disable cloud sync')
115
+ .option('--team <id>', 'Set team ID for sync')
116
+ .option('--status', 'Show sync status')
117
+ .option('--push', 'Upload any unsynced local tasks to the team')
118
+ .action(safeAction(async (options) => {
119
+ const { sync } = await import('./commands/sync.js');
120
+ await sync(options);
121
+ }));
92
122
  program.parse();
@@ -0,0 +1 @@
1
+ export declare function login(): Promise<void>;
@@ -0,0 +1,115 @@
1
+ // Login command - Device authorization flow
2
+ // Authenticates CLI with Grov cloud using OAuth-like device flow
3
+ import open from 'open';
4
+ import { writeCredentials, isAuthenticated, readCredentials } from '../lib/credentials.js';
5
+ import { startDeviceFlow, pollDeviceFlow, sleep } from '../lib/api-client.js';
6
+ /**
7
+ * Decode JWT payload to extract user info
8
+ */
9
+ function decodeTokenPayload(token) {
10
+ try {
11
+ const parts = token.split('.');
12
+ if (parts.length !== 3)
13
+ return null;
14
+ const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf-8'));
15
+ return {
16
+ sub: payload.sub,
17
+ email: payload.email,
18
+ };
19
+ }
20
+ catch {
21
+ return null;
22
+ }
23
+ }
24
+ export async function login() {
25
+ console.log('Logging in to Grov cloud...\n');
26
+ // Check if already authenticated
27
+ if (isAuthenticated()) {
28
+ const creds = readCredentials();
29
+ if (creds) {
30
+ console.log(`Already logged in as ${creds.email}`);
31
+ console.log('Run "grov logout" to log out first.\n');
32
+ return;
33
+ }
34
+ }
35
+ // Start device flow
36
+ console.log('Starting device authorization...');
37
+ const startResult = await startDeviceFlow();
38
+ if (startResult.error || !startResult.data) {
39
+ console.error(`\nError: ${startResult.error || 'Failed to start login flow'}`);
40
+ console.error('Please check your network connection and try again.');
41
+ process.exit(1);
42
+ }
43
+ const { device_code, user_code, verification_uri, expires_in, interval } = startResult.data;
44
+ // Display code to user
45
+ console.log('\n┌─────────────────────────────────────────┐');
46
+ console.log('│ │');
47
+ console.log(`│ Your code: ${user_code} │`);
48
+ console.log('│ │');
49
+ console.log('└─────────────────────────────────────────┘\n');
50
+ console.log('Opening browser to authorize...');
51
+ console.log(`If browser does not open, visit: ${verification_uri}?code=${user_code}\n`);
52
+ // Open browser
53
+ try {
54
+ await open(`${verification_uri}?code=${user_code}`);
55
+ }
56
+ catch {
57
+ console.log('Could not open browser automatically.');
58
+ console.log(`Please visit: ${verification_uri}?code=${user_code}\n`);
59
+ }
60
+ // Poll for authorization
61
+ console.log('Waiting for authorization...');
62
+ const maxAttempts = Math.floor(expires_in / interval);
63
+ let attempts = 0;
64
+ while (attempts < maxAttempts) {
65
+ await sleep(interval * 1000);
66
+ attempts++;
67
+ const pollResult = await pollDeviceFlow(device_code);
68
+ if (pollResult.error) {
69
+ process.stdout.write('.');
70
+ continue;
71
+ }
72
+ if (!pollResult.data) {
73
+ process.stdout.write('.');
74
+ continue;
75
+ }
76
+ const { status, access_token, refresh_token, expires_at } = pollResult.data;
77
+ if (status === 'authorized' && access_token && refresh_token && expires_at) {
78
+ console.log('\n');
79
+ // Decode token to get user info
80
+ const userInfo = decodeTokenPayload(access_token);
81
+ if (!userInfo) {
82
+ console.error('Error: Failed to decode user info from token');
83
+ process.exit(1);
84
+ }
85
+ // Save credentials
86
+ writeCredentials({
87
+ access_token,
88
+ refresh_token,
89
+ expires_at,
90
+ user_id: userInfo.sub,
91
+ email: userInfo.email,
92
+ sync_enabled: false,
93
+ });
94
+ console.log('╔═════════════════════════════════════════╗');
95
+ console.log('║ ║');
96
+ console.log('║ Successfully logged in! ║');
97
+ console.log('║ ║');
98
+ console.log('╚═════════════════════════════════════════╝');
99
+ console.log(`\nLogged in as: ${userInfo.email}\n`);
100
+ console.log('Next steps:');
101
+ console.log(' 1. Run "grov sync --status" to check sync settings');
102
+ console.log(' 2. Run "grov sync --enable --team <team-id>" to enable sync');
103
+ console.log(' 3. Run "grov status" to view local memories\n');
104
+ return;
105
+ }
106
+ if (status === 'expired') {
107
+ console.log('\n\nAuthorization expired. Please run "grov login" again.\n');
108
+ process.exit(1);
109
+ }
110
+ // Still pending
111
+ process.stdout.write('.');
112
+ }
113
+ console.log('\n\nAuthorization timed out. Please run "grov login" again.\n');
114
+ process.exit(1);
115
+ }
@@ -0,0 +1 @@
1
+ export declare function logout(): Promise<void>;
@@ -0,0 +1,13 @@
1
+ // Logout command - Clear stored credentials
2
+ import { clearCredentials, readCredentials } from '../lib/credentials.js';
3
+ export async function logout() {
4
+ const creds = readCredentials();
5
+ if (!creds) {
6
+ console.log('Not currently logged in.\n');
7
+ return;
8
+ }
9
+ console.log(`Logging out ${creds.email}...`);
10
+ clearCredentials();
11
+ console.log('Successfully logged out.\n');
12
+ console.log('Run "grov login" to log in again.\n');
13
+ }
@@ -0,0 +1,8 @@
1
+ export interface SyncOptions {
2
+ enable?: boolean;
3
+ disable?: boolean;
4
+ team?: string;
5
+ status?: boolean;
6
+ push?: boolean;
7
+ }
8
+ export declare function sync(options: SyncOptions): Promise<void>;
@@ -0,0 +1,127 @@
1
+ // Sync command - Configure cloud sync settings
2
+ import { readCredentials, setTeamId, setSyncEnabled, getSyncStatus } from '../lib/credentials.js';
3
+ import { fetchTeams, getApiUrl } from '../lib/api-client.js';
4
+ import { syncTasks } from '../lib/cloud-sync.js';
5
+ import { getUnsyncedTasks, markTaskSynced, setTaskSyncError } from '../lib/store.js';
6
+ export async function sync(options) {
7
+ const creds = readCredentials();
8
+ if (!creds) {
9
+ console.log('Not logged in. Run "grov login" first.\n');
10
+ process.exit(1);
11
+ }
12
+ // Manual catch-up: push unsynced tasks for current project
13
+ if (options.push) {
14
+ const projectPath = process.cwd();
15
+ const unsynced = getUnsyncedTasks(projectPath);
16
+ const apiUrl = getApiUrl();
17
+ if (unsynced.length === 0) {
18
+ console.log('No unsynced tasks found for this project.\n');
19
+ return;
20
+ }
21
+ console.log(`Syncing ${unsynced.length} pending task(s) to the cloud via ${apiUrl}...\n`);
22
+ const result = await syncTasks(unsynced);
23
+ if (result.syncedIds.length > 0) {
24
+ for (const id of result.syncedIds) {
25
+ markTaskSynced(id);
26
+ }
27
+ }
28
+ if (result.failedIds.length > 0) {
29
+ const errorMessage = result.errors[0] || 'Sync failed';
30
+ for (const id of result.failedIds) {
31
+ setTaskSyncError(id, errorMessage);
32
+ }
33
+ }
34
+ console.log(`Synced: ${result.synced}, Failed: ${result.failed}`);
35
+ if (result.errors.length > 0) {
36
+ console.log('Errors:');
37
+ for (const err of result.errors) {
38
+ console.log(`- ${err}`);
39
+ }
40
+ }
41
+ console.log('');
42
+ return;
43
+ }
44
+ // Show status
45
+ if (options.status || (!options.enable && !options.disable && !options.team)) {
46
+ const syncStatus = getSyncStatus();
47
+ console.log('Cloud Sync Status\n');
48
+ console.log(` Logged in as: ${creds.email}`);
49
+ console.log(` Sync enabled: ${syncStatus?.enabled ? 'Yes' : 'No'}`);
50
+ console.log(` Team ID: ${syncStatus?.teamId || 'Not set'}`);
51
+ console.log(` API URL: ${getApiUrl()}`);
52
+ console.log('');
53
+ if (!syncStatus?.enabled) {
54
+ console.log('To enable sync:');
55
+ console.log(' grov sync --enable --team <team-id>\n');
56
+ console.log('To see available teams:');
57
+ console.log(' grov sync --enable\n');
58
+ }
59
+ return;
60
+ }
61
+ // Disable sync
62
+ if (options.disable) {
63
+ setSyncEnabled(false);
64
+ console.log('Cloud sync disabled.\n');
65
+ console.log('Local memories will no longer be uploaded to your team.\n');
66
+ return;
67
+ }
68
+ // Enable sync
69
+ if (options.enable) {
70
+ // Check if team is specified
71
+ if (options.team) {
72
+ setTeamId(options.team);
73
+ setSyncEnabled(true);
74
+ console.log(`Cloud sync enabled for team: ${options.team}\n`);
75
+ console.log('Your memories will now be uploaded to your team dashboard.\n');
76
+ return;
77
+ }
78
+ // No team specified - show available teams
79
+ const currentStatus = getSyncStatus();
80
+ if (currentStatus?.teamId) {
81
+ // Team already set, just enable
82
+ setSyncEnabled(true);
83
+ console.log(`Cloud sync enabled for team: ${currentStatus.teamId}\n`);
84
+ console.log('Your memories will now be uploaded to your team dashboard.\n');
85
+ return;
86
+ }
87
+ // Need to select a team
88
+ console.log('Fetching your teams...\n');
89
+ try {
90
+ const teams = await fetchTeams();
91
+ if (teams.length === 0) {
92
+ console.log('You are not a member of any teams.\n');
93
+ console.log('Create a team at https://app.grov.dev or ask your team admin for an invite.\n');
94
+ return;
95
+ }
96
+ console.log('Available teams:\n');
97
+ console.log(' ID Name');
98
+ console.log(' ───────────────────────────────────── ────────────────────');
99
+ for (const team of teams) {
100
+ console.log(` ${team.id} ${team.name}`);
101
+ }
102
+ console.log('');
103
+ console.log('Enable sync with:');
104
+ console.log(` grov sync --enable --team <team-id>\n`);
105
+ // If only one team, offer to use it
106
+ if (teams.length === 1) {
107
+ console.log(`Or, to use "${teams[0].name}":`);
108
+ console.log(` grov sync --enable --team ${teams[0].id}\n`);
109
+ }
110
+ }
111
+ catch (err) {
112
+ console.error(`Error fetching teams: ${err instanceof Error ? err.message : 'Unknown error'}`);
113
+ console.error('Please check your network connection and try again.\n');
114
+ process.exit(1);
115
+ }
116
+ return;
117
+ }
118
+ // Just setting team (without enable flag)
119
+ if (options.team) {
120
+ setTeamId(options.team);
121
+ console.log(`Team ID set to: ${options.team}\n`);
122
+ const syncStatus = getSyncStatus();
123
+ if (!syncStatus?.enabled) {
124
+ console.log('Note: Sync is not enabled. Run "grov sync --enable" to start syncing.\n');
125
+ }
126
+ }
127
+ }
@@ -0,0 +1,40 @@
1
+ import type { Team, MemorySyncRequest, MemorySyncResponse, DeviceFlowStartResponse, DeviceFlowPollResponse } from '@grov/shared';
2
+ export interface ApiResponse<T> {
3
+ data?: T;
4
+ error?: string;
5
+ status: number;
6
+ }
7
+ /**
8
+ * Make an authenticated API request
9
+ */
10
+ export declare function apiRequest<T>(method: 'GET' | 'POST' | 'PATCH' | 'DELETE', path: string, body?: unknown, options?: {
11
+ requireAuth?: boolean;
12
+ }): Promise<ApiResponse<T>>;
13
+ /**
14
+ * Start device authorization flow
15
+ */
16
+ export declare function startDeviceFlow(): Promise<ApiResponse<DeviceFlowStartResponse>>;
17
+ /**
18
+ * Poll for device authorization
19
+ */
20
+ export declare function pollDeviceFlow(deviceCode: string): Promise<ApiResponse<DeviceFlowPollResponse>>;
21
+ /**
22
+ * List user's teams
23
+ */
24
+ export declare function fetchTeams(): Promise<Team[]>;
25
+ /**
26
+ * Get team by ID
27
+ */
28
+ export declare function fetchTeam(teamId: string): Promise<Team>;
29
+ /**
30
+ * Sync memories to team
31
+ */
32
+ export declare function syncMemories(teamId: string, request: MemorySyncRequest): Promise<MemorySyncResponse>;
33
+ /**
34
+ * Sleep helper for polling
35
+ */
36
+ export declare function sleep(ms: number): Promise<void>;
37
+ /**
38
+ * Get API URL (for display)
39
+ */
40
+ export declare function getApiUrl(): string;
@@ -0,0 +1,117 @@
1
+ // HTTP client for Grov API calls
2
+ // Handles authentication, retries, and error handling
3
+ import { request } from 'undici';
4
+ import { getAccessToken } from './credentials.js';
5
+ // API configuration
6
+ const API_URL = process.env.GROV_API_URL || 'https://api.grov.dev';
7
+ /**
8
+ * Make an authenticated API request
9
+ */
10
+ export async function apiRequest(method, path, body, options) {
11
+ const headers = {
12
+ 'Content-Type': 'application/json',
13
+ };
14
+ // Add auth header if required (default: true)
15
+ if (options?.requireAuth !== false) {
16
+ const token = await getAccessToken();
17
+ if (!token) {
18
+ return {
19
+ error: 'Not authenticated. Please run: grov login',
20
+ status: 401,
21
+ };
22
+ }
23
+ headers['Authorization'] = `Bearer ${token}`;
24
+ }
25
+ try {
26
+ const response = await request(`${API_URL}${path}`, {
27
+ method,
28
+ headers,
29
+ body: body ? JSON.stringify(body) : undefined,
30
+ });
31
+ const responseBody = await response.body.text();
32
+ // Handle empty responses
33
+ if (!responseBody) {
34
+ return {
35
+ data: undefined,
36
+ status: response.statusCode,
37
+ };
38
+ }
39
+ const data = JSON.parse(responseBody);
40
+ if (response.statusCode >= 400) {
41
+ return {
42
+ error: data.error || data.message || 'Request failed',
43
+ status: response.statusCode,
44
+ };
45
+ }
46
+ return {
47
+ data: data,
48
+ status: response.statusCode,
49
+ };
50
+ }
51
+ catch (err) {
52
+ return {
53
+ error: err instanceof Error ? err.message : 'Network error',
54
+ status: 0,
55
+ };
56
+ }
57
+ }
58
+ // ============= Auth Endpoints (no auth required) =============
59
+ /**
60
+ * Start device authorization flow
61
+ */
62
+ export async function startDeviceFlow() {
63
+ return apiRequest('POST', '/auth/device', {}, {
64
+ requireAuth: false,
65
+ });
66
+ }
67
+ /**
68
+ * Poll for device authorization
69
+ */
70
+ export async function pollDeviceFlow(deviceCode) {
71
+ return apiRequest('POST', '/auth/device/poll', { device_code: deviceCode }, { requireAuth: false });
72
+ }
73
+ // ============= Team Endpoints =============
74
+ /**
75
+ * List user's teams
76
+ */
77
+ export async function fetchTeams() {
78
+ const response = await apiRequest('GET', '/teams');
79
+ if (response.error || !response.data) {
80
+ throw new Error(response.error || 'Failed to fetch teams');
81
+ }
82
+ return response.data.teams;
83
+ }
84
+ /**
85
+ * Get team by ID
86
+ */
87
+ export async function fetchTeam(teamId) {
88
+ const response = await apiRequest('GET', `/teams/${teamId}`);
89
+ if (response.error || !response.data) {
90
+ throw new Error(response.error || 'Failed to fetch team');
91
+ }
92
+ return response.data;
93
+ }
94
+ // ============= Memory Endpoints =============
95
+ /**
96
+ * Sync memories to team
97
+ */
98
+ export async function syncMemories(teamId, request) {
99
+ const response = await apiRequest('POST', `/teams/${teamId}/memories/sync`, request);
100
+ if (response.error || !response.data) {
101
+ throw new Error(response.error || 'Failed to sync memories');
102
+ }
103
+ return response.data;
104
+ }
105
+ // ============= Utility Functions =============
106
+ /**
107
+ * Sleep helper for polling
108
+ */
109
+ export function sleep(ms) {
110
+ return new Promise((resolve) => setTimeout(resolve, ms));
111
+ }
112
+ /**
113
+ * Get API URL (for display)
114
+ */
115
+ export function getApiUrl() {
116
+ return API_URL;
117
+ }
@@ -0,0 +1,33 @@
1
+ import type { CreateMemoryInput } from '@grov/shared';
2
+ import type { Task } from './store.js';
3
+ /**
4
+ * Convert local Task to CreateMemoryInput for API
5
+ */
6
+ export declare function taskToMemory(task: Task): CreateMemoryInput;
7
+ /**
8
+ * Check if sync is enabled and configured
9
+ */
10
+ export declare function isSyncEnabled(): boolean;
11
+ /**
12
+ * Get the configured team ID for sync
13
+ */
14
+ export declare function getSyncTeamId(): string | null;
15
+ /**
16
+ * Sync a single task to the cloud
17
+ * Called when a task is completed
18
+ */
19
+ export declare function syncTask(task: Task): Promise<boolean>;
20
+ /**
21
+ * Sync multiple tasks with batching and retry
22
+ */
23
+ export declare function syncTasks(tasks: Task[]): Promise<{
24
+ synced: number;
25
+ failed: number;
26
+ errors: string[];
27
+ syncedIds: string[];
28
+ failedIds: string[];
29
+ }>;
30
+ /**
31
+ * Get sync status summary
32
+ */
33
+ export declare function getSyncStatusSummary(): string;