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.
- package/LICENSE +190 -0
- package/README.md +211 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +106 -0
- package/dist/commands/capture.d.ts +6 -0
- package/dist/commands/capture.js +324 -0
- package/dist/commands/drift-test.d.ts +7 -0
- package/dist/commands/drift-test.js +177 -0
- package/dist/commands/init.d.ts +1 -0
- package/dist/commands/init.js +27 -0
- package/dist/commands/inject.d.ts +5 -0
- package/dist/commands/inject.js +88 -0
- package/dist/commands/prompt-inject.d.ts +4 -0
- package/dist/commands/prompt-inject.js +451 -0
- package/dist/commands/status.d.ts +5 -0
- package/dist/commands/status.js +51 -0
- package/dist/commands/unregister.d.ts +1 -0
- package/dist/commands/unregister.js +22 -0
- package/dist/lib/anchor-extractor.d.ts +30 -0
- package/dist/lib/anchor-extractor.js +296 -0
- package/dist/lib/correction-builder.d.ts +10 -0
- package/dist/lib/correction-builder.js +226 -0
- package/dist/lib/debug.d.ts +24 -0
- package/dist/lib/debug.js +34 -0
- package/dist/lib/drift-checker.d.ts +66 -0
- package/dist/lib/drift-checker.js +341 -0
- package/dist/lib/hooks.d.ts +27 -0
- package/dist/lib/hooks.js +258 -0
- package/dist/lib/jsonl-parser.d.ts +87 -0
- package/dist/lib/jsonl-parser.js +281 -0
- package/dist/lib/llm-extractor.d.ts +50 -0
- package/dist/lib/llm-extractor.js +408 -0
- package/dist/lib/session-parser.d.ts +44 -0
- package/dist/lib/session-parser.js +256 -0
- package/dist/lib/store.d.ts +248 -0
- package/dist/lib/store.js +793 -0
- package/dist/lib/utils.d.ts +31 -0
- package/dist/lib/utils.js +76 -0
- package/package.json +67 -0
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
// LLM-based extraction using OpenAI GPT-3.5-turbo for reasoning summaries
|
|
2
|
+
// and Anthropic Claude Haiku for drift detection
|
|
3
|
+
import OpenAI from 'openai';
|
|
4
|
+
import Anthropic from '@anthropic-ai/sdk';
|
|
5
|
+
import { debugLLM } from './debug.js';
|
|
6
|
+
import { truncate } from './utils.js';
|
|
7
|
+
let client = null;
|
|
8
|
+
let anthropicClient = null;
|
|
9
|
+
/**
|
|
10
|
+
* Initialize the OpenAI client
|
|
11
|
+
*/
|
|
12
|
+
function getClient() {
|
|
13
|
+
if (!client) {
|
|
14
|
+
const apiKey = process.env.OPENAI_API_KEY;
|
|
15
|
+
if (!apiKey) {
|
|
16
|
+
// SECURITY: Generic error to avoid confirming API key mechanism exists
|
|
17
|
+
throw new Error('LLM extraction unavailable');
|
|
18
|
+
}
|
|
19
|
+
client = new OpenAI({ apiKey });
|
|
20
|
+
}
|
|
21
|
+
return client;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Initialize the Anthropic client
|
|
25
|
+
*/
|
|
26
|
+
function getAnthropicClient() {
|
|
27
|
+
if (!anthropicClient) {
|
|
28
|
+
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
29
|
+
if (!apiKey) {
|
|
30
|
+
throw new Error('ANTHROPIC_API_KEY environment variable is required for drift detection');
|
|
31
|
+
}
|
|
32
|
+
anthropicClient = new Anthropic({ apiKey });
|
|
33
|
+
}
|
|
34
|
+
return anthropicClient;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Check if LLM extraction is available (OpenAI API key set)
|
|
38
|
+
*/
|
|
39
|
+
export function isLLMAvailable() {
|
|
40
|
+
return !!process.env.OPENAI_API_KEY;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Check if Anthropic API is available (for drift detection)
|
|
44
|
+
*/
|
|
45
|
+
export function isAnthropicAvailable() {
|
|
46
|
+
return !!process.env.ANTHROPIC_API_KEY;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Get the drift model to use (from env or default)
|
|
50
|
+
*/
|
|
51
|
+
export function getDriftModel() {
|
|
52
|
+
return process.env.GROV_DRIFT_MODEL || 'claude-haiku-4-5';
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Extract structured reasoning from a parsed session using GPT-3.5-turbo
|
|
56
|
+
*/
|
|
57
|
+
export async function extractReasoning(session) {
|
|
58
|
+
const openai = getClient();
|
|
59
|
+
// Build session summary for the prompt
|
|
60
|
+
const sessionSummary = buildSessionSummary(session);
|
|
61
|
+
const response = await openai.chat.completions.create({
|
|
62
|
+
model: 'gpt-3.5-turbo',
|
|
63
|
+
max_tokens: 1024,
|
|
64
|
+
messages: [
|
|
65
|
+
{
|
|
66
|
+
role: 'system',
|
|
67
|
+
content: 'You are a helpful assistant that extracts structured information from coding sessions. Always respond with valid JSON only, no explanation.'
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
role: 'user',
|
|
71
|
+
content: `Analyze this Claude Code session and extract a structured reasoning summary.
|
|
72
|
+
|
|
73
|
+
SESSION DATA:
|
|
74
|
+
${sessionSummary}
|
|
75
|
+
|
|
76
|
+
Extract the following as JSON:
|
|
77
|
+
{
|
|
78
|
+
"task": "Brief description of what the user was trying to do (1 sentence)",
|
|
79
|
+
"goal": "The underlying goal or problem being solved",
|
|
80
|
+
"reasoning_trace": ["Key reasoning steps taken", "Decisions made and why", "What was investigated"],
|
|
81
|
+
"decisions": [{"choice": "What was decided", "reason": "Why this choice was made"}],
|
|
82
|
+
"constraints": ["Any constraints or requirements discovered"],
|
|
83
|
+
"status": "complete|partial|question|abandoned",
|
|
84
|
+
"tags": ["relevant", "domain", "tags"]
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
Status definitions:
|
|
88
|
+
- "complete": Task was finished, implementation done
|
|
89
|
+
- "partial": Work started but not finished
|
|
90
|
+
- "question": Claude asked a question and is waiting for user response
|
|
91
|
+
- "abandoned": User interrupted or moved to different topic
|
|
92
|
+
|
|
93
|
+
Return ONLY valid JSON, no explanation.`
|
|
94
|
+
}
|
|
95
|
+
]
|
|
96
|
+
});
|
|
97
|
+
// Parse the response
|
|
98
|
+
const content = response.choices[0]?.message?.content;
|
|
99
|
+
if (!content) {
|
|
100
|
+
throw new Error('No response from OpenAI');
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
// SECURITY: Parse to plain object first, then sanitize prototype pollution
|
|
104
|
+
const rawParsed = JSON.parse(content);
|
|
105
|
+
// SECURITY: Prevent prototype pollution from LLM-generated JSON
|
|
106
|
+
// An attacker could manipulate LLM to return {"__proto__": {"isAdmin": true}}
|
|
107
|
+
const pollutionKeys = ['__proto__', 'constructor', 'prototype'];
|
|
108
|
+
for (const key of pollutionKeys) {
|
|
109
|
+
if (key in rawParsed) {
|
|
110
|
+
delete rawParsed[key];
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const extracted = rawParsed;
|
|
114
|
+
// SECURITY: Validate types to prevent LLM injection attacks
|
|
115
|
+
const safeTask = typeof extracted.task === 'string' ? extracted.task : '';
|
|
116
|
+
const safeGoal = typeof extracted.goal === 'string' ? extracted.goal : '';
|
|
117
|
+
const safeTrace = Array.isArray(extracted.reasoning_trace)
|
|
118
|
+
? extracted.reasoning_trace.filter((t) => typeof t === 'string')
|
|
119
|
+
: [];
|
|
120
|
+
const safeDecisions = Array.isArray(extracted.decisions)
|
|
121
|
+
? extracted.decisions.filter((d) => d && typeof d === 'object' && typeof d.choice === 'string' && typeof d.reason === 'string')
|
|
122
|
+
: [];
|
|
123
|
+
const safeConstraints = Array.isArray(extracted.constraints)
|
|
124
|
+
? extracted.constraints.filter((c) => typeof c === 'string')
|
|
125
|
+
: [];
|
|
126
|
+
const safeTags = Array.isArray(extracted.tags)
|
|
127
|
+
? extracted.tags.filter((t) => typeof t === 'string')
|
|
128
|
+
: [];
|
|
129
|
+
// Fill defaults with validated values
|
|
130
|
+
return {
|
|
131
|
+
task: safeTask || session.userMessages[0]?.substring(0, 100) || 'Unknown task',
|
|
132
|
+
goal: safeGoal || safeTask || 'Unknown goal',
|
|
133
|
+
reasoning_trace: safeTrace,
|
|
134
|
+
files_touched: session.filesRead.concat(session.filesWritten),
|
|
135
|
+
decisions: safeDecisions,
|
|
136
|
+
constraints: safeConstraints,
|
|
137
|
+
status: validateStatus(extracted.status),
|
|
138
|
+
tags: safeTags
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
catch (parseError) {
|
|
142
|
+
// If JSON parsing fails, return basic extraction
|
|
143
|
+
debugLLM('Failed to parse LLM response, using fallback');
|
|
144
|
+
return createFallbackExtraction(session);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Classify just the task status (lighter weight than full extraction)
|
|
149
|
+
*/
|
|
150
|
+
export async function classifyTaskStatus(session) {
|
|
151
|
+
const openai = getClient();
|
|
152
|
+
// Get last few exchanges for classification
|
|
153
|
+
const lastMessages = session.userMessages.slice(-2).join('\n---\n');
|
|
154
|
+
const lastAssistant = session.assistantMessages.slice(-1)[0] || '';
|
|
155
|
+
const response = await openai.chat.completions.create({
|
|
156
|
+
model: 'gpt-3.5-turbo',
|
|
157
|
+
max_tokens: 50,
|
|
158
|
+
messages: [
|
|
159
|
+
{
|
|
160
|
+
role: 'system',
|
|
161
|
+
content: 'Classify conversation state. Return ONLY one word: complete, partial, question, or abandoned.'
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
role: 'user',
|
|
165
|
+
content: `Last user message(s):
|
|
166
|
+
${lastMessages}
|
|
167
|
+
|
|
168
|
+
Last assistant response (truncated):
|
|
169
|
+
${lastAssistant.substring(0, 500)}
|
|
170
|
+
|
|
171
|
+
Files written: ${session.filesWritten.length}
|
|
172
|
+
Files read: ${session.filesRead.length}
|
|
173
|
+
|
|
174
|
+
Classification:`
|
|
175
|
+
}
|
|
176
|
+
]
|
|
177
|
+
});
|
|
178
|
+
const content = response.choices[0]?.message?.content;
|
|
179
|
+
if (!content) {
|
|
180
|
+
return 'partial';
|
|
181
|
+
}
|
|
182
|
+
return validateStatus(content.trim().toLowerCase());
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Build a summary of the session for the LLM prompt
|
|
186
|
+
*/
|
|
187
|
+
function buildSessionSummary(session) {
|
|
188
|
+
const lines = [];
|
|
189
|
+
// User messages
|
|
190
|
+
lines.push('USER MESSAGES:');
|
|
191
|
+
session.userMessages.forEach((msg, i) => {
|
|
192
|
+
lines.push(`[${i + 1}] ${truncate(msg, 300)}`);
|
|
193
|
+
});
|
|
194
|
+
lines.push('');
|
|
195
|
+
// Files touched
|
|
196
|
+
lines.push('FILES READ:');
|
|
197
|
+
session.filesRead.slice(0, 10).forEach(f => lines.push(` - ${f}`));
|
|
198
|
+
if (session.filesRead.length > 10) {
|
|
199
|
+
lines.push(` ... and ${session.filesRead.length - 10} more`);
|
|
200
|
+
}
|
|
201
|
+
lines.push('');
|
|
202
|
+
lines.push('FILES WRITTEN/EDITED:');
|
|
203
|
+
session.filesWritten.forEach(f => lines.push(` - ${f}`));
|
|
204
|
+
lines.push('');
|
|
205
|
+
// Tool usage summary
|
|
206
|
+
lines.push('TOOL USAGE:');
|
|
207
|
+
const toolCounts = session.toolCalls.reduce((acc, t) => {
|
|
208
|
+
acc[t.name] = (acc[t.name] || 0) + 1;
|
|
209
|
+
return acc;
|
|
210
|
+
}, {});
|
|
211
|
+
Object.entries(toolCounts).forEach(([name, count]) => {
|
|
212
|
+
lines.push(` - ${name}: ${count}x`);
|
|
213
|
+
});
|
|
214
|
+
lines.push('');
|
|
215
|
+
// Last assistant message (often contains summary/conclusion)
|
|
216
|
+
const lastAssistant = session.assistantMessages[session.assistantMessages.length - 1];
|
|
217
|
+
if (lastAssistant) {
|
|
218
|
+
lines.push('LAST ASSISTANT MESSAGE:');
|
|
219
|
+
lines.push(truncate(lastAssistant, 500));
|
|
220
|
+
}
|
|
221
|
+
return lines.join('\n');
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Create fallback extraction when LLM fails
|
|
225
|
+
*/
|
|
226
|
+
function createFallbackExtraction(session) {
|
|
227
|
+
const filesTouched = [...new Set([...session.filesRead, ...session.filesWritten])];
|
|
228
|
+
return {
|
|
229
|
+
task: session.userMessages[0]?.substring(0, 100) || 'Unknown task',
|
|
230
|
+
goal: session.userMessages[0]?.substring(0, 100) || 'Unknown goal',
|
|
231
|
+
reasoning_trace: generateBasicTrace(session),
|
|
232
|
+
files_touched: filesTouched,
|
|
233
|
+
decisions: [],
|
|
234
|
+
constraints: [],
|
|
235
|
+
status: session.filesWritten.length > 0 ? 'complete' : 'partial',
|
|
236
|
+
tags: generateTagsFromFiles(filesTouched)
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Generate basic reasoning trace from tool usage
|
|
241
|
+
*/
|
|
242
|
+
function generateBasicTrace(session) {
|
|
243
|
+
const trace = [];
|
|
244
|
+
const toolCounts = session.toolCalls.reduce((acc, t) => {
|
|
245
|
+
acc[t.name] = (acc[t.name] || 0) + 1;
|
|
246
|
+
return acc;
|
|
247
|
+
}, {});
|
|
248
|
+
if (toolCounts['Read'])
|
|
249
|
+
trace.push(`Read ${toolCounts['Read']} files`);
|
|
250
|
+
if (toolCounts['Write'])
|
|
251
|
+
trace.push(`Wrote ${toolCounts['Write']} files`);
|
|
252
|
+
if (toolCounts['Edit'])
|
|
253
|
+
trace.push(`Edited ${toolCounts['Edit']} files`);
|
|
254
|
+
if (toolCounts['Grep'] || toolCounts['Glob'])
|
|
255
|
+
trace.push('Searched codebase');
|
|
256
|
+
if (toolCounts['Bash'])
|
|
257
|
+
trace.push(`Ran ${toolCounts['Bash']} commands`);
|
|
258
|
+
return trace;
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Generate tags from file paths
|
|
262
|
+
*/
|
|
263
|
+
function generateTagsFromFiles(files) {
|
|
264
|
+
const tags = new Set();
|
|
265
|
+
for (const file of files) {
|
|
266
|
+
const parts = file.split('/');
|
|
267
|
+
for (const part of parts) {
|
|
268
|
+
if (part && !part.includes('.') && part !== 'src' && part !== 'lib') {
|
|
269
|
+
tags.add(part.toLowerCase());
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
// Common patterns
|
|
273
|
+
if (file.includes('auth'))
|
|
274
|
+
tags.add('auth');
|
|
275
|
+
if (file.includes('api'))
|
|
276
|
+
tags.add('api');
|
|
277
|
+
if (file.includes('test'))
|
|
278
|
+
tags.add('test');
|
|
279
|
+
}
|
|
280
|
+
return [...tags].slice(0, 10);
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Validate and normalize status
|
|
284
|
+
*/
|
|
285
|
+
function validateStatus(status) {
|
|
286
|
+
const normalized = status?.toLowerCase().trim();
|
|
287
|
+
if (normalized === 'complete' || normalized === 'partial' ||
|
|
288
|
+
normalized === 'question' || normalized === 'abandoned') {
|
|
289
|
+
return normalized;
|
|
290
|
+
}
|
|
291
|
+
return 'partial'; // Default
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Extract intent from a prompt using Claude Haiku
|
|
295
|
+
* Falls back to basic extraction if API unavailable
|
|
296
|
+
*/
|
|
297
|
+
export async function extractIntent(prompt) {
|
|
298
|
+
// Try LLM extraction if available
|
|
299
|
+
if (isAnthropicAvailable()) {
|
|
300
|
+
try {
|
|
301
|
+
return await extractIntentWithLLM(prompt);
|
|
302
|
+
}
|
|
303
|
+
catch (error) {
|
|
304
|
+
debugLLM('extractIntent LLM failed, using fallback: %O', error);
|
|
305
|
+
return extractIntentBasic(prompt);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
// Fallback to basic extraction
|
|
309
|
+
return extractIntentBasic(prompt);
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Extract intent using Claude Haiku
|
|
313
|
+
*/
|
|
314
|
+
async function extractIntentWithLLM(prompt) {
|
|
315
|
+
const anthropic = getAnthropicClient();
|
|
316
|
+
const model = getDriftModel();
|
|
317
|
+
const response = await anthropic.messages.create({
|
|
318
|
+
model,
|
|
319
|
+
max_tokens: 1024,
|
|
320
|
+
messages: [
|
|
321
|
+
{
|
|
322
|
+
role: 'user',
|
|
323
|
+
content: `Analyze this user prompt and extract the task intent. Return ONLY valid JSON, no explanation.
|
|
324
|
+
|
|
325
|
+
USER PROMPT:
|
|
326
|
+
${prompt}
|
|
327
|
+
|
|
328
|
+
Extract as JSON:
|
|
329
|
+
{
|
|
330
|
+
"goal": "The main objective the user wants to achieve (1 sentence)",
|
|
331
|
+
"expected_scope": ["List of files, directories, or components that should be touched"],
|
|
332
|
+
"constraints": ["Any constraints or requirements mentioned"],
|
|
333
|
+
"success_criteria": ["How to know when the task is complete"],
|
|
334
|
+
"keywords": ["Important technical terms from the prompt"]
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
Return ONLY valid JSON.`
|
|
338
|
+
}
|
|
339
|
+
]
|
|
340
|
+
});
|
|
341
|
+
// Extract text content from response
|
|
342
|
+
const content = response.content[0];
|
|
343
|
+
if (content.type !== 'text') {
|
|
344
|
+
throw new Error('Unexpected response type from Anthropic');
|
|
345
|
+
}
|
|
346
|
+
const parsed = JSON.parse(content.text);
|
|
347
|
+
return {
|
|
348
|
+
goal: parsed.goal || prompt.substring(0, 100),
|
|
349
|
+
expected_scope: parsed.expected_scope || [],
|
|
350
|
+
constraints: parsed.constraints || [],
|
|
351
|
+
success_criteria: parsed.success_criteria || [],
|
|
352
|
+
keywords: parsed.keywords || extractKeywordsBasic(prompt)
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Basic intent extraction without LLM
|
|
357
|
+
*/
|
|
358
|
+
function extractIntentBasic(prompt) {
|
|
359
|
+
return {
|
|
360
|
+
goal: prompt.substring(0, 200),
|
|
361
|
+
expected_scope: extractFilesFromPrompt(prompt),
|
|
362
|
+
constraints: [],
|
|
363
|
+
success_criteria: [],
|
|
364
|
+
keywords: extractKeywordsBasic(prompt)
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Extract file paths from prompt text
|
|
369
|
+
*/
|
|
370
|
+
function extractFilesFromPrompt(prompt) {
|
|
371
|
+
const patterns = [
|
|
372
|
+
/(?:^|\s)(\/[\w\-\.\/]+\.\w+)/g,
|
|
373
|
+
/(?:^|\s)(\.\/[\w\-\.\/]+\.\w+)/g,
|
|
374
|
+
/(?:^|\s)([\w\-]+\/[\w\-\.\/]+\.\w+)/g,
|
|
375
|
+
/(?:^|\s|['"`])([\w\-]+\.\w{1,5})(?:\s|$|,|:|['"`])/g,
|
|
376
|
+
];
|
|
377
|
+
const files = new Set();
|
|
378
|
+
for (const pattern of patterns) {
|
|
379
|
+
const matches = prompt.matchAll(pattern);
|
|
380
|
+
for (const match of matches) {
|
|
381
|
+
const file = match[1].trim();
|
|
382
|
+
if (file && !file.match(/^(http|https|ftp|mailto)/) && !file.match(/^\d+\.\d+/)) {
|
|
383
|
+
files.add(file);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
return [...files];
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Extract keywords from prompt (basic)
|
|
391
|
+
*/
|
|
392
|
+
function extractKeywordsBasic(prompt) {
|
|
393
|
+
const stopWords = new Set([
|
|
394
|
+
'the', 'a', 'an', 'is', 'are', 'was', 'were', 'be', 'been',
|
|
395
|
+
'to', 'for', 'and', 'or', 'in', 'on', 'at', 'of', 'with',
|
|
396
|
+
'this', 'that', 'it', 'i', 'you', 'we', 'they', 'my', 'your',
|
|
397
|
+
'can', 'could', 'would', 'should', 'will', 'do', 'does', 'did',
|
|
398
|
+
'have', 'has', 'had', 'not', 'but', 'if', 'then', 'when', 'where',
|
|
399
|
+
'how', 'what', 'why', 'which', 'who', 'all', 'some', 'any', 'no',
|
|
400
|
+
'from', 'by', 'as', 'so', 'too', 'also', 'just', 'only', 'now',
|
|
401
|
+
'please', 'help', 'me', 'make', 'get', 'add', 'fix', 'update', 'change'
|
|
402
|
+
]);
|
|
403
|
+
const words = prompt.toLowerCase()
|
|
404
|
+
.replace(/[^\w\s]/g, ' ')
|
|
405
|
+
.split(/\s+/)
|
|
406
|
+
.filter(w => w.length > 2 && !stopWords.has(w));
|
|
407
|
+
return [...new Set(words)].slice(0, 15);
|
|
408
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude's action extracted from session JSONL
|
|
3
|
+
*/
|
|
4
|
+
export interface ClaudeAction {
|
|
5
|
+
type: 'edit' | 'write' | 'bash' | 'read' | 'delete' | 'grep' | 'glob' | 'multiedit';
|
|
6
|
+
files: string[];
|
|
7
|
+
command?: string;
|
|
8
|
+
timestamp: number;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Find session JSONL path from session_id and project path.
|
|
12
|
+
*
|
|
13
|
+
* Claude Code stores sessions in:
|
|
14
|
+
* ~/.claude/projects/<encoded-path>/<session_id>.jsonl
|
|
15
|
+
*
|
|
16
|
+
* The encoded path uses a specific encoding (not standard URL encoding).
|
|
17
|
+
*/
|
|
18
|
+
export declare function findSessionFile(sessionId: string, projectPath: string): string | null;
|
|
19
|
+
/**
|
|
20
|
+
* Parse JSONL and extract ALL Claude's tool calls
|
|
21
|
+
*/
|
|
22
|
+
export declare function parseSessionActions(sessionPath: string): ClaudeAction[];
|
|
23
|
+
/**
|
|
24
|
+
* Get only NEW actions since last check timestamp.
|
|
25
|
+
* This is the main function used by prompt-inject.
|
|
26
|
+
*/
|
|
27
|
+
export declare function getNewActions(sessionPath: string, lastCheckedTimestamp: number): ClaudeAction[];
|
|
28
|
+
/**
|
|
29
|
+
* Get actions that MODIFY files (not reads).
|
|
30
|
+
* Use this for drift detection - reads are exploration, not drift.
|
|
31
|
+
*/
|
|
32
|
+
export declare function getModifyingActions(actions: ClaudeAction[]): ClaudeAction[];
|
|
33
|
+
/**
|
|
34
|
+
* Extract all unique files touched by actions
|
|
35
|
+
*/
|
|
36
|
+
export declare function extractFilesFromActions(actions: ClaudeAction[]): string[];
|
|
37
|
+
/**
|
|
38
|
+
* Extract unique folders from actions
|
|
39
|
+
*/
|
|
40
|
+
export declare function extractFoldersFromActions(actions: ClaudeAction[]): string[];
|
|
41
|
+
/**
|
|
42
|
+
* Extract keywords from an action (for step storage)
|
|
43
|
+
*/
|
|
44
|
+
export declare function extractKeywordsFromAction(action: ClaudeAction): string[];
|