grov 0.1.0

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 (39) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +211 -0
  3. package/dist/cli.d.ts +2 -0
  4. package/dist/cli.js +106 -0
  5. package/dist/commands/capture.d.ts +6 -0
  6. package/dist/commands/capture.js +324 -0
  7. package/dist/commands/drift-test.d.ts +7 -0
  8. package/dist/commands/drift-test.js +177 -0
  9. package/dist/commands/init.d.ts +1 -0
  10. package/dist/commands/init.js +27 -0
  11. package/dist/commands/inject.d.ts +5 -0
  12. package/dist/commands/inject.js +88 -0
  13. package/dist/commands/prompt-inject.d.ts +4 -0
  14. package/dist/commands/prompt-inject.js +451 -0
  15. package/dist/commands/status.d.ts +5 -0
  16. package/dist/commands/status.js +51 -0
  17. package/dist/commands/unregister.d.ts +1 -0
  18. package/dist/commands/unregister.js +22 -0
  19. package/dist/lib/anchor-extractor.d.ts +30 -0
  20. package/dist/lib/anchor-extractor.js +296 -0
  21. package/dist/lib/correction-builder.d.ts +10 -0
  22. package/dist/lib/correction-builder.js +226 -0
  23. package/dist/lib/debug.d.ts +24 -0
  24. package/dist/lib/debug.js +34 -0
  25. package/dist/lib/drift-checker.d.ts +66 -0
  26. package/dist/lib/drift-checker.js +341 -0
  27. package/dist/lib/hooks.d.ts +27 -0
  28. package/dist/lib/hooks.js +258 -0
  29. package/dist/lib/jsonl-parser.d.ts +87 -0
  30. package/dist/lib/jsonl-parser.js +281 -0
  31. package/dist/lib/llm-extractor.d.ts +50 -0
  32. package/dist/lib/llm-extractor.js +408 -0
  33. package/dist/lib/session-parser.d.ts +44 -0
  34. package/dist/lib/session-parser.js +256 -0
  35. package/dist/lib/store.d.ts +248 -0
  36. package/dist/lib/store.js +793 -0
  37. package/dist/lib/utils.d.ts +31 -0
  38. package/dist/lib/utils.js +76 -0
  39. package/package.json +67 -0
@@ -0,0 +1,87 @@
1
+ interface JsonlEntry {
2
+ type: 'user' | 'assistant' | 'system';
3
+ message?: {
4
+ role: string;
5
+ content: string | ContentBlock[];
6
+ };
7
+ timestamp?: string;
8
+ session_id?: string;
9
+ [key: string]: unknown;
10
+ }
11
+ interface ContentBlock {
12
+ type: string;
13
+ text?: string;
14
+ name?: string;
15
+ input?: unknown;
16
+ [key: string]: unknown;
17
+ }
18
+ export interface ParsedSession {
19
+ sessionId: string;
20
+ projectPath: string;
21
+ startTime: string;
22
+ endTime: string;
23
+ userMessages: string[];
24
+ assistantMessages: string[];
25
+ toolCalls: ToolCall[];
26
+ filesRead: string[];
27
+ filesWritten: string[];
28
+ rawEntries: JsonlEntry[];
29
+ }
30
+ export interface ToolCall {
31
+ name: string;
32
+ input: unknown;
33
+ timestamp?: string;
34
+ }
35
+ /**
36
+ * Encode a project path the same way Claude Code does
37
+ * /Users/dev/myapp -> -Users-dev-myapp
38
+ */
39
+ export declare function encodeProjectPath(projectPath: string): string;
40
+ /**
41
+ * Decode an encoded project path back to the original.
42
+ * SECURITY: Validates that the decoded path doesn't contain traversal sequences.
43
+ * @throws Error if path contains traversal sequences
44
+ */
45
+ export declare function decodeProjectPath(encoded: string): string;
46
+ /**
47
+ * Validate that a file path is within the expected project boundary.
48
+ * SECURITY: Prevents path traversal outside project directory.
49
+ */
50
+ export declare function isPathWithinProject(projectPath: string, filePath: string): boolean;
51
+ /**
52
+ * Get the directory where Claude stores sessions for a project
53
+ */
54
+ export declare function getProjectSessionsDir(projectPath: string): string;
55
+ /**
56
+ * Extract session ID from a JSONL file path
57
+ * ~/.claude/projects/-Users-dev-myapp/abc123def456.jsonl -> abc123def456
58
+ * SECURITY: Validates session ID format to prevent path confusion attacks.
59
+ */
60
+ export declare function getSessionIdFromPath(jsonlPath: string): string;
61
+ /**
62
+ * Get the current session ID for a project (from the most recent session file)
63
+ */
64
+ export declare function getCurrentSessionId(projectPath: string): string | null;
65
+ /**
66
+ * Find the most recent session file for a project.
67
+ * SECURITY: Handles TOCTOU race conditions gracefully.
68
+ */
69
+ export declare function findLatestSessionFile(projectPath: string): string | null;
70
+ /**
71
+ * List all session files for a project
72
+ */
73
+ export declare function listSessionFiles(projectPath: string): string[];
74
+ /**
75
+ * Parse a JSONL file into entries.
76
+ * SECURITY: Limits entries to prevent memory exhaustion attacks.
77
+ */
78
+ export declare function parseJsonlFile(filePath: string): JsonlEntry[];
79
+ /**
80
+ * Parse a session file and extract structured data
81
+ */
82
+ export declare function parseSession(filePath: string): ParsedSession;
83
+ /**
84
+ * Get a summary of the session for LLM extraction
85
+ */
86
+ export declare function getSessionSummary(session: ParsedSession): string;
87
+ export {};
@@ -0,0 +1,281 @@
1
+ // Parse Claude Code session JSONL files from ~/.claude/projects/
2
+ import { readFileSync, readdirSync, statSync, existsSync } from 'fs';
3
+ import { homedir } from 'os';
4
+ import { join, basename, resolve, normalize } from 'path';
5
+ import { debugParser } from './debug.js';
6
+ const CLAUDE_PROJECTS_DIR = join(homedir(), '.claude', 'projects');
7
+ /**
8
+ * Encode a project path the same way Claude Code does
9
+ * /Users/dev/myapp -> -Users-dev-myapp
10
+ */
11
+ export function encodeProjectPath(projectPath) {
12
+ // Normalize the path first to prevent encoding malicious paths
13
+ const normalized = projectPath.replace(/\.\.+/g, '.');
14
+ return normalized.replace(/\//g, '-');
15
+ }
16
+ /**
17
+ * Decode an encoded project path back to the original.
18
+ * SECURITY: Validates that the decoded path doesn't contain traversal sequences.
19
+ * @throws Error if path contains traversal sequences
20
+ */
21
+ export function decodeProjectPath(encoded) {
22
+ // First char is always '-' representing the root '/'
23
+ const decoded = encoded.replace(/-/g, '/');
24
+ // SECURITY: Prevent path traversal attacks
25
+ if (decoded.includes('..')) {
26
+ throw new Error('Invalid path: traversal sequence detected');
27
+ }
28
+ return decoded;
29
+ }
30
+ /**
31
+ * Validate that a file path is within the expected project boundary.
32
+ * SECURITY: Prevents path traversal outside project directory.
33
+ */
34
+ export function isPathWithinProject(projectPath, filePath) {
35
+ const resolvedProject = resolve(normalize(projectPath));
36
+ const resolvedFile = resolve(normalize(filePath));
37
+ return resolvedFile.startsWith(resolvedProject);
38
+ }
39
+ /**
40
+ * Get the directory where Claude stores sessions for a project
41
+ */
42
+ export function getProjectSessionsDir(projectPath) {
43
+ const encoded = encodeProjectPath(projectPath);
44
+ return join(CLAUDE_PROJECTS_DIR, encoded);
45
+ }
46
+ // SECURITY: Valid session ID pattern (hex chars, dashes for UUIDs)
47
+ const SESSION_ID_PATTERN = /^[a-f0-9-]+$/i;
48
+ /**
49
+ * Extract session ID from a JSONL file path
50
+ * ~/.claude/projects/-Users-dev-myapp/abc123def456.jsonl -> abc123def456
51
+ * SECURITY: Validates session ID format to prevent path confusion attacks.
52
+ */
53
+ export function getSessionIdFromPath(jsonlPath) {
54
+ const sessionId = basename(jsonlPath, '.jsonl');
55
+ // SECURITY: Validate session ID contains only safe characters
56
+ if (!SESSION_ID_PATTERN.test(sessionId)) {
57
+ debugParser('Invalid session ID format: %s', sessionId.substring(0, 20));
58
+ throw new Error('Invalid session ID format');
59
+ }
60
+ return sessionId;
61
+ }
62
+ /**
63
+ * Get the current session ID for a project (from the most recent session file)
64
+ */
65
+ export function getCurrentSessionId(projectPath) {
66
+ const latestFile = findLatestSessionFile(projectPath);
67
+ return latestFile ? getSessionIdFromPath(latestFile) : null;
68
+ }
69
+ /**
70
+ * Find the most recent session file for a project.
71
+ * SECURITY: Handles TOCTOU race conditions gracefully.
72
+ */
73
+ export function findLatestSessionFile(projectPath) {
74
+ const sessionsDir = getProjectSessionsDir(projectPath);
75
+ if (!existsSync(sessionsDir)) {
76
+ return null;
77
+ }
78
+ // SECURITY: Handle TOCTOU - files may be deleted between list and stat
79
+ const files = [];
80
+ try {
81
+ const entries = readdirSync(sessionsDir).filter(f => f.endsWith('.jsonl'));
82
+ for (const f of entries) {
83
+ const filePath = join(sessionsDir, f);
84
+ try {
85
+ // SECURITY: Wrap stat in try-catch to handle deleted files
86
+ const stat = statSync(filePath);
87
+ files.push({
88
+ name: f,
89
+ path: filePath,
90
+ mtime: stat.mtime
91
+ });
92
+ }
93
+ catch {
94
+ // File was deleted between readdir and stat - skip it
95
+ debugParser('File disappeared during listing: %s', f);
96
+ }
97
+ }
98
+ }
99
+ catch {
100
+ // Directory may have been deleted
101
+ debugParser('Could not read sessions directory: %s', sessionsDir);
102
+ return null;
103
+ }
104
+ files.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
105
+ return files.length > 0 ? files[0].path : null;
106
+ }
107
+ /**
108
+ * List all session files for a project
109
+ */
110
+ export function listSessionFiles(projectPath) {
111
+ const sessionsDir = getProjectSessionsDir(projectPath);
112
+ if (!existsSync(sessionsDir)) {
113
+ return [];
114
+ }
115
+ return readdirSync(sessionsDir)
116
+ .filter(f => f.endsWith('.jsonl'))
117
+ .map(f => join(sessionsDir, f));
118
+ }
119
+ // SECURITY: Maximum entries to prevent memory exhaustion
120
+ const MAX_JSONL_ENTRIES = 10000;
121
+ /**
122
+ * Parse a JSONL file into entries.
123
+ * SECURITY: Limits entries to prevent memory exhaustion attacks.
124
+ */
125
+ export function parseJsonlFile(filePath) {
126
+ let content;
127
+ try {
128
+ content = readFileSync(filePath, 'utf-8');
129
+ }
130
+ catch {
131
+ // File may have been deleted/moved between finding and reading
132
+ debugParser('Could not read file: %s', filePath);
133
+ return [];
134
+ }
135
+ const lines = content.trim().split('\n').filter(line => line.trim());
136
+ const entries = [];
137
+ for (const line of lines) {
138
+ // SECURITY: Limit entries to prevent memory exhaustion
139
+ if (entries.length >= MAX_JSONL_ENTRIES) {
140
+ debugParser('Entry limit reached (%d), stopping parse', MAX_JSONL_ENTRIES);
141
+ break;
142
+ }
143
+ try {
144
+ const entry = JSON.parse(line);
145
+ entries.push(entry);
146
+ }
147
+ catch {
148
+ // Skip malformed lines
149
+ debugParser('Skipping malformed JSONL line');
150
+ }
151
+ }
152
+ return entries;
153
+ }
154
+ /**
155
+ * Extract text content from a message content array
156
+ */
157
+ function extractTextContent(content) {
158
+ if (typeof content === 'string') {
159
+ return content;
160
+ }
161
+ return content
162
+ .filter(block => block.type === 'text' && block.text)
163
+ .map(block => block.text)
164
+ .join('\n');
165
+ }
166
+ /**
167
+ * Extract tool calls from assistant message content
168
+ */
169
+ function extractToolCalls(content, timestamp) {
170
+ if (typeof content === 'string') {
171
+ return [];
172
+ }
173
+ return content
174
+ .filter(block => block.type === 'tool_use' && block.name)
175
+ .map(block => ({
176
+ name: block.name,
177
+ input: block.input,
178
+ timestamp
179
+ }));
180
+ }
181
+ /**
182
+ * Parse a session file and extract structured data
183
+ */
184
+ export function parseSession(filePath) {
185
+ const entries = parseJsonlFile(filePath);
186
+ const sessionId = basename(filePath, '.jsonl');
187
+ const userMessages = [];
188
+ const assistantMessages = [];
189
+ const toolCalls = [];
190
+ const filesRead = new Set();
191
+ const filesWritten = new Set();
192
+ let startTime = '';
193
+ let endTime = '';
194
+ let projectPath = '';
195
+ for (const entry of entries) {
196
+ // Track timestamps
197
+ if (entry.timestamp) {
198
+ if (!startTime)
199
+ startTime = entry.timestamp;
200
+ endTime = entry.timestamp;
201
+ }
202
+ // Extract user messages
203
+ if (entry.type === 'user' && entry.message?.content) {
204
+ const text = extractTextContent(entry.message.content);
205
+ if (text)
206
+ userMessages.push(text);
207
+ }
208
+ // Extract assistant messages and tool calls
209
+ if (entry.type === 'assistant' && entry.message?.content) {
210
+ const text = extractTextContent(entry.message.content);
211
+ if (text)
212
+ assistantMessages.push(text);
213
+ const tools = extractToolCalls(entry.message.content, entry.timestamp);
214
+ toolCalls.push(...tools);
215
+ // Track files from tool calls
216
+ for (const tool of tools) {
217
+ const input = tool.input;
218
+ if (input?.file_path && typeof input.file_path === 'string') {
219
+ if (tool.name === 'Read') {
220
+ filesRead.add(input.file_path);
221
+ }
222
+ else if (tool.name === 'Write' || tool.name === 'Edit') {
223
+ filesWritten.add(input.file_path);
224
+ }
225
+ }
226
+ }
227
+ }
228
+ }
229
+ // Try to infer project path from file paths
230
+ const allFiles = [...filesRead, ...filesWritten];
231
+ if (allFiles.length > 0) {
232
+ // Find common prefix
233
+ const firstFile = allFiles[0];
234
+ const parts = firstFile.split('/');
235
+ // Assume project is a few levels deep (e.g., /Users/dev/project)
236
+ if (parts.length >= 4) {
237
+ projectPath = parts.slice(0, 4).join('/');
238
+ }
239
+ }
240
+ return {
241
+ sessionId,
242
+ projectPath,
243
+ startTime,
244
+ endTime,
245
+ userMessages,
246
+ assistantMessages,
247
+ toolCalls,
248
+ filesRead: [...filesRead],
249
+ filesWritten: [...filesWritten],
250
+ rawEntries: entries
251
+ };
252
+ }
253
+ /**
254
+ * Get a summary of the session for LLM extraction
255
+ */
256
+ export function getSessionSummary(session) {
257
+ const lines = [];
258
+ lines.push(`Session ID: ${session.sessionId}`);
259
+ lines.push(`Time: ${session.startTime} to ${session.endTime}`);
260
+ lines.push('');
261
+ lines.push('## User Messages');
262
+ session.userMessages.forEach((msg, i) => {
263
+ lines.push(`[${i + 1}] ${msg.substring(0, 500)}${msg.length > 500 ? '...' : ''}`);
264
+ });
265
+ lines.push('');
266
+ lines.push('## Files Read');
267
+ session.filesRead.forEach(f => lines.push(` - ${f}`));
268
+ lines.push('');
269
+ lines.push('## Files Written/Edited');
270
+ session.filesWritten.forEach(f => lines.push(` - ${f}`));
271
+ lines.push('');
272
+ lines.push('## Tool Calls');
273
+ const toolSummary = session.toolCalls.reduce((acc, t) => {
274
+ acc[t.name] = (acc[t.name] || 0) + 1;
275
+ return acc;
276
+ }, {});
277
+ Object.entries(toolSummary).forEach(([name, count]) => {
278
+ lines.push(` - ${name}: ${count}x`);
279
+ });
280
+ return lines.join('\n');
281
+ }
@@ -0,0 +1,50 @@
1
+ import type { ParsedSession } from './jsonl-parser.js';
2
+ import type { TaskStatus } 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;
20
+ /**
21
+ * Check if Anthropic API is available (for drift detection)
22
+ */
23
+ export declare function isAnthropicAvailable(): boolean;
24
+ /**
25
+ * Get the drift model to use (from env or default)
26
+ */
27
+ export declare function getDriftModel(): string;
28
+ /**
29
+ * Extract structured reasoning from a parsed session using GPT-3.5-turbo
30
+ */
31
+ export declare function extractReasoning(session: ParsedSession): Promise<ExtractedReasoning>;
32
+ /**
33
+ * Classify just the task status (lighter weight than full extraction)
34
+ */
35
+ export declare function classifyTaskStatus(session: ParsedSession): Promise<TaskStatus>;
36
+ /**
37
+ * Extracted intent from first prompt
38
+ */
39
+ export interface ExtractedIntent {
40
+ goal: string;
41
+ expected_scope: string[];
42
+ constraints: string[];
43
+ success_criteria: string[];
44
+ keywords: string[];
45
+ }
46
+ /**
47
+ * Extract intent from a prompt using Claude Haiku
48
+ * Falls back to basic extraction if API unavailable
49
+ */
50
+ export declare function extractIntent(prompt: string): Promise<ExtractedIntent>;