grov 0.2.3 → 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 +25 -4
- package/dist/cli.js +32 -2
- package/dist/commands/login.d.ts +1 -0
- package/dist/commands/login.js +115 -0
- package/dist/commands/logout.d.ts +1 -0
- package/dist/commands/logout.js +13 -0
- package/dist/commands/sync.d.ts +8 -0
- package/dist/commands/sync.js +127 -0
- package/dist/lib/api-client.d.ts +40 -0
- package/dist/lib/api-client.js +117 -0
- package/dist/lib/cloud-sync.d.ts +33 -0
- package/dist/lib/cloud-sync.js +176 -0
- package/dist/lib/credentials.d.ts +53 -0
- package/dist/lib/credentials.js +201 -0
- package/dist/lib/llm-extractor.d.ts +1 -1
- package/dist/lib/llm-extractor.js +20 -12
- package/dist/lib/store.d.ts +32 -2
- package/dist/lib/store.js +133 -11
- package/dist/lib/utils.d.ts +5 -0
- package/dist/lib/utils.js +45 -0
- package/dist/proxy/action-parser.d.ts +10 -2
- package/dist/proxy/action-parser.js +4 -2
- package/dist/proxy/forwarder.d.ts +7 -1
- package/dist/proxy/forwarder.js +157 -7
- package/dist/proxy/request-processor.d.ts +4 -3
- package/dist/proxy/request-processor.js +7 -5
- package/dist/proxy/response-processor.js +26 -5
- package/dist/proxy/server.d.ts +5 -1
- package/dist/proxy/server.js +667 -104
- package/package.json +18 -3
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="#
|
|
19
|
+
<a href="#team-sync">Team Sync</a> •
|
|
19
20
|
<a href="#contributing">Contributing</a>
|
|
20
21
|
</p>
|
|
21
22
|
|
|
@@ -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
|
|
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
|
|
|
@@ -169,9 +189,10 @@ YOU MAY SKIP EXPLORE AGENTS for files mentioned above.
|
|
|
169
189
|
- [x] LLM-powered extraction
|
|
170
190
|
- [x] Local proxy with real-time monitoring
|
|
171
191
|
- [x] Anti-drift detection & correction
|
|
172
|
-
- [
|
|
173
|
-
- [
|
|
192
|
+
- [x] Team sync (cloud backend)
|
|
193
|
+
- [x] Web dashboard
|
|
174
194
|
- [ ] Semantic search
|
|
195
|
+
- [ ] VS Code extension
|
|
175
196
|
|
|
176
197
|
## Contributing
|
|
177
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
|
-
.
|
|
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,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;
|