grov 0.2.3 → 0.5.3

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 (64) hide show
  1. package/README.md +44 -5
  2. package/dist/cli.js +40 -2
  3. package/dist/commands/login.d.ts +1 -0
  4. package/dist/commands/login.js +115 -0
  5. package/dist/commands/logout.d.ts +1 -0
  6. package/dist/commands/logout.js +13 -0
  7. package/dist/commands/sync.d.ts +8 -0
  8. package/dist/commands/sync.js +127 -0
  9. package/dist/lib/api-client.d.ts +57 -0
  10. package/dist/lib/api-client.js +174 -0
  11. package/dist/lib/cloud-sync.d.ts +33 -0
  12. package/dist/lib/cloud-sync.js +176 -0
  13. package/dist/lib/credentials.d.ts +53 -0
  14. package/dist/lib/credentials.js +201 -0
  15. package/dist/lib/llm-extractor.d.ts +15 -39
  16. package/dist/lib/llm-extractor.js +400 -418
  17. package/dist/lib/store/convenience.d.ts +40 -0
  18. package/dist/lib/store/convenience.js +104 -0
  19. package/dist/lib/store/database.d.ts +22 -0
  20. package/dist/lib/store/database.js +375 -0
  21. package/dist/lib/store/drift.d.ts +9 -0
  22. package/dist/lib/store/drift.js +89 -0
  23. package/dist/lib/store/index.d.ts +7 -0
  24. package/dist/lib/store/index.js +13 -0
  25. package/dist/lib/store/sessions.d.ts +32 -0
  26. package/dist/lib/store/sessions.js +240 -0
  27. package/dist/lib/store/steps.d.ts +40 -0
  28. package/dist/lib/store/steps.js +161 -0
  29. package/dist/lib/store/tasks.d.ts +33 -0
  30. package/dist/lib/store/tasks.js +133 -0
  31. package/dist/lib/store/types.d.ts +167 -0
  32. package/dist/lib/store/types.js +2 -0
  33. package/dist/lib/store.d.ts +1 -406
  34. package/dist/lib/store.js +2 -1356
  35. package/dist/lib/utils.d.ts +5 -0
  36. package/dist/lib/utils.js +45 -0
  37. package/dist/proxy/action-parser.d.ts +10 -2
  38. package/dist/proxy/action-parser.js +4 -2
  39. package/dist/proxy/cache.d.ts +36 -0
  40. package/dist/proxy/cache.js +51 -0
  41. package/dist/proxy/config.d.ts +1 -0
  42. package/dist/proxy/config.js +2 -0
  43. package/dist/proxy/extended-cache.d.ts +10 -0
  44. package/dist/proxy/extended-cache.js +155 -0
  45. package/dist/proxy/forwarder.d.ts +7 -1
  46. package/dist/proxy/forwarder.js +157 -7
  47. package/dist/proxy/handlers/preprocess.d.ts +20 -0
  48. package/dist/proxy/handlers/preprocess.js +169 -0
  49. package/dist/proxy/injection/delta-tracking.d.ts +11 -0
  50. package/dist/proxy/injection/delta-tracking.js +93 -0
  51. package/dist/proxy/injection/injectors.d.ts +7 -0
  52. package/dist/proxy/injection/injectors.js +139 -0
  53. package/dist/proxy/request-processor.d.ts +18 -3
  54. package/dist/proxy/request-processor.js +151 -28
  55. package/dist/proxy/response-processor.js +116 -47
  56. package/dist/proxy/server.d.ts +4 -1
  57. package/dist/proxy/server.js +592 -253
  58. package/dist/proxy/types.d.ts +13 -0
  59. package/dist/proxy/types.js +2 -0
  60. package/dist/proxy/utils/extractors.d.ts +18 -0
  61. package/dist/proxy/utils/extractors.js +109 -0
  62. package/dist/proxy/utils/logging.d.ts +18 -0
  63. package/dist/proxy/utils/logging.js +42 -0
  64. package/package.json +22 -4
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,7 +36,7 @@ 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
- ![grov status](docs/grov-status.jpeg)
39
+ ![grov demo](demo-converted.gif)
39
40
 
40
41
  ### What Gets Captured
41
42
 
@@ -77,6 +78,8 @@ grov init # Configure proxy URL (one-time)
77
78
  grov proxy # Start the proxy (required)
78
79
  grov proxy-status # Show active sessions
79
80
  grov status # Show captured tasks
81
+ grov login # Login to cloud dashboard
82
+ grov sync # Sync memories to team dashboard
80
83
  grov disable # Disable grov
81
84
  ```
82
85
 
@@ -84,7 +87,24 @@ grov disable # Disable grov
84
87
 
85
88
  - **Database:** `~/.grov/memory.db` (SQLite)
86
89
  - **Per-project:** Context is filtered by project path
87
- - **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.
88
108
 
89
109
  ## Requirements
90
110
 
@@ -109,6 +129,24 @@ Grov monitors what Claude **does** (not what you ask) and corrects if it drifts
109
129
  grov drift-test "refactor the auth system" --goal "fix login bug"
110
130
  ```
111
131
 
132
+ ### Extended Cache (Experimental)
133
+
134
+ Anthropic's prompt cache expires after 5 minutes of inactivity. If you pause to think between prompts, the cache expires and must be recreated (costs more, takes longer).
135
+
136
+ ```bash
137
+ grov proxy --extended-cache
138
+ ```
139
+
140
+ **What this does:** Sends minimal keep-alive requests (~$0.002 each) during idle periods to preserve your cache.
141
+
142
+ **Important:** By using `--extended-cache`, you consent to Grov making API requests on your behalf to keep the cache active. These requests:
143
+ - Use your Anthropic API key
144
+ - Are sent automatically during idle periods (every ~4 minutes)
145
+ - Cost approximately $0.002 per keep-alive
146
+ - Are discarded (not added to your conversation)
147
+
148
+ This feature is **disabled by default** and requires explicit opt-in.
149
+
112
150
  ### Environment Variables
113
151
 
114
152
  ```bash
@@ -169,9 +207,10 @@ YOU MAY SKIP EXPLORE AGENTS for files mentioned above.
169
207
  - [x] LLM-powered extraction
170
208
  - [x] Local proxy with real-time monitoring
171
209
  - [x] Anti-drift detection & correction
172
- - [ ] Team sync (cloud backend)
173
- - [ ] Web dashboard
210
+ - [x] Team sync (cloud backend)
211
+ - [x] Web dashboard
174
212
  - [ ] Semantic search
213
+ - [ ] VS Code extension
175
214
 
176
215
  ## Contributing
177
216
 
package/dist/cli.js CHANGED
@@ -77,9 +77,18 @@ 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
+ .option('--extended-cache', 'Keep Anthropic cache alive during idle (sends requests on your behalf)')
82
+ .action(async (options) => {
83
+ if (options.extendedCache) {
84
+ process.env.GROV_EXTENDED_CACHE = 'true';
85
+ console.log('\n⚠️ Extended Cache Enabled');
86
+ console.log(' By using --extended-cache, you consent to Grov making');
87
+ console.log(' minimal keep-alive requests on your behalf to preserve');
88
+ console.log(' Anthropic\'s prompt cache during idle periods.\n');
89
+ }
81
90
  const { startServer } = await import('./proxy/server.js');
82
- await startServer();
91
+ await startServer({ debug: options.debug ?? false });
83
92
  });
84
93
  // grov proxy-status - Show active proxy sessions
85
94
  program
@@ -89,4 +98,33 @@ program
89
98
  const { proxyStatus } = await import('./commands/proxy-status.js');
90
99
  await proxyStatus();
91
100
  }));
101
+ // grov login - Authenticate with Grov cloud
102
+ program
103
+ .command('login')
104
+ .description('Login to Grov cloud (opens browser for authentication)')
105
+ .action(safeAction(async () => {
106
+ const { login } = await import('./commands/login.js');
107
+ await login();
108
+ }));
109
+ // grov logout - Clear stored credentials
110
+ program
111
+ .command('logout')
112
+ .description('Logout from Grov cloud')
113
+ .action(safeAction(async () => {
114
+ const { logout } = await import('./commands/logout.js');
115
+ await logout();
116
+ }));
117
+ // grov sync - Configure cloud sync
118
+ program
119
+ .command('sync')
120
+ .description('Configure cloud sync to team dashboard')
121
+ .option('--enable', 'Enable cloud sync')
122
+ .option('--disable', 'Disable cloud sync')
123
+ .option('--team <id>', 'Set team ID for sync')
124
+ .option('--status', 'Show sync status')
125
+ .option('--push', 'Upload any unsynced local tasks to the team')
126
+ .action(safeAction(async (options) => {
127
+ const { sync } = await import('./commands/sync.js');
128
+ await sync(options);
129
+ }));
92
130
  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,57 @@
1
+ import type { Team, Memory, 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
+ * Fetch team memories from cloud (Supabase via API)
35
+ * Cloud equivalent of getTasksForProject() from store.ts
36
+ * Supports hybrid search when context is provided
37
+ *
38
+ * @param teamId - Team UUID
39
+ * @param projectPath - Project path to filter by (exact match)
40
+ * @param options - Optional filters (files, status, limit, context, current_files)
41
+ * @returns Array of memories (empty array on error - fail silent)
42
+ */
43
+ export declare function fetchTeamMemories(teamId: string, projectPath: string, options?: {
44
+ files?: string[];
45
+ status?: string;
46
+ limit?: number;
47
+ context?: string;
48
+ current_files?: string[];
49
+ }): Promise<Memory[]>;
50
+ /**
51
+ * Sleep helper for polling
52
+ */
53
+ export declare function sleep(ms: number): Promise<void>;
54
+ /**
55
+ * Get API URL (for display)
56
+ */
57
+ export declare function getApiUrl(): string;
@@ -0,0 +1,174 @@
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
+ // Security limits for API params
106
+ const MAX_CONTEXT_LENGTH = 2000; // Max chars for semantic search context
107
+ const MAX_FILES_COUNT = 20; // Max files for boost/filter
108
+ /**
109
+ * Fetch team memories from cloud (Supabase via API)
110
+ * Cloud equivalent of getTasksForProject() from store.ts
111
+ * Supports hybrid search when context is provided
112
+ *
113
+ * @param teamId - Team UUID
114
+ * @param projectPath - Project path to filter by (exact match)
115
+ * @param options - Optional filters (files, status, limit, context, current_files)
116
+ * @returns Array of memories (empty array on error - fail silent)
117
+ */
118
+ export async function fetchTeamMemories(teamId, projectPath, options) {
119
+ // Build query params
120
+ const params = new URLSearchParams();
121
+ params.set('project_path', projectPath);
122
+ if (options?.status) {
123
+ params.set('status', options.status);
124
+ }
125
+ if (options?.limit) {
126
+ params.set('limit', options.limit.toString());
127
+ }
128
+ if (options?.files && options.files.length > 0) {
129
+ // API expects multiple 'files' params for array
130
+ options.files.slice(0, MAX_FILES_COUNT).forEach(f => params.append('files', f));
131
+ }
132
+ // Hybrid search params (with security limits)
133
+ if (options?.context) {
134
+ params.set('context', options.context.substring(0, MAX_CONTEXT_LENGTH));
135
+ }
136
+ if (options?.current_files && options.current_files.length > 0) {
137
+ // Comma-separated for current_files (boost)
138
+ const files = options.current_files.slice(0, MAX_FILES_COUNT);
139
+ params.set('current_files', files.join(','));
140
+ }
141
+ const url = `/teams/${teamId}/memories?${params.toString()}`;
142
+ console.log(`[API] fetchTeamMemories: GET ${url}`);
143
+ try {
144
+ const response = await apiRequest('GET', url);
145
+ if (response.error) {
146
+ console.warn(`[API] fetchTeamMemories failed: ${response.error} (status: ${response.status})`);
147
+ return []; // Fail silent - don't block Claude Code
148
+ }
149
+ if (!response.data || !response.data.memories) {
150
+ console.log('[API] fetchTeamMemories: No memories returned');
151
+ return [];
152
+ }
153
+ console.log(`[API] fetchTeamMemories: Got ${response.data.memories.length} memories`);
154
+ return response.data.memories;
155
+ }
156
+ catch (err) {
157
+ const errorMsg = err instanceof Error ? err.message : 'Unknown error';
158
+ console.error(`[API] fetchTeamMemories exception: ${errorMsg}`);
159
+ return []; // Fail silent - don't block Claude Code
160
+ }
161
+ }
162
+ // ============= Utility Functions =============
163
+ /**
164
+ * Sleep helper for polling
165
+ */
166
+ export function sleep(ms) {
167
+ return new Promise((resolve) => setTimeout(resolve, ms));
168
+ }
169
+ /**
170
+ * Get API URL (for display)
171
+ */
172
+ export function getApiUrl() {
173
+ return API_URL;
174
+ }
@@ -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;