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,13 @@
1
+ export interface MessagesRequestBody {
2
+ model: string;
3
+ messages: Array<{
4
+ role: string;
5
+ content: unknown;
6
+ }>;
7
+ system?: string | Array<{
8
+ type: string;
9
+ text: string;
10
+ }>;
11
+ max_tokens?: number;
12
+ [key: string]: unknown;
13
+ }
@@ -0,0 +1,2 @@
1
+ // Shared types for Grov proxy
2
+ export {};
@@ -0,0 +1,18 @@
1
+ import type { AnthropicResponse } from '../action-parser.js';
2
+ import type { ConversationMessage } from '../../lib/llm-extractor.js';
3
+ import type { MessagesRequestBody } from '../types.js';
4
+ export declare function detectKeyDecision(action: {
5
+ actionType: string;
6
+ files: string[];
7
+ command?: string;
8
+ }, reasoning: string): boolean;
9
+ export declare function extractTextContent(response: AnthropicResponse): string;
10
+ export declare function extractProjectPath(body: MessagesRequestBody): string | null;
11
+ export declare function extractGoalFromMessages(messages: Array<{
12
+ role: string;
13
+ content: unknown;
14
+ }>): string | undefined;
15
+ export declare function extractConversationHistory(messages: Array<{
16
+ role: string;
17
+ content: unknown;
18
+ }>): ConversationMessage[];
@@ -0,0 +1,109 @@
1
+ // Pure extraction functions for parsing request/response data
2
+ export function detectKeyDecision(action, reasoning) {
3
+ // Code modifications are always key decisions
4
+ if (action.actionType === 'edit' || action.actionType === 'write') {
5
+ return true;
6
+ }
7
+ // Check for decision-related keywords in reasoning
8
+ const decisionKeywords = [
9
+ 'decision', 'decided', 'chose', 'chosen', 'selected', 'picked',
10
+ 'approach', 'strategy', 'solution', 'implementation',
11
+ 'because', 'reason', 'rationale', 'trade-off', 'tradeoff',
12
+ 'instead of', 'rather than', 'prefer', 'opted',
13
+ 'conclusion', 'determined', 'resolved'
14
+ ];
15
+ const reasoningLower = reasoning.toLowerCase();
16
+ const hasDecisionKeyword = decisionKeywords.some(kw => reasoningLower.includes(kw));
17
+ // Substantial reasoning (>200 chars) with decision keyword = key decision
18
+ if (hasDecisionKeyword && reasoning.length > 200) {
19
+ return true;
20
+ }
21
+ return false;
22
+ }
23
+ export function extractTextContent(response) {
24
+ return response.content
25
+ .filter((block) => block.type === 'text')
26
+ .map(block => block.text)
27
+ .join('\n');
28
+ }
29
+ export function extractProjectPath(body) {
30
+ // Try to extract from system prompt or messages
31
+ // Handle both string and array format for system prompt
32
+ let systemPrompt = '';
33
+ if (typeof body.system === 'string') {
34
+ systemPrompt = body.system;
35
+ }
36
+ else if (Array.isArray(body.system)) {
37
+ // New API format: system is array of {type: 'text', text: '...'}
38
+ systemPrompt = body.system
39
+ .filter((block) => block && typeof block === 'object' && block.type === 'text' && typeof block.text === 'string')
40
+ .map(block => block.text)
41
+ .join('\n');
42
+ }
43
+ const cwdMatch = systemPrompt.match(/Working directory:\s*([^\n]+)/);
44
+ if (cwdMatch) {
45
+ return cwdMatch[1].trim();
46
+ }
47
+ return null;
48
+ }
49
+ export function extractGoalFromMessages(messages) {
50
+ const userMessages = messages?.filter(m => m.role === 'user') || [];
51
+ // Iterate in REVERSE to get the LAST (most recent) user message
52
+ for (const userMsg of [...userMessages].reverse()) {
53
+ let rawContent = '';
54
+ // Handle string content
55
+ if (typeof userMsg.content === 'string') {
56
+ rawContent = userMsg.content;
57
+ }
58
+ // Handle array content - look for text blocks (skip tool_result)
59
+ if (Array.isArray(userMsg.content)) {
60
+ const textBlocks = userMsg.content
61
+ .filter((block) => block && typeof block === 'object' && block.type === 'text' && typeof block.text === 'string')
62
+ .map(block => block.text);
63
+ rawContent = textBlocks.join('\n');
64
+ }
65
+ // Remove <system-reminder>...</system-reminder> tags (including orphaned tags from split content blocks)
66
+ const cleanContent = rawContent
67
+ .replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g, '')
68
+ .replace(/<\/system-reminder>/g, '')
69
+ .replace(/<system-reminder>[^<]*/g, '')
70
+ .trim();
71
+ // If we found valid text content, return it
72
+ if (cleanContent && cleanContent.length >= 5) {
73
+ return cleanContent.substring(0, 500);
74
+ }
75
+ }
76
+ return undefined;
77
+ }
78
+ export function extractConversationHistory(messages) {
79
+ if (!messages || messages.length === 0)
80
+ return [];
81
+ const result = [];
82
+ for (const msg of messages.slice(-10)) {
83
+ if (msg.role !== 'user' && msg.role !== 'assistant')
84
+ continue;
85
+ let textContent = '';
86
+ // Handle string content
87
+ if (typeof msg.content === 'string') {
88
+ textContent = msg.content;
89
+ }
90
+ // Handle array content - extract text blocks only
91
+ if (Array.isArray(msg.content)) {
92
+ const textBlocks = msg.content
93
+ .filter((block) => block && typeof block === 'object' && block.type === 'text' && typeof block.text === 'string')
94
+ .map(block => block.text);
95
+ textContent = textBlocks.join('\n');
96
+ }
97
+ // Remove system-reminder tags
98
+ const cleanContent = textContent
99
+ .replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g, '')
100
+ .trim();
101
+ if (cleanContent && cleanContent.length > 0) {
102
+ result.push({
103
+ role: msg.role,
104
+ content: cleanContent,
105
+ });
106
+ }
107
+ }
108
+ return result;
109
+ }
@@ -0,0 +1,18 @@
1
+ export declare function setDebugMode(enabled: boolean): void;
2
+ export declare function getNextRequestId(): number;
3
+ export declare function taskLog(event: string, data: Record<string, unknown>): void;
4
+ interface ProxyLogEntry {
5
+ timestamp: string;
6
+ requestId: number;
7
+ type: 'REQUEST' | 'RESPONSE' | 'INJECTION';
8
+ sessionId?: string;
9
+ data: Record<string, unknown>;
10
+ }
11
+ export declare function proxyLog(entry: Omit<ProxyLogEntry, 'timestamp'>): void;
12
+ export declare function logTokenUsage(requestId: number, usage: {
13
+ cacheCreation: number;
14
+ cacheRead: number;
15
+ inputTokens: number;
16
+ outputTokens: number;
17
+ }, latencyMs: number): void;
18
+ export {};
@@ -0,0 +1,42 @@
1
+ // Debug and file logging utilities for Grov proxy
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+ let debugMode = false;
5
+ let requestCounter = 0;
6
+ const PROXY_LOG_PATH = path.join(process.cwd(), 'grov-proxy.log');
7
+ const TASK_LOG_PATH = path.join(process.cwd(), 'grov-task.log');
8
+ export function setDebugMode(enabled) {
9
+ debugMode = enabled;
10
+ }
11
+ export function getNextRequestId() {
12
+ return ++requestCounter;
13
+ }
14
+ export function taskLog(event, data) {
15
+ const timestamp = new Date().toISOString();
16
+ const sessionId = data.sessionId ? String(data.sessionId).substring(0, 8) : '-';
17
+ // Format: [timestamp] [session] EVENT: key=value key=value
18
+ const kvPairs = Object.entries(data)
19
+ .filter(([k]) => k !== 'sessionId')
20
+ .map(([k, v]) => {
21
+ const val = typeof v === 'string' ? v.substring(0, 100) : JSON.stringify(v);
22
+ return `${k}=${val}`;
23
+ })
24
+ .join(' | ');
25
+ const line = `[${timestamp}] [${sessionId}] ${event}: ${kvPairs}\n`;
26
+ fs.appendFileSync(TASK_LOG_PATH, line);
27
+ }
28
+ export function proxyLog(entry) {
29
+ if (!debugMode)
30
+ return;
31
+ const logEntry = {
32
+ timestamp: new Date().toISOString(),
33
+ ...entry,
34
+ };
35
+ const line = JSON.stringify(logEntry) + '\n';
36
+ fs.appendFileSync(PROXY_LOG_PATH, line);
37
+ }
38
+ export function logTokenUsage(requestId, usage, latencyMs) {
39
+ const total = usage.cacheCreation + usage.cacheRead;
40
+ const hitRatio = total > 0 ? ((usage.cacheRead / total) * 100).toFixed(0) : '0';
41
+ console.log(`[${requestId}] ${hitRatio}% cache | in:${usage.inputTokens} out:${usage.outputTokens} | create:${usage.cacheCreation} read:${usage.cacheRead} | ${latencyMs}ms`);
42
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grov",
3
- "version": "0.2.3",
3
+ "version": "0.5.3",
4
4
  "description": "Collective AI memory for Claude Code - captures reasoning from sessions and injects context into future sessions",
5
5
  "type": "module",
6
6
  "main": "dist/cli.js",
@@ -16,14 +16,27 @@
16
16
  "README.md",
17
17
  "LICENSE"
18
18
  ],
19
+ "workspaces": [
20
+ "shared",
21
+ "dashboard",
22
+ "api",
23
+ "landing"
24
+ ],
19
25
  "scripts": {
20
26
  "build": "tsc",
27
+ "build:all": "turbo run build",
21
28
  "dev": "tsc --watch",
29
+ "dev:dashboard": "turbo run dev --filter=@grov/dashboard",
30
+ "dev:api": "turbo run dev --filter=@grov/api",
22
31
  "start": "node dist/cli.js",
23
32
  "proxy": "node dist/proxy/index.js",
24
33
  "test": "vitest run",
25
34
  "test:watch": "vitest",
26
- "prepublishOnly": "npm run build && npm test"
35
+ "test:all": "turbo run test",
36
+ "typecheck": "turbo run typecheck",
37
+ "prepublishOnly": "npm run build && npm test && npm run security:scan",
38
+ "prepare": "husky",
39
+ "security:scan": "./scripts/scan-secrets.sh"
27
40
  },
28
41
  "keywords": [
29
42
  "claude",
@@ -55,19 +68,24 @@
55
68
  "debug": "^4.4.3",
56
69
  "dotenv": "^16.4.5",
57
70
  "fastify": "^5.6.2",
71
+ "open": "^11.0.0",
58
72
  "openai": "^4.70.0",
59
73
  "pino": "^10.1.0",
60
74
  "pino-pretty": "^13.1.2",
61
75
  "undici": "^7.16.0"
62
76
  },
63
77
  "devDependencies": {
78
+ "@grov/shared": "workspace:*",
64
79
  "@types/better-sqlite3": "^7.6.11",
65
80
  "@types/debug": "^4.1.12",
66
81
  "@types/node": "^22.19.1",
82
+ "husky": "^9.1.7",
83
+ "turbo": "^2.6.3",
67
84
  "typescript": "^5.7.2",
68
85
  "vitest": "^2.1.9"
69
86
  },
70
87
  "engines": {
71
- "node": ">=18.0.0"
72
- }
88
+ "node": ">=20.0.0"
89
+ },
90
+ "packageManager": "pnpm@9.15.0"
73
91
  }