grov 0.6.16 → 0.6.18

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.
@@ -1,4 +1,4 @@
1
- import type { Team, Memory, MemorySyncRequest, MemorySyncResponse, DeviceFlowStartResponse, DeviceFlowPollResponse, ReasoningTraceEntry } from '@grov/shared';
1
+ import type { Team, Memory, MemorySyncRequest, MemorySyncResponse, DeviceFlowStartResponse, DeviceFlowPollResponse, ReasoningTraceEntry, RecordInjectionRequest, RecordInjectionResponse } from '@grov/shared';
2
2
  export interface ApiResponse<T> {
3
3
  data?: T;
4
4
  error?: string;
@@ -88,6 +88,7 @@ export interface MatchResponse {
88
88
  * @returns Match response with memory and score
89
89
  */
90
90
  export declare function fetchMatch(teamId: string, data: MatchInput): Promise<MatchResponse>;
91
+ export declare function reportInjection(event: RecordInjectionRequest): Promise<RecordInjectionResponse | null>;
91
92
  /**
92
93
  * Sleep helper for polling
93
94
  */
@@ -148,6 +148,12 @@ export async function fetchTeamMemories(teamId, projectPath, options) {
148
148
  if (!response.data || !response.data.memories) {
149
149
  return [];
150
150
  }
151
+ // Check if blocked due to quota
152
+ if (response.data.blocked) {
153
+ console.log('\n[grov] \x1b[33m⚠️ Free quota exceeded (110%). Upgrade to continue using memory injection.\x1b[0m');
154
+ console.log('[grov] Manage plan: https://app.grov.dev/settings\n');
155
+ return [];
156
+ }
151
157
  return response.data.memories;
152
158
  }
153
159
  catch (err) {
@@ -186,6 +192,16 @@ export async function fetchMatch(teamId, data) {
186
192
  return { match: null };
187
193
  }
188
194
  }
195
+ // ============= Usage Tracking =============
196
+ export async function reportInjection(event) {
197
+ try {
198
+ const response = await apiRequest('POST', '/usage/injection', event);
199
+ return response.data || null;
200
+ }
201
+ catch {
202
+ return null;
203
+ }
204
+ }
189
205
  // ============= Utility Functions =============
190
206
  /**
191
207
  * Sleep helper for polling
@@ -36,18 +36,12 @@ export interface UpdateMemoryInput extends CreateMemoryInput {
36
36
  /**
37
37
  * Convert local Task to CreateMemoryInput for API
38
38
  */
39
- export declare function taskToMemory(task: Task): CreateMemoryInput;
39
+ export declare function taskToMemory(task: Task, taskType?: 'information' | 'planning' | 'implementation'): CreateMemoryInput;
40
40
  /**
41
41
  * Prepare sync payload for UPDATE path
42
42
  * Merges existing memory with new data based on shouldUpdateMemory result
43
- *
44
- * @param existingMemory - The memory that was matched
45
- * @param newData - Extracted reasoning and decisions from current session
46
- * @param updateResult - Result from shouldUpdateMemory Haiku call
47
- * @param task - The current task being synced
48
- * @returns Payload ready for sync with memory_id for UPDATE
49
43
  */
50
- export declare function prepareSyncPayload(existingMemory: Memory, newData: ExtractedReasoningAndDecisions, updateResult: ShouldUpdateResult, task: Task): UpdateMemoryInput;
44
+ export declare function prepareSyncPayload(existingMemory: Memory, newData: ExtractedReasoningAndDecisions, updateResult: ShouldUpdateResult, task: Task, taskType?: 'information' | 'planning' | 'implementation'): UpdateMemoryInput;
51
45
  /**
52
46
  * Check if sync is enabled and configured
53
47
  */
@@ -12,13 +12,13 @@ const SYNC_CONFIG = {
12
12
  /**
13
13
  * Convert local Task to CreateMemoryInput for API
14
14
  */
15
- export function taskToMemory(task) {
15
+ export function taskToMemory(task, taskType) {
16
16
  return {
17
17
  client_task_id: task.id,
18
18
  project_path: task.project_path,
19
19
  original_query: task.original_query,
20
20
  goal: task.goal,
21
- system_name: task.system_name, // Parent anchor for semantic search
21
+ system_name: task.system_name,
22
22
  summary: task.summary,
23
23
  reasoning_trace: task.reasoning_trace,
24
24
  files_touched: task.files_touched,
@@ -26,6 +26,7 @@ export function taskToMemory(task) {
26
26
  constraints: task.constraints,
27
27
  tags: task.tags,
28
28
  status: task.status,
29
+ task_type: taskType,
29
30
  linked_commit: task.linked_commit,
30
31
  };
31
32
  }
@@ -38,14 +39,8 @@ function getToday() {
38
39
  /**
39
40
  * Prepare sync payload for UPDATE path
40
41
  * Merges existing memory with new data based on shouldUpdateMemory result
41
- *
42
- * @param existingMemory - The memory that was matched
43
- * @param newData - Extracted reasoning and decisions from current session
44
- * @param updateResult - Result from shouldUpdateMemory Haiku call
45
- * @param task - The current task being synced
46
- * @returns Payload ready for sync with memory_id for UPDATE
47
42
  */
48
- export function prepareSyncPayload(existingMemory, newData, updateResult, task) {
43
+ export function prepareSyncPayload(existingMemory, newData, updateResult, task, taskType) {
49
44
  const today = getToday();
50
45
  // 1. Get existing decisions with proper typing
51
46
  const existingDecisions = (existingMemory.decisions || []);
@@ -109,18 +104,19 @@ export function prepareSyncPayload(existingMemory, newData, updateResult, task)
109
104
  const finalReasoningEvolution = reasoningEvolution.slice(-MAX_REASONING_EVOLUTION);
110
105
  // 8. Build final payload
111
106
  return {
112
- memory_id: existingMemory.id, // Triggers UPDATE path in API
107
+ memory_id: existingMemory.id,
113
108
  client_task_id: task.id,
114
109
  project_path: task.project_path,
115
110
  original_query: task.original_query,
116
111
  goal: task.goal,
117
- system_name: newData.system_name || task.system_name, // Parent anchor for semantic search
118
- reasoning_trace: newData.reasoning_trace, // OVERWRITE with new
112
+ system_name: newData.system_name || task.system_name,
113
+ reasoning_trace: newData.reasoning_trace,
119
114
  files_touched: task.files_touched,
120
115
  decisions: finalDecisions,
121
116
  constraints: task.constraints,
122
117
  tags: task.tags,
123
118
  status: task.status,
119
+ task_type: taskType,
124
120
  linked_commit: task.linked_commit,
125
121
  evolution_steps: finalEvolutionSteps,
126
122
  reasoning_evolution: finalReasoningEvolution,
@@ -188,7 +184,7 @@ export async function syncTask(task, extractedData, taskType, headers) {
188
184
  });
189
185
  // Step 2: If no match, INSERT as new memory
190
186
  if (!matchResult.match) {
191
- const memory = taskToMemory(task);
187
+ const memory = taskToMemory(task, taskType);
192
188
  const result = await syncMemories(teamId, { memories: [memory] });
193
189
  console.log(`[SYNC TO CLOUD] ${taskId} -> INSERT (${taskType || 'unknown'})`);
194
190
  return result.synced === 1;
@@ -197,7 +193,7 @@ export async function syncTask(task, extractedData, taskType, headers) {
197
193
  const score = matchResult.combined_score?.toFixed(3) || '-';
198
194
  // If no extracted data, INSERT anyway
199
195
  if (!effectiveExtractedData) {
200
- const memory = taskToMemory(task);
196
+ const memory = taskToMemory(task, taskType);
201
197
  const result = await syncMemories(teamId, { memories: [memory] });
202
198
  console.log(`[SYNC TO CLOUD] ${taskId} -> INSERT (${taskType || 'unknown'})`);
203
199
  return result.synced === 1;
@@ -223,7 +219,7 @@ export async function syncTask(task, extractedData, taskType, headers) {
223
219
  return true;
224
220
  }
225
221
  // Prepare payload for UPDATE
226
- const payload = prepareSyncPayload(matchResult.match, effectiveExtractedData, updateResult, task);
222
+ const payload = prepareSyncPayload(matchResult.match, effectiveExtractedData, updateResult, task, taskType);
227
223
  // Sync with memory_id for UPDATE path
228
224
  const result = await syncMemories(teamId, { memories: [payload] });
229
225
  console.log(`[SYNC TO CLOUD] ${taskId} -> UPDATE (${taskType || 'unknown'}) [matched: ${matchedId}]`);
@@ -269,7 +265,7 @@ export async function syncTasks(tasks) {
269
265
  };
270
266
  }
271
267
  // Convert tasks to memories
272
- const memories = tasks.map(taskToMemory);
268
+ const memories = tasks.map(t => taskToMemory(t));
273
269
  // Batch and sync
274
270
  const batches = [];
275
271
  for (let i = 0; i < memories.length; i += SYNC_CONFIG.batchSize) {
@@ -4,7 +4,10 @@ import { extractFilesFromMessages } from '../request-processor.js';
4
4
  import { fetchTeamMemories } from '../../../core/cloud/api-client.js';
5
5
  import { getSessionState, updateSessionState, markCleared, } from '../../../core/store/store.js';
6
6
  import { isSyncEnabled, getSyncTeamId } from '../../../core/cloud/cloud-sync.js';
7
+ import { getCurrentUser } from '../../../core/cloud/credentials.js';
8
+ import { reportInjection } from '../../../core/cloud/api-client.js';
7
9
  import { clearSessionState, cacheMemories, buildMemoryPreview, buildToolDescription, buildDriftRecoveryInjection, reconstructMessages, addInjectionRecord, commitPendingRecords, setCachedPreview, getCachedPreview, } from '../injection/memory-injection.js';
10
+ import { handleInjectionResponse } from '../utils/usage-warnings.js';
8
11
  let pendingPlanClear = null;
9
12
  export function getPendingPlanClear() {
10
13
  return pendingPlanClear;
@@ -141,6 +144,20 @@ export async function preProcessRequest(adapter, body, sessionInfo, logger, dete
141
144
  previewSize: preview?.length || 0,
142
145
  hasDriftRecovery: !!driftRecovery,
143
146
  });
147
+ const user = getCurrentUser();
148
+ if (user && teamId) {
149
+ reportInjection({
150
+ team_id: teamId,
151
+ user_id: user.id,
152
+ session_id: sessionInfo.sessionId,
153
+ event_id: `${sessionInfo.sessionId}:${Date.now()}:preview`,
154
+ injection_type: 'preview',
155
+ memory_ids: memories.map(m => m.id),
156
+ timestamp: new Date().toISOString(),
157
+ })
158
+ .then(res => handleInjectionResponse(res, teamId))
159
+ .catch(() => { });
160
+ }
144
161
  }
145
162
  else {
146
163
  // No memories found - inject explicit "no entries" so Claude doesn't use old previews
@@ -2,6 +2,9 @@
2
2
  // Handles session management, task orchestration, drift detection, and memory injection
3
3
  import { randomUUID } from 'crypto';
4
4
  import { config, buildSafeHeaders } from './config.js';
5
+ import { getSyncTeamId } from '../../core/cloud/cloud-sync.js';
6
+ import { getCurrentUser } from '../../core/cloud/credentials.js';
7
+ import { reportInjection } from '../../core/cloud/api-client.js';
5
8
  import { extendedCache, evictOldestCacheEntry } from './cache/extended-cache.js';
6
9
  import { getNextRequestId, taskLog, proxyLog, logTokenUsage } from './utils/logging.js';
7
10
  import { preProcessRequest, setPendingPlanClear } from './handlers/preprocess.js';
@@ -11,6 +14,7 @@ import { buildCorrection, formatCorrectionForInjection } from '../../core/extrac
11
14
  import { generateSessionSummary, analyzeTaskContext, } from '../../core/extraction/llm-extractor.js';
12
15
  import { saveToTeamMemory } from './response-processor.js';
13
16
  import { getCachedMemoryById, buildExpandedMemory, addInjectionRecord, hasToolCycleAtPosition, } from './injection/memory-injection.js';
17
+ import { handleInjectionResponse } from './utils/usage-warnings.js';
14
18
  // In-memory state
15
19
  const lastDriftResults = new Map();
16
20
  const lastMessageCount = new Map();
@@ -180,6 +184,24 @@ export async function handleAgentRequest(context) {
180
184
  if (ids.length > 0) {
181
185
  const expandedCount = expandedParts.filter(p => !p.includes('not found')).length;
182
186
  console.log(`[MEMORY] Expanded ${expandedCount}/${ids.length} memories`);
187
+ const teamId = getSyncTeamId();
188
+ const user = getCurrentUser();
189
+ if (teamId && user && expandedCount > 0) {
190
+ const expandedIds = ids.filter((_, i) => !expandedParts[i].includes('not found'));
191
+ for (const memoryId of expandedIds) {
192
+ reportInjection({
193
+ team_id: teamId,
194
+ user_id: user.id,
195
+ session_id: sessionInfo.sessionId,
196
+ event_id: `${sessionInfo.sessionId}:${Date.now()}:expand:${memoryId}`,
197
+ injection_type: 'expand',
198
+ memory_ids: [memoryId],
199
+ timestamp: new Date().toISOString(),
200
+ })
201
+ .then(res => handleInjectionResponse(res, teamId))
202
+ .catch(() => { });
203
+ }
204
+ }
183
205
  }
184
206
  grovExpandResult = expandedParts.join('\n\n');
185
207
  toolResultBlocks.push({
@@ -0,0 +1,2 @@
1
+ import type { RecordInjectionResponse } from '@grov/shared';
2
+ export declare function handleInjectionResponse(response: RecordInjectionResponse | null, teamId: string): void;
@@ -0,0 +1,40 @@
1
+ // Usage warning display for CLI
2
+ // Shows warnings when approaching or exceeding monthly quota
3
+ const SEVERITY = {
4
+ normal: 0,
5
+ warning_80: 1,
6
+ warning_100: 2,
7
+ overage: 3,
8
+ };
9
+ // Cache: teamId -> last warned status (prevents duplicate warnings)
10
+ const warnedStatus = new Map();
11
+ export function handleInjectionResponse(response, teamId) {
12
+ if (!response)
13
+ return;
14
+ // Period reset - clear cache
15
+ if (response.status === 'normal') {
16
+ warnedStatus.delete(teamId);
17
+ return;
18
+ }
19
+ const lastSeverity = SEVERITY[warnedStatus.get(teamId) ?? 'normal'];
20
+ const currentSeverity = SEVERITY[response.status];
21
+ // Only warn on escalation
22
+ if (currentSeverity <= lastSeverity)
23
+ return;
24
+ warnedStatus.set(teamId, response.status);
25
+ const percent = Math.round((response.current_count / response.quota) * 100);
26
+ const usage = `${response.current_count}/${response.quota}`;
27
+ switch (response.status) {
28
+ case 'warning_80':
29
+ console.log(`[grov] ⚠️ Usage at ${percent}% of monthly quota (${usage} injections)`);
30
+ break;
31
+ case 'warning_100':
32
+ console.log(`[grov] ⚠️ Monthly quota reached (${usage} injections)`);
33
+ console.log(`[grov] Overage charges apply after 110% usage`);
34
+ break;
35
+ case 'overage':
36
+ console.log(`[grov] ⚠️ Overage billing now active (${usage} injections)`);
37
+ console.log(`[grov] Manage usage: https://grov.dev/settings`);
38
+ break;
39
+ }
40
+ }
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "grov",
3
- "version": "0.6.16",
3
+ "version": "0.6.18",
4
+
4
5
  "description": "Collective AI memory for Coding Agents - captures reasoning from sessions and injects context into future sessions",
5
6
  "type": "module",
6
7
  "main": "dist/cli/index.js",