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
@@ -0,0 +1,176 @@
1
+ // Cloud sync logic - Upload memories from local database to API
2
+ // Handles batching, retries, and conversion from Task to Memory format
3
+ import { getSyncStatus, getAccessToken } from './credentials.js';
4
+ import { syncMemories, sleep, getApiUrl } from './api-client.js';
5
+ // Sync configuration
6
+ const SYNC_CONFIG = {
7
+ batchSize: 10, // Number of memories per batch
8
+ retryAttempts: 3, // Number of retry attempts per batch
9
+ retryDelay: 1000, // Base delay between retries (ms)
10
+ };
11
+ /**
12
+ * Convert local Task to CreateMemoryInput for API
13
+ */
14
+ export function taskToMemory(task) {
15
+ return {
16
+ client_task_id: task.id,
17
+ project_path: task.project_path,
18
+ original_query: task.original_query,
19
+ goal: task.goal,
20
+ reasoning_trace: task.reasoning_trace,
21
+ files_touched: task.files_touched,
22
+ decisions: task.decisions,
23
+ constraints: task.constraints,
24
+ tags: task.tags,
25
+ status: task.status,
26
+ linked_commit: task.linked_commit,
27
+ };
28
+ }
29
+ /**
30
+ * Check if sync is enabled and configured
31
+ */
32
+ export function isSyncEnabled() {
33
+ const status = getSyncStatus();
34
+ return status?.enabled === true && !!status.teamId;
35
+ }
36
+ /**
37
+ * Get the configured team ID for sync
38
+ */
39
+ export function getSyncTeamId() {
40
+ const status = getSyncStatus();
41
+ return status?.teamId || null;
42
+ }
43
+ /**
44
+ * Sync a single task to the cloud
45
+ * Called when a task is completed
46
+ */
47
+ export async function syncTask(task) {
48
+ if (!isSyncEnabled()) {
49
+ return false;
50
+ }
51
+ const teamId = getSyncTeamId();
52
+ if (!teamId) {
53
+ return false;
54
+ }
55
+ const token = await getAccessToken();
56
+ if (!token) {
57
+ return false;
58
+ }
59
+ try {
60
+ const memory = taskToMemory(task);
61
+ const result = await syncMemories(teamId, { memories: [memory] });
62
+ return result.synced === 1;
63
+ }
64
+ catch {
65
+ return false;
66
+ }
67
+ }
68
+ /**
69
+ * Sync multiple tasks with batching and retry
70
+ */
71
+ export async function syncTasks(tasks) {
72
+ if (!isSyncEnabled()) {
73
+ return {
74
+ synced: 0,
75
+ failed: tasks.length,
76
+ errors: [`Sync is not enabled. Run "grov sync --enable --team <team-id>" first. (API: ${getApiUrl()})`],
77
+ syncedIds: [],
78
+ failedIds: tasks.map(t => t.id),
79
+ };
80
+ }
81
+ const teamId = getSyncTeamId();
82
+ if (!teamId) {
83
+ return {
84
+ synced: 0,
85
+ failed: tasks.length,
86
+ errors: [`No team configured. Run "grov sync --enable --team <team-id>" first. (API: ${getApiUrl()})`],
87
+ syncedIds: [],
88
+ failedIds: tasks.map(t => t.id),
89
+ };
90
+ }
91
+ const token = await getAccessToken();
92
+ if (!token) {
93
+ return {
94
+ synced: 0,
95
+ failed: tasks.length,
96
+ errors: [`Not authenticated. Run "grov login" first. (API: ${getApiUrl()})`],
97
+ syncedIds: [],
98
+ failedIds: tasks.map(t => t.id),
99
+ };
100
+ }
101
+ // Convert tasks to memories
102
+ const memories = tasks.map(taskToMemory);
103
+ // Batch and sync
104
+ const batches = [];
105
+ for (let i = 0; i < memories.length; i += SYNC_CONFIG.batchSize) {
106
+ batches.push(memories.slice(i, i + SYNC_CONFIG.batchSize));
107
+ }
108
+ let totalSynced = 0;
109
+ let totalFailed = 0;
110
+ const allErrors = [];
111
+ const syncedIds = [];
112
+ const failedIds = [];
113
+ for (const batch of batches) {
114
+ const batchResult = await syncBatchWithRetry(teamId, batch);
115
+ totalSynced += batchResult.synced;
116
+ totalFailed += batchResult.failed;
117
+ if (batchResult.errors) {
118
+ allErrors.push(...batchResult.errors);
119
+ }
120
+ const batchIds = batch.map((m) => m.client_task_id || '');
121
+ if (batchResult.synced === batch.length) {
122
+ syncedIds.push(...batchIds);
123
+ }
124
+ else if (batchResult.failed === batch.length) {
125
+ failedIds.push(...batchIds);
126
+ }
127
+ }
128
+ return {
129
+ synced: totalSynced,
130
+ failed: totalFailed,
131
+ errors: allErrors,
132
+ syncedIds: syncedIds.filter(Boolean),
133
+ failedIds: failedIds.filter(Boolean),
134
+ };
135
+ }
136
+ /**
137
+ * Sync a batch with retry logic
138
+ */
139
+ async function syncBatchWithRetry(teamId, memories) {
140
+ let lastError;
141
+ for (let attempt = 0; attempt < SYNC_CONFIG.retryAttempts; attempt++) {
142
+ try {
143
+ return await syncMemories(teamId, { memories });
144
+ }
145
+ catch (err) {
146
+ lastError = err instanceof Error ? err.message : 'Unknown error';
147
+ // Exponential backoff
148
+ if (attempt < SYNC_CONFIG.retryAttempts - 1) {
149
+ const delay = SYNC_CONFIG.retryDelay * Math.pow(2, attempt);
150
+ await sleep(delay);
151
+ }
152
+ }
153
+ }
154
+ // All retries failed
155
+ return {
156
+ synced: 0,
157
+ failed: memories.length,
158
+ errors: [lastError || 'Sync failed after retries'],
159
+ };
160
+ }
161
+ /**
162
+ * Get sync status summary
163
+ */
164
+ export function getSyncStatusSummary() {
165
+ const status = getSyncStatus();
166
+ if (!status) {
167
+ return 'Not logged in';
168
+ }
169
+ if (!status.enabled) {
170
+ return 'Sync disabled';
171
+ }
172
+ if (!status.teamId) {
173
+ return 'No team configured';
174
+ }
175
+ return `Syncing to team: ${status.teamId}`;
176
+ }
@@ -0,0 +1,53 @@
1
+ export interface Credentials {
2
+ access_token: string;
3
+ refresh_token: string;
4
+ expires_at: string;
5
+ user_id: string;
6
+ email: string;
7
+ team_id?: string;
8
+ sync_enabled: boolean;
9
+ }
10
+ /**
11
+ * Read credentials from disk
12
+ * @returns Credentials or null if not found/invalid
13
+ */
14
+ export declare function readCredentials(): Credentials | null;
15
+ /**
16
+ * Write credentials to disk with secure permissions
17
+ */
18
+ export declare function writeCredentials(creds: Credentials): void;
19
+ /**
20
+ * Clear credentials (logout)
21
+ */
22
+ export declare function clearCredentials(): void;
23
+ /**
24
+ * Check if user is authenticated (has valid credentials)
25
+ */
26
+ export declare function isAuthenticated(): boolean;
27
+ /**
28
+ * Get a valid access token, refreshing if necessary
29
+ * @returns Access token or null if not authenticated
30
+ */
31
+ export declare function getAccessToken(): Promise<string | null>;
32
+ /**
33
+ * Set the team ID for sync
34
+ */
35
+ export declare function setTeamId(teamId: string): void;
36
+ /**
37
+ * Enable or disable sync
38
+ */
39
+ export declare function setSyncEnabled(enabled: boolean): void;
40
+ /**
41
+ * Get current sync status
42
+ */
43
+ export declare function getSyncStatus(): {
44
+ enabled: boolean;
45
+ teamId: string | undefined;
46
+ } | null;
47
+ /**
48
+ * Get current user info
49
+ */
50
+ export declare function getCurrentUser(): {
51
+ id: string;
52
+ email: string;
53
+ } | null;
@@ -0,0 +1,201 @@
1
+ // Secure credential storage for CLI authentication
2
+ // Stores tokens at ~/.grov/credentials.json with 0o600 permissions
3
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, unlinkSync, chmodSync } from 'fs';
4
+ import { homedir } from 'os';
5
+ import { join } from 'path';
6
+ import { request } from 'undici';
7
+ const GROV_DIR = join(homedir(), '.grov');
8
+ const CREDENTIALS_PATH = join(GROV_DIR, 'credentials.json');
9
+ /**
10
+ * Ensure .grov directory exists with proper permissions
11
+ */
12
+ function ensureGrovDir() {
13
+ if (!existsSync(GROV_DIR)) {
14
+ mkdirSync(GROV_DIR, { recursive: true, mode: 0o700 });
15
+ }
16
+ }
17
+ /**
18
+ * Read credentials from disk
19
+ * @returns Credentials or null if not found/invalid
20
+ */
21
+ export function readCredentials() {
22
+ if (!existsSync(CREDENTIALS_PATH)) {
23
+ return null;
24
+ }
25
+ try {
26
+ const content = readFileSync(CREDENTIALS_PATH, 'utf-8');
27
+ const creds = JSON.parse(content);
28
+ // Validate required fields
29
+ if (!creds.access_token || !creds.refresh_token || !creds.expires_at) {
30
+ return null;
31
+ }
32
+ return creds;
33
+ }
34
+ catch {
35
+ return null;
36
+ }
37
+ }
38
+ /**
39
+ * Write credentials to disk with secure permissions
40
+ */
41
+ export function writeCredentials(creds) {
42
+ ensureGrovDir();
43
+ // Write with restrictive permissions (owner read/write only)
44
+ writeFileSync(CREDENTIALS_PATH, JSON.stringify(creds, null, 2), { mode: 0o600 });
45
+ // Ensure permissions are correct even if file existed
46
+ chmodSync(CREDENTIALS_PATH, 0o600);
47
+ }
48
+ /**
49
+ * Clear credentials (logout)
50
+ */
51
+ export function clearCredentials() {
52
+ if (existsSync(CREDENTIALS_PATH)) {
53
+ unlinkSync(CREDENTIALS_PATH);
54
+ }
55
+ }
56
+ /**
57
+ * Check if user is authenticated (has valid credentials)
58
+ */
59
+ export function isAuthenticated() {
60
+ const creds = readCredentials();
61
+ return creds !== null;
62
+ }
63
+ /**
64
+ * Check if access token is expired or will expire soon (within 5 minutes)
65
+ */
66
+ function isTokenExpiringSoon(expiresAt) {
67
+ const expiryTime = new Date(expiresAt).getTime();
68
+ const bufferTime = 5 * 60 * 1000; // 5 minutes
69
+ return Date.now() > expiryTime - bufferTime;
70
+ }
71
+ /**
72
+ * Refresh tokens using the API
73
+ * @returns New credentials or null if refresh failed
74
+ */
75
+ async function refreshTokens(refreshToken, apiUrl) {
76
+ try {
77
+ const response = await request(`${apiUrl}/auth/refresh`, {
78
+ method: 'POST',
79
+ headers: {
80
+ 'Content-Type': 'application/json',
81
+ },
82
+ body: JSON.stringify({ refresh_token: refreshToken }),
83
+ });
84
+ if (response.statusCode !== 200) {
85
+ return null;
86
+ }
87
+ const data = await response.body.json();
88
+ // Decode user info from new token (basic decode, no verification needed here)
89
+ const payload = decodeTokenPayload(data.access_token);
90
+ if (!payload) {
91
+ return null;
92
+ }
93
+ // Read existing credentials to preserve team_id and sync_enabled
94
+ const existing = readCredentials();
95
+ return {
96
+ access_token: data.access_token,
97
+ refresh_token: data.refresh_token,
98
+ expires_at: data.expires_at,
99
+ user_id: payload.sub,
100
+ email: payload.email,
101
+ team_id: existing?.team_id,
102
+ sync_enabled: existing?.sync_enabled ?? false,
103
+ };
104
+ }
105
+ catch {
106
+ return null;
107
+ }
108
+ }
109
+ /**
110
+ * Decode JWT payload without verification (for extracting user info)
111
+ * WARNING: Do not use for authentication - tokens are verified server-side
112
+ */
113
+ function decodeTokenPayload(token) {
114
+ try {
115
+ const parts = token.split('.');
116
+ if (parts.length !== 3)
117
+ return null;
118
+ const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf-8'));
119
+ return {
120
+ sub: payload.sub,
121
+ email: payload.email,
122
+ };
123
+ }
124
+ catch {
125
+ return null;
126
+ }
127
+ }
128
+ /**
129
+ * Get a valid access token, refreshing if necessary
130
+ * @returns Access token or null if not authenticated
131
+ */
132
+ export async function getAccessToken() {
133
+ const creds = readCredentials();
134
+ if (!creds) {
135
+ return null;
136
+ }
137
+ // Check if token needs refresh
138
+ if (isTokenExpiringSoon(creds.expires_at)) {
139
+ const apiUrl = process.env.GROV_API_URL || 'https://api.grov.dev';
140
+ const newCreds = await refreshTokens(creds.refresh_token, apiUrl);
141
+ if (newCreds) {
142
+ writeCredentials(newCreds);
143
+ return newCreds.access_token;
144
+ }
145
+ // Refresh failed - user needs to login again
146
+ return null;
147
+ }
148
+ return creds.access_token;
149
+ }
150
+ /**
151
+ * Set the team ID for sync
152
+ */
153
+ export function setTeamId(teamId) {
154
+ const creds = readCredentials();
155
+ if (!creds) {
156
+ throw new Error('Not authenticated. Please run: grov login');
157
+ }
158
+ writeCredentials({
159
+ ...creds,
160
+ team_id: teamId,
161
+ });
162
+ }
163
+ /**
164
+ * Enable or disable sync
165
+ */
166
+ export function setSyncEnabled(enabled) {
167
+ const creds = readCredentials();
168
+ if (!creds) {
169
+ throw new Error('Not authenticated. Please run: grov login');
170
+ }
171
+ writeCredentials({
172
+ ...creds,
173
+ sync_enabled: enabled,
174
+ });
175
+ }
176
+ /**
177
+ * Get current sync status
178
+ */
179
+ export function getSyncStatus() {
180
+ const creds = readCredentials();
181
+ if (!creds) {
182
+ return null;
183
+ }
184
+ return {
185
+ enabled: creds.sync_enabled,
186
+ teamId: creds.team_id,
187
+ };
188
+ }
189
+ /**
190
+ * Get current user info
191
+ */
192
+ export function getCurrentUser() {
193
+ const creds = readCredentials();
194
+ if (!creds) {
195
+ return null;
196
+ }
197
+ return {
198
+ id: creds.user_id,
199
+ email: creds.email,
200
+ };
201
+ }
@@ -1,22 +1,4 @@
1
- import type { ParsedSession } from './jsonl-parser.js';
2
- import type { TaskStatus, SessionState, StepRecord } from './store.js';
3
- export interface ExtractedReasoning {
4
- task: string;
5
- goal: string;
6
- reasoning_trace: string[];
7
- files_touched: string[];
8
- decisions: Array<{
9
- choice: string;
10
- reason: string;
11
- }>;
12
- constraints: string[];
13
- status: TaskStatus;
14
- tags: string[];
15
- }
16
- /**
17
- * Check if LLM extraction is available (OpenAI API key set)
18
- */
19
- export declare function isLLMAvailable(): boolean;
1
+ import type { SessionState, StepRecord } from './store.js';
20
2
  export interface ExtractedIntent {
21
3
  goal: string;
22
4
  expected_scope: string[];
@@ -34,22 +16,6 @@ export declare function extractIntent(firstPrompt: string): Promise<ExtractedInt
34
16
  * Check if intent extraction is available
35
17
  */
36
18
  export declare function isIntentExtractionAvailable(): boolean;
37
- /**
38
- * Check if Anthropic API is available (for drift detection)
39
- */
40
- export declare function isAnthropicAvailable(): boolean;
41
- /**
42
- * Get the drift model to use (from env or default)
43
- */
44
- export declare function getDriftModel(): string;
45
- /**
46
- * Extract structured reasoning from a parsed session using GPT-3.5-turbo
47
- */
48
- export declare function extractReasoning(session: ParsedSession): Promise<ExtractedReasoning>;
49
- /**
50
- * Classify just the task status (lighter weight than full extraction)
51
- */
52
- export declare function classifyTaskStatus(session: ParsedSession): Promise<TaskStatus>;
53
19
  /**
54
20
  * Check if session summary generation is available
55
21
  */
@@ -58,19 +24,26 @@ export declare function isSummaryAvailable(): boolean;
58
24
  * Generate session summary for CLEAR operation
59
25
  * Reference: plan_proxy_local.md Section 2.3, 4.5
60
26
  */
61
- export declare function generateSessionSummary(sessionState: SessionState, steps: StepRecord[]): Promise<string>;
27
+ export declare function generateSessionSummary(sessionState: SessionState, steps: StepRecord[], maxTokens?: number): Promise<string>;
62
28
  /**
63
29
  * Task analysis result from Haiku
64
30
  */
65
31
  export interface TaskAnalysis {
32
+ task_type: 'information' | 'planning' | 'implementation';
66
33
  action: 'continue' | 'new_task' | 'subtask' | 'parallel_task' | 'task_complete' | 'subtask_complete';
67
- topic_match?: 'YES' | 'NO';
68
34
  task_id: string;
69
35
  current_goal: string;
70
36
  parent_task_id?: string;
71
37
  reasoning: string;
72
38
  step_reasoning?: string;
73
39
  }
40
+ /**
41
+ * Conversation message for task analysis
42
+ */
43
+ export interface ConversationMessage {
44
+ role: 'user' | 'assistant';
45
+ content: string;
46
+ }
74
47
  /**
75
48
  * Check if task analysis is available
76
49
  */
@@ -80,7 +53,7 @@ export declare function isTaskAnalysisAvailable(): boolean;
80
53
  * Called after each main model response to orchestrate sessions
81
54
  * Also compresses reasoning for steps if assistantResponse > 1000 chars
82
55
  */
83
- export declare function analyzeTaskContext(currentSession: SessionState | null, latestUserMessage: string, recentSteps: StepRecord[], assistantResponse: string): Promise<TaskAnalysis>;
56
+ export declare function analyzeTaskContext(currentSession: SessionState | null, latestUserMessage: string, recentSteps: StepRecord[], assistantResponse: string, conversationHistory?: ConversationMessage[]): Promise<TaskAnalysis>;
84
57
  export interface ExtractedReasoningAndDecisions {
85
58
  reasoning_trace: string[];
86
59
  decisions: Array<{
@@ -95,5 +68,8 @@ export declare function isReasoningExtractionAvailable(): boolean;
95
68
  /**
96
69
  * Extract reasoning trace and decisions from steps
97
70
  * Called at task_complete to populate team memory with rich context
71
+ *
72
+ * @param formattedSteps - Pre-formatted XML string with grouped steps and actions
73
+ * @param originalGoal - The original task goal
98
74
  */
99
- export declare function extractReasoningAndDecisions(stepsReasoning: string[], originalGoal: string): Promise<ExtractedReasoningAndDecisions>;
75
+ export declare function extractReasoningAndDecisions(formattedSteps: string, originalGoal: string): Promise<ExtractedReasoningAndDecisions>;