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.
- package/README.md +44 -5
- package/dist/cli.js +40 -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 +57 -0
- package/dist/lib/api-client.js +174 -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 +15 -39
- package/dist/lib/llm-extractor.js +400 -418
- package/dist/lib/store/convenience.d.ts +40 -0
- package/dist/lib/store/convenience.js +104 -0
- package/dist/lib/store/database.d.ts +22 -0
- package/dist/lib/store/database.js +375 -0
- package/dist/lib/store/drift.d.ts +9 -0
- package/dist/lib/store/drift.js +89 -0
- package/dist/lib/store/index.d.ts +7 -0
- package/dist/lib/store/index.js +13 -0
- package/dist/lib/store/sessions.d.ts +32 -0
- package/dist/lib/store/sessions.js +240 -0
- package/dist/lib/store/steps.d.ts +40 -0
- package/dist/lib/store/steps.js +161 -0
- package/dist/lib/store/tasks.d.ts +33 -0
- package/dist/lib/store/tasks.js +133 -0
- package/dist/lib/store/types.d.ts +167 -0
- package/dist/lib/store/types.js +2 -0
- package/dist/lib/store.d.ts +1 -406
- package/dist/lib/store.js +2 -1356
- 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/cache.d.ts +36 -0
- package/dist/proxy/cache.js +51 -0
- package/dist/proxy/config.d.ts +1 -0
- package/dist/proxy/config.js +2 -0
- package/dist/proxy/extended-cache.d.ts +10 -0
- package/dist/proxy/extended-cache.js +155 -0
- package/dist/proxy/forwarder.d.ts +7 -1
- package/dist/proxy/forwarder.js +157 -7
- package/dist/proxy/handlers/preprocess.d.ts +20 -0
- package/dist/proxy/handlers/preprocess.js +169 -0
- package/dist/proxy/injection/delta-tracking.d.ts +11 -0
- package/dist/proxy/injection/delta-tracking.js +93 -0
- package/dist/proxy/injection/injectors.d.ts +7 -0
- package/dist/proxy/injection/injectors.js +139 -0
- package/dist/proxy/request-processor.d.ts +18 -3
- package/dist/proxy/request-processor.js +151 -28
- package/dist/proxy/response-processor.js +116 -47
- package/dist/proxy/server.d.ts +4 -1
- package/dist/proxy/server.js +592 -253
- package/dist/proxy/types.d.ts +13 -0
- package/dist/proxy/types.js +2 -0
- package/dist/proxy/utils/extractors.d.ts +18 -0
- package/dist/proxy/utils/extractors.js +109 -0
- package/dist/proxy/utils/logging.d.ts +18 -0
- package/dist/proxy/utils/logging.js +42 -0
- package/package.json +22 -4
|
@@ -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.
|
|
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
|
-
"
|
|
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": ">=
|
|
72
|
-
}
|
|
88
|
+
"node": ">=20.0.0"
|
|
89
|
+
},
|
|
90
|
+
"packageManager": "pnpm@9.15.0"
|
|
73
91
|
}
|