grov 0.5.11 → 0.6.13
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/dist/cli/agents/registry.d.ts +17 -0
- package/dist/cli/agents/registry.js +132 -0
- package/dist/cli/commands/agents.d.ts +1 -0
- package/dist/cli/commands/agents.js +48 -0
- package/dist/cli/commands/disable.d.ts +1 -0
- package/dist/cli/commands/disable.js +179 -0
- package/dist/cli/commands/doctor.d.ts +1 -0
- package/dist/cli/commands/doctor.js +157 -0
- package/dist/{commands → cli/commands}/drift-test.js +39 -26
- package/dist/cli/commands/init.d.ts +1 -0
- package/dist/cli/commands/init.js +90 -0
- package/dist/{commands → cli/commands}/login.js +19 -18
- package/dist/{commands → cli/commands}/logout.js +1 -1
- package/dist/{commands → cli/commands}/proxy-status.js +1 -1
- package/dist/cli/commands/setup.d.ts +6 -0
- package/dist/cli/commands/setup.js +309 -0
- package/dist/{commands → cli/commands}/status.js +1 -1
- package/dist/{commands → cli/commands}/sync.d.ts +1 -0
- package/dist/{commands → cli/commands}/sync.js +59 -4
- package/dist/{commands → cli/commands}/uninstall.js +2 -2
- package/dist/cli/index.js +270 -0
- package/dist/{lib → core/cloud}/cloud-sync.d.ts +3 -3
- package/dist/{lib → core/cloud}/cloud-sync.js +10 -10
- package/dist/{lib → core/extraction}/correction-builder-proxy.d.ts +1 -1
- package/dist/{lib → core/extraction}/correction-builder-proxy.js +0 -4
- package/dist/{lib → core/extraction}/drift-checker-proxy.d.ts +13 -9
- package/dist/core/extraction/drift-checker-proxy.js +510 -0
- package/dist/{lib → core/extraction}/llm-extractor.d.ts +8 -38
- package/dist/{lib → core/extraction}/llm-extractor.js +132 -220
- package/dist/{lib → core}/store/sessions.js +3 -19
- package/dist/core/store/store.d.ts +1 -0
- package/dist/{lib → core/store}/store.js +1 -1
- package/dist/{lib → core}/store/types.d.ts +0 -4
- package/dist/integrations/mcp/cache.d.ts +27 -0
- package/dist/integrations/mcp/cache.js +106 -0
- package/dist/integrations/mcp/capture/antigravity-parser.d.ts +26 -0
- package/dist/integrations/mcp/capture/antigravity-parser.js +272 -0
- package/dist/integrations/mcp/capture/antigravity-scanner.d.ts +24 -0
- package/dist/integrations/mcp/capture/antigravity-scanner.js +153 -0
- package/dist/integrations/mcp/capture/antigravity-sync-tracker.d.ts +29 -0
- package/dist/integrations/mcp/capture/antigravity-sync-tracker.js +115 -0
- package/dist/integrations/mcp/capture/cli-extractor.d.ts +18 -0
- package/dist/integrations/mcp/capture/cli-extractor.js +258 -0
- package/dist/integrations/mcp/capture/cli-synced.d.ts +4 -0
- package/dist/integrations/mcp/capture/cli-synced.js +62 -0
- package/dist/integrations/mcp/capture/cli-transform.d.ts +30 -0
- package/dist/integrations/mcp/capture/cli-transform.js +62 -0
- package/dist/integrations/mcp/capture/cli-watcher.d.ts +31 -0
- package/dist/integrations/mcp/capture/cli-watcher.js +106 -0
- package/dist/integrations/mcp/capture/hook-handler.d.ts +2 -0
- package/dist/integrations/mcp/capture/hook-handler.js +157 -0
- package/dist/integrations/mcp/capture/sqlite-reader.d.ts +35 -0
- package/dist/integrations/mcp/capture/sqlite-reader.js +388 -0
- package/dist/integrations/mcp/capture/sync-tracker.d.ts +16 -0
- package/dist/integrations/mcp/capture/sync-tracker.js +102 -0
- package/dist/integrations/mcp/clients/cursor/rules-installer.d.ts +19 -0
- package/dist/integrations/mcp/clients/cursor/rules-installer.js +123 -0
- package/dist/integrations/mcp/index.d.ts +1 -0
- package/dist/integrations/mcp/index.js +94 -0
- package/dist/integrations/mcp/logger.d.ts +8 -0
- package/dist/integrations/mcp/logger.js +50 -0
- package/dist/integrations/mcp/server.d.ts +5 -0
- package/dist/integrations/mcp/server.js +58 -0
- package/dist/integrations/mcp/tools/expand.d.ts +1 -0
- package/dist/integrations/mcp/tools/expand.js +53 -0
- package/dist/integrations/mcp/tools/preview.d.ts +1 -0
- package/dist/integrations/mcp/tools/preview.js +64 -0
- package/dist/integrations/proxy/agents/base.d.ts +43 -0
- package/dist/integrations/proxy/agents/base.js +13 -0
- package/dist/{proxy/utils → integrations/proxy/agents/claude}/extractors.d.ts +4 -8
- package/dist/{proxy/utils → integrations/proxy/agents/claude}/extractors.js +4 -33
- package/dist/{proxy → integrations/proxy/agents/claude}/forwarder.d.ts +1 -1
- package/dist/{proxy → integrations/proxy/agents/claude}/forwarder.js +22 -6
- package/dist/integrations/proxy/agents/claude/index.d.ts +43 -0
- package/dist/integrations/proxy/agents/claude/index.js +386 -0
- package/dist/{proxy/action-parser.d.ts → integrations/proxy/agents/claude/parser.d.ts} +1 -1
- package/dist/integrations/proxy/agents/codex/extractors.d.ts +6 -0
- package/dist/integrations/proxy/agents/codex/extractors.js +49 -0
- package/dist/integrations/proxy/agents/codex/forwarder.d.ts +9 -0
- package/dist/integrations/proxy/agents/codex/forwarder.js +125 -0
- package/dist/integrations/proxy/agents/codex/index.d.ts +44 -0
- package/dist/integrations/proxy/agents/codex/index.js +371 -0
- package/dist/integrations/proxy/agents/codex/parser.d.ts +11 -0
- package/dist/integrations/proxy/agents/codex/parser.js +104 -0
- package/dist/integrations/proxy/agents/codex/patch.d.ts +12 -0
- package/dist/integrations/proxy/agents/codex/patch.js +40 -0
- package/dist/integrations/proxy/agents/codex/settings.d.ts +18 -0
- package/dist/integrations/proxy/agents/codex/settings.js +73 -0
- package/dist/integrations/proxy/agents/codex/types.d.ts +59 -0
- package/dist/integrations/proxy/agents/codex/types.js +2 -0
- package/dist/integrations/proxy/agents/index.d.ts +11 -0
- package/dist/integrations/proxy/agents/index.js +25 -0
- package/dist/integrations/proxy/agents/types.d.ts +77 -0
- package/dist/integrations/proxy/agents/types.js +2 -0
- package/dist/{proxy → integrations/proxy/cache}/extended-cache.js +2 -6
- package/dist/{proxy → integrations/proxy}/config.js +1 -1
- package/dist/{proxy → integrations/proxy}/handlers/preprocess.d.ts +3 -3
- package/dist/integrations/proxy/handlers/preprocess.js +194 -0
- package/dist/integrations/proxy/index.js +20 -0
- package/dist/integrations/proxy/injection/memory-injection.d.ts +56 -0
- package/dist/integrations/proxy/injection/memory-injection.js +252 -0
- package/dist/integrations/proxy/orchestrator.d.ts +30 -0
- package/dist/integrations/proxy/orchestrator.js +954 -0
- package/dist/integrations/proxy/request-processor.d.ts +14 -0
- package/dist/integrations/proxy/request-processor.js +68 -0
- package/dist/{proxy → integrations/proxy}/response-processor.d.ts +4 -3
- package/dist/{proxy → integrations/proxy}/response-processor.js +51 -43
- package/dist/{proxy → integrations/proxy}/server.d.ts +0 -1
- package/dist/integrations/proxy/server.js +146 -0
- package/dist/{proxy → integrations/proxy}/types.d.ts +4 -0
- package/dist/{proxy → integrations/proxy}/utils/logging.d.ts +1 -0
- package/dist/{proxy → integrations/proxy}/utils/logging.js +5 -0
- package/package.json +31 -10
- package/postinstall.js +62 -6
- package/dist/cli.js +0 -149
- package/dist/commands/capture.d.ts +0 -6
- package/dist/commands/capture.js +0 -324
- package/dist/commands/disable.d.ts +0 -1
- package/dist/commands/disable.js +0 -14
- package/dist/commands/doctor.d.ts +0 -1
- package/dist/commands/doctor.js +0 -89
- package/dist/commands/init.d.ts +0 -1
- package/dist/commands/init.js +0 -52
- package/dist/commands/inject.d.ts +0 -5
- package/dist/commands/inject.js +0 -88
- package/dist/commands/prompt-inject.d.ts +0 -4
- package/dist/commands/prompt-inject.js +0 -451
- package/dist/commands/unregister.d.ts +0 -1
- package/dist/commands/unregister.js +0 -28
- package/dist/lib/anchor-extractor.d.ts +0 -30
- package/dist/lib/anchor-extractor.js +0 -296
- package/dist/lib/correction-builder.d.ts +0 -10
- package/dist/lib/correction-builder.js +0 -226
- package/dist/lib/drift-checker-proxy.js +0 -373
- package/dist/lib/drift-checker.d.ts +0 -66
- package/dist/lib/drift-checker.js +0 -341
- package/dist/lib/hooks.d.ts +0 -38
- package/dist/lib/hooks.js +0 -291
- package/dist/lib/jsonl-parser.d.ts +0 -87
- package/dist/lib/jsonl-parser.js +0 -281
- package/dist/lib/session-parser.d.ts +0 -44
- package/dist/lib/session-parser.js +0 -256
- package/dist/lib/store.d.ts +0 -1
- package/dist/proxy/cache.d.ts +0 -32
- package/dist/proxy/cache.js +0 -47
- package/dist/proxy/handlers/preprocess.js +0 -186
- package/dist/proxy/index.js +0 -30
- package/dist/proxy/injection/delta-tracking.d.ts +0 -11
- package/dist/proxy/injection/delta-tracking.js +0 -94
- package/dist/proxy/injection/injectors.d.ts +0 -7
- package/dist/proxy/injection/injectors.js +0 -139
- package/dist/proxy/request-processor.d.ts +0 -27
- package/dist/proxy/request-processor.js +0 -233
- package/dist/proxy/server.js +0 -1289
- /package/dist/{commands → cli/commands}/drift-test.d.ts +0 -0
- /package/dist/{commands → cli/commands}/login.d.ts +0 -0
- /package/dist/{commands → cli/commands}/logout.d.ts +0 -0
- /package/dist/{commands → cli/commands}/proxy-status.d.ts +0 -0
- /package/dist/{commands → cli/commands}/status.d.ts +0 -0
- /package/dist/{commands → cli/commands}/uninstall.d.ts +0 -0
- /package/dist/{cli.d.ts → cli/index.d.ts} +0 -0
- /package/dist/{lib → core/cloud}/api-client.d.ts +0 -0
- /package/dist/{lib → core/cloud}/api-client.js +0 -0
- /package/dist/{lib → core/cloud}/credentials.d.ts +0 -0
- /package/dist/{lib → core/cloud}/credentials.js +0 -0
- /package/dist/{lib → core}/store/convenience.d.ts +0 -0
- /package/dist/{lib → core}/store/convenience.js +0 -0
- /package/dist/{lib → core}/store/database.d.ts +0 -0
- /package/dist/{lib → core}/store/database.js +0 -0
- /package/dist/{lib → core}/store/drift.d.ts +0 -0
- /package/dist/{lib → core}/store/drift.js +0 -0
- /package/dist/{lib → core}/store/index.d.ts +0 -0
- /package/dist/{lib → core}/store/index.js +0 -0
- /package/dist/{lib → core}/store/sessions.d.ts +0 -0
- /package/dist/{lib → core}/store/steps.d.ts +0 -0
- /package/dist/{lib → core}/store/steps.js +0 -0
- /package/dist/{lib → core}/store/tasks.d.ts +0 -0
- /package/dist/{lib → core}/store/tasks.js +0 -0
- /package/dist/{lib → core}/store/types.js +0 -0
- /package/dist/{proxy/action-parser.js → integrations/proxy/agents/claude/parser.js} +0 -0
- /package/dist/{lib → integrations/proxy/agents/claude}/settings.d.ts +0 -0
- /package/dist/{lib → integrations/proxy/agents/claude}/settings.js +0 -0
- /package/dist/{proxy → integrations/proxy/cache}/extended-cache.d.ts +0 -0
- /package/dist/{proxy → integrations/proxy}/config.d.ts +0 -0
- /package/dist/{proxy → integrations/proxy}/index.d.ts +0 -0
- /package/dist/{proxy → integrations/proxy}/types.js +0 -0
- /package/dist/{lib → utils}/debug.d.ts +0 -0
- /package/dist/{lib → utils}/debug.js +0 -0
- /package/dist/{lib → utils}/utils.d.ts +0 -0
- /package/dist/{lib → utils}/utils.js +0 -0
package/dist/commands/inject.js
DELETED
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
// grov inject - Called by SessionStart hook, outputs context JSON
|
|
2
|
-
import { getTasksForProject, createSessionState, getSessionState } from '../lib/store.js';
|
|
3
|
-
import { getCurrentSessionId } from '../lib/jsonl-parser.js';
|
|
4
|
-
import { debugInject } from '../lib/debug.js';
|
|
5
|
-
import { truncate } from '../lib/utils.js';
|
|
6
|
-
export async function inject(options) {
|
|
7
|
-
debugInject('inject called - CLAUDE_PROJECT_DIR=%s, cwd=%s', process.env.CLAUDE_PROJECT_DIR || 'NOT_SET', process.cwd());
|
|
8
|
-
try {
|
|
9
|
-
// Get project path from Claude Code env var, fallback to cwd
|
|
10
|
-
// CLAUDE_PROJECT_DIR is set by Claude Code when running hooks
|
|
11
|
-
const projectPath = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
12
|
-
// Initialize session state for this session
|
|
13
|
-
const sessionId = getCurrentSessionId(projectPath);
|
|
14
|
-
if (sessionId) {
|
|
15
|
-
const existing = getSessionState(sessionId);
|
|
16
|
-
if (!existing) {
|
|
17
|
-
createSessionState({
|
|
18
|
-
session_id: sessionId,
|
|
19
|
-
project_path: projectPath,
|
|
20
|
-
user_id: process.env.USER || undefined,
|
|
21
|
-
});
|
|
22
|
-
debugInject('Created session state: %s...', sessionId.substring(0, 8));
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
// Get completed tasks for this project
|
|
26
|
-
const tasks = getTasksForProject(projectPath, {
|
|
27
|
-
status: 'complete',
|
|
28
|
-
limit: 5 // Only inject most recent 5
|
|
29
|
-
});
|
|
30
|
-
// Build context string
|
|
31
|
-
const context = buildContextString(tasks);
|
|
32
|
-
// Only output if we have context to inject
|
|
33
|
-
// Claude Code expects JSON with hookEventName for SessionStart hooks
|
|
34
|
-
if (context) {
|
|
35
|
-
const output = {
|
|
36
|
-
hookSpecificOutput: {
|
|
37
|
-
hookEventName: "SessionStart",
|
|
38
|
-
additionalContext: context
|
|
39
|
-
}
|
|
40
|
-
};
|
|
41
|
-
console.log(JSON.stringify(output));
|
|
42
|
-
}
|
|
43
|
-
// If no context, output nothing - this is cleaner for Claude Code
|
|
44
|
-
}
|
|
45
|
-
catch (error) {
|
|
46
|
-
// On error, output nothing - don't break the session
|
|
47
|
-
// Silent fail is better than outputting potentially invalid JSON
|
|
48
|
-
debugInject('Inject error: %O', error);
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
/**
|
|
52
|
-
* Build the context string to inject
|
|
53
|
-
*/
|
|
54
|
-
function buildContextString(tasks) {
|
|
55
|
-
if (tasks.length === 0) {
|
|
56
|
-
return ''; // No context to inject
|
|
57
|
-
}
|
|
58
|
-
const lines = [];
|
|
59
|
-
lines.push('VERIFIED CONTEXT FROM PREVIOUS SESSIONS:');
|
|
60
|
-
lines.push('(This context was captured from your previous work on this codebase)');
|
|
61
|
-
lines.push('');
|
|
62
|
-
for (const task of tasks) {
|
|
63
|
-
lines.push(`[Task: ${truncate(task.original_query, 80)}]`);
|
|
64
|
-
// Files touched
|
|
65
|
-
if (task.files_touched.length > 0) {
|
|
66
|
-
const fileList = task.files_touched
|
|
67
|
-
.slice(0, 5)
|
|
68
|
-
.map(f => f.split('/').pop())
|
|
69
|
-
.join(', ');
|
|
70
|
-
lines.push(`- Files: ${fileList}${task.files_touched.length > 5 ? ` (+${task.files_touched.length - 5} more)` : ''}`);
|
|
71
|
-
}
|
|
72
|
-
// Reasoning trace
|
|
73
|
-
if (task.reasoning_trace.length > 0) {
|
|
74
|
-
for (const trace of task.reasoning_trace.slice(0, 3)) {
|
|
75
|
-
lines.push(`- ${trace}`);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
// Tags
|
|
79
|
-
if (task.tags.length > 0) {
|
|
80
|
-
lines.push(`- Tags: ${task.tags.join(', ')}`);
|
|
81
|
-
}
|
|
82
|
-
lines.push('');
|
|
83
|
-
}
|
|
84
|
-
// Add instruction for Claude
|
|
85
|
-
lines.push('YOU MAY SKIP EXPLORE AGENTS for files mentioned above.');
|
|
86
|
-
lines.push('Read them directly if relevant to the current task.');
|
|
87
|
-
return lines.join('\n');
|
|
88
|
-
}
|
|
@@ -1,451 +0,0 @@
|
|
|
1
|
-
// grov prompt-inject - Called by UserPromptSubmit hook, outputs context JSON
|
|
2
|
-
// This provides continuous context injection on every user prompt
|
|
3
|
-
// Includes anti-drift detection and correction injection
|
|
4
|
-
//
|
|
5
|
-
// CRITICAL: We check Claude's ACTIONS, NOT user prompts.
|
|
6
|
-
// User can explore freely. We monitor what CLAUDE DOES.
|
|
7
|
-
import 'dotenv/config';
|
|
8
|
-
import { getTasksForProject, getTasksByFiles, getFileReasoningByPathPattern, getSessionState, createSessionState, updateSessionDrift, saveStep, updateLastChecked, } from '../lib/store.js';
|
|
9
|
-
import { extractIntent } from '../lib/llm-extractor.js';
|
|
10
|
-
import { buildDriftCheckInput, checkDrift } from '../lib/drift-checker.js';
|
|
11
|
-
import { determineCorrectionLevel, buildCorrection } from '../lib/correction-builder.js';
|
|
12
|
-
import { debugInject } from '../lib/debug.js';
|
|
13
|
-
import { truncate } from '../lib/utils.js';
|
|
14
|
-
import { findSessionFile, getNewActions, getModifyingActions, extractKeywordsFromAction, } from '../lib/session-parser.js';
|
|
15
|
-
// Maximum stdin size to prevent memory exhaustion (1MB)
|
|
16
|
-
const MAX_STDIN_SIZE = 1024 * 1024;
|
|
17
|
-
// Simple prompts that don't need context injection
|
|
18
|
-
const SIMPLE_PROMPTS = [
|
|
19
|
-
'yes', 'no', 'ok', 'okay', 'continue', 'go ahead',
|
|
20
|
-
'sure', 'yep', 'nope', 'y', 'n', 'proceed', 'do it',
|
|
21
|
-
'looks good', 'that works', 'perfect', 'thanks', 'thank you',
|
|
22
|
-
'next', 'done', 'good', 'great', 'fine', 'correct',
|
|
23
|
-
'right', 'exactly', 'agreed', 'approve', 'confirm'
|
|
24
|
-
];
|
|
25
|
-
// Stop words to filter out when extracting keywords
|
|
26
|
-
const STOP_WORDS = new Set([
|
|
27
|
-
'the', 'a', 'an', 'is', 'are', 'was', 'were', 'be', 'been',
|
|
28
|
-
'to', 'for', 'and', 'or', 'in', 'on', 'at', 'of', 'with',
|
|
29
|
-
'this', 'that', 'these', 'those', 'it', 'its', 'i', 'you',
|
|
30
|
-
'we', 'they', 'my', 'your', 'our', 'their', 'can', 'could',
|
|
31
|
-
'would', 'should', 'will', 'do', 'does', 'did', 'have', 'has',
|
|
32
|
-
'had', 'not', 'but', 'if', 'then', 'else', 'when', 'where',
|
|
33
|
-
'how', 'what', 'why', 'which', 'who', 'all', 'each', 'every',
|
|
34
|
-
'some', 'any', 'no', 'from', 'by', 'as', 'so', 'too', 'also',
|
|
35
|
-
'just', 'only', 'now', 'here', 'there', 'please', 'help', 'me',
|
|
36
|
-
'make', 'get', 'add', 'fix', 'update', 'change', 'modify', 'create'
|
|
37
|
-
]);
|
|
38
|
-
export async function promptInject(_options) {
|
|
39
|
-
try {
|
|
40
|
-
// Read input from stdin
|
|
41
|
-
const input = await readStdinInput();
|
|
42
|
-
if (!input || !input.prompt) {
|
|
43
|
-
return; // No prompt, no injection
|
|
44
|
-
}
|
|
45
|
-
// Skip simple prompts to save tokens
|
|
46
|
-
if (isSimplePrompt(input.prompt)) {
|
|
47
|
-
return; // No output = no injection
|
|
48
|
-
}
|
|
49
|
-
const projectPath = input.cwd || process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
50
|
-
const sessionId = input.session_id;
|
|
51
|
-
// Check if we have a session state (determines if this is first prompt)
|
|
52
|
-
const sessionState = sessionId ? getSessionState(sessionId) : null;
|
|
53
|
-
let correctionText = null;
|
|
54
|
-
// === FIRST PROMPT: Create session state with extracted intent ===
|
|
55
|
-
if (!sessionState && sessionId) {
|
|
56
|
-
correctionText = await handleFirstPrompt(input.prompt, projectPath, sessionId);
|
|
57
|
-
}
|
|
58
|
-
// === SUBSEQUENT PROMPTS: Check Claude's ACTIONS for drift ===
|
|
59
|
-
else if (sessionState) {
|
|
60
|
-
// CRITICAL: We pass projectPath, not prompt. We check Claude's ACTIONS.
|
|
61
|
-
correctionText = await handleDriftCheck(projectPath, sessionState);
|
|
62
|
-
}
|
|
63
|
-
// Get recent completed tasks for this project
|
|
64
|
-
const tasks = getTasksForProject(projectPath, {
|
|
65
|
-
status: 'complete',
|
|
66
|
-
limit: 20
|
|
67
|
-
});
|
|
68
|
-
// Find relevant tasks via file paths and keywords
|
|
69
|
-
const explicitFiles = extractFilePaths(input.prompt);
|
|
70
|
-
const fileTasks = explicitFiles.length > 0 && tasks.length > 0
|
|
71
|
-
? getTasksByFiles(projectPath, explicitFiles, { status: 'complete', limit: 10 })
|
|
72
|
-
: [];
|
|
73
|
-
const keywordTasks = tasks.length > 0
|
|
74
|
-
? findKeywordMatches(input.prompt, tasks)
|
|
75
|
-
: [];
|
|
76
|
-
// Also get file-level reasoning for mentioned files
|
|
77
|
-
const fileReasonings = explicitFiles.length > 0
|
|
78
|
-
? explicitFiles.flatMap(f => getFileReasoningByPathPattern(f, 5))
|
|
79
|
-
: [];
|
|
80
|
-
// Combine and deduplicate tasks
|
|
81
|
-
const relevantTasks = dedupeAndLimit([...fileTasks, ...keywordTasks], 5);
|
|
82
|
-
// Build context (past reasoning from team memory)
|
|
83
|
-
const memoryContext = (relevantTasks.length > 0 || fileReasonings.length > 0)
|
|
84
|
-
? buildPromptContext(relevantTasks, explicitFiles, fileReasonings)
|
|
85
|
-
: null;
|
|
86
|
-
// Combine correction and memory context
|
|
87
|
-
const combinedContext = buildCombinedContext(correctionText, memoryContext);
|
|
88
|
-
// Output if we have anything to inject
|
|
89
|
-
if (combinedContext) {
|
|
90
|
-
const output = {
|
|
91
|
-
hookSpecificOutput: {
|
|
92
|
-
hookEventName: "UserPromptSubmit",
|
|
93
|
-
additionalContext: combinedContext
|
|
94
|
-
}
|
|
95
|
-
};
|
|
96
|
-
console.log(JSON.stringify(output));
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
catch (error) {
|
|
100
|
-
// Silent fail - don't break user workflow
|
|
101
|
-
debugInject('prompt-inject error: %O', error);
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
/**
|
|
105
|
-
* Handle first prompt: extract intent and create session state
|
|
106
|
-
*/
|
|
107
|
-
async function handleFirstPrompt(prompt, projectPath, sessionId) {
|
|
108
|
-
try {
|
|
109
|
-
debugInject('First prompt detected, extracting intent...');
|
|
110
|
-
// Extract intent from prompt
|
|
111
|
-
const intent = await extractIntent(prompt);
|
|
112
|
-
// Create session state with intent
|
|
113
|
-
createSessionState({
|
|
114
|
-
session_id: sessionId,
|
|
115
|
-
project_path: projectPath,
|
|
116
|
-
original_goal: intent.goal,
|
|
117
|
-
expected_scope: intent.expected_scope,
|
|
118
|
-
constraints: intent.constraints,
|
|
119
|
-
success_criteria: intent.success_criteria,
|
|
120
|
-
keywords: intent.keywords
|
|
121
|
-
});
|
|
122
|
-
debugInject('Session state created: goal=%s', intent.goal.substring(0, 50));
|
|
123
|
-
// No correction needed for first prompt
|
|
124
|
-
return null;
|
|
125
|
-
}
|
|
126
|
-
catch (error) {
|
|
127
|
-
debugInject('handleFirstPrompt error: %O', error);
|
|
128
|
-
return null;
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
/**
|
|
132
|
-
* Handle drift check for subsequent prompts.
|
|
133
|
-
*
|
|
134
|
-
* CRITICAL: We check Claude's ACTIONS, not user prompts.
|
|
135
|
-
* 1. Parse session JSONL to get Claude's recent actions
|
|
136
|
-
* 2. If no modifying actions, skip drift check (user just asked a question - OK!)
|
|
137
|
-
* 3. Build drift input from ACTIONS
|
|
138
|
-
* 4. Run drift check
|
|
139
|
-
* 5. Save steps and update last_checked_at
|
|
140
|
-
*/
|
|
141
|
-
async function handleDriftCheck(projectPath, sessionState) {
|
|
142
|
-
try {
|
|
143
|
-
const sessionId = sessionState.session_id;
|
|
144
|
-
debugInject('Running drift check for session: %s', sessionId);
|
|
145
|
-
// 1. Find session JSONL file
|
|
146
|
-
const sessionPath = findSessionFile(sessionId, projectPath);
|
|
147
|
-
if (!sessionPath) {
|
|
148
|
-
debugInject('Session JSONL not found, skipping drift check');
|
|
149
|
-
return null;
|
|
150
|
-
}
|
|
151
|
-
// 2. Get Claude's actions since last check
|
|
152
|
-
const lastChecked = sessionState.last_checked_at || 0;
|
|
153
|
-
const claudeActions = getNewActions(sessionPath, lastChecked);
|
|
154
|
-
debugInject('Found %d new actions since last check', claudeActions.length);
|
|
155
|
-
// 3. If no new actions, user just asked a question - OK!
|
|
156
|
-
if (claudeActions.length === 0) {
|
|
157
|
-
debugInject('No new actions - user is exploring, not drift');
|
|
158
|
-
return null;
|
|
159
|
-
}
|
|
160
|
-
// 4. Filter to modifying actions only (read is always OK)
|
|
161
|
-
const modifyingActions = getModifyingActions(claudeActions);
|
|
162
|
-
if (modifyingActions.length === 0) {
|
|
163
|
-
debugInject('Only read actions - exploration, not drift');
|
|
164
|
-
// Still update last_checked to track progress
|
|
165
|
-
updateLastChecked(sessionId, Date.now());
|
|
166
|
-
return null;
|
|
167
|
-
}
|
|
168
|
-
// 5. Build drift input from ACTIONS (not prompt!)
|
|
169
|
-
const driftInput = buildDriftCheckInput(claudeActions, sessionId, sessionState);
|
|
170
|
-
// 6. Check drift
|
|
171
|
-
const driftResult = await checkDrift(driftInput);
|
|
172
|
-
debugInject('Drift check result: score=%d, type=%s', driftResult.score, driftResult.type);
|
|
173
|
-
// 7. Save steps to DB
|
|
174
|
-
for (const action of claudeActions) {
|
|
175
|
-
const isKeyDecision = driftResult.score >= 9 && action.type !== 'read';
|
|
176
|
-
const keywords = extractKeywordsFromAction(action);
|
|
177
|
-
saveStep(sessionId, action, driftResult.score, isKeyDecision, keywords);
|
|
178
|
-
}
|
|
179
|
-
// 8. Update last_checked timestamp
|
|
180
|
-
updateLastChecked(sessionId, Date.now());
|
|
181
|
-
// 9. Determine correction level
|
|
182
|
-
const level = determineCorrectionLevel(driftResult.score, sessionState.escalation_count);
|
|
183
|
-
debugInject('Correction level: %s', level || 'none');
|
|
184
|
-
// 10. Update session drift metrics
|
|
185
|
-
const actionsSummary = `${modifyingActions.length} modifying actions`;
|
|
186
|
-
updateSessionDrift(sessionId, driftResult.score, level, actionsSummary, driftResult.recoveryPlan);
|
|
187
|
-
// 11. Build correction if needed
|
|
188
|
-
if (level) {
|
|
189
|
-
return buildCorrection(driftResult, sessionState, level);
|
|
190
|
-
}
|
|
191
|
-
return null;
|
|
192
|
-
}
|
|
193
|
-
catch (error) {
|
|
194
|
-
debugInject('handleDriftCheck error: %O', error);
|
|
195
|
-
return null;
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
/**
|
|
199
|
-
* Combine correction and memory context
|
|
200
|
-
*/
|
|
201
|
-
function buildCombinedContext(correction, memoryContext) {
|
|
202
|
-
if (!correction && !memoryContext) {
|
|
203
|
-
return null;
|
|
204
|
-
}
|
|
205
|
-
const parts = [];
|
|
206
|
-
// Correction comes first (most important)
|
|
207
|
-
if (correction) {
|
|
208
|
-
parts.push(correction);
|
|
209
|
-
}
|
|
210
|
-
// Memory context second
|
|
211
|
-
if (memoryContext) {
|
|
212
|
-
parts.push(memoryContext);
|
|
213
|
-
}
|
|
214
|
-
return parts.join('\n\n');
|
|
215
|
-
}
|
|
216
|
-
/**
|
|
217
|
-
* Read JSON input from stdin with timeout and size limit.
|
|
218
|
-
* SECURITY: Limits input size to prevent memory exhaustion attacks.
|
|
219
|
-
* OPTIMIZED: Uses array + join instead of O(n²) string concatenation.
|
|
220
|
-
* FIXED: Properly cleans up event listeners to prevent memory leaks.
|
|
221
|
-
*/
|
|
222
|
-
async function readStdinInput() {
|
|
223
|
-
return new Promise((resolve) => {
|
|
224
|
-
const chunks = [];
|
|
225
|
-
let totalLength = 0;
|
|
226
|
-
let resolved = false;
|
|
227
|
-
// Cleanup function to remove all listeners
|
|
228
|
-
const cleanup = () => {
|
|
229
|
-
process.stdin.removeListener('readable', onReadable);
|
|
230
|
-
process.stdin.removeListener('end', onEnd);
|
|
231
|
-
process.stdin.removeListener('error', onError);
|
|
232
|
-
};
|
|
233
|
-
// Safe resolve that only resolves once and cleans up
|
|
234
|
-
const safeResolve = (value) => {
|
|
235
|
-
if (resolved)
|
|
236
|
-
return;
|
|
237
|
-
resolved = true;
|
|
238
|
-
clearTimeout(timeout);
|
|
239
|
-
cleanup();
|
|
240
|
-
resolve(value);
|
|
241
|
-
};
|
|
242
|
-
// Set a timeout to prevent hanging
|
|
243
|
-
const timeout = setTimeout(() => {
|
|
244
|
-
debugInject('stdin timeout reached');
|
|
245
|
-
safeResolve(null);
|
|
246
|
-
}, 3000); // 3 second timeout
|
|
247
|
-
process.stdin.setEncoding('utf-8');
|
|
248
|
-
const onReadable = () => {
|
|
249
|
-
let chunk;
|
|
250
|
-
while ((chunk = process.stdin.read()) !== null) {
|
|
251
|
-
totalLength += chunk.length;
|
|
252
|
-
// SECURITY: Check size limit
|
|
253
|
-
if (totalLength > MAX_STDIN_SIZE) {
|
|
254
|
-
debugInject('stdin size limit exceeded');
|
|
255
|
-
safeResolve(null);
|
|
256
|
-
return;
|
|
257
|
-
}
|
|
258
|
-
chunks.push(chunk);
|
|
259
|
-
}
|
|
260
|
-
};
|
|
261
|
-
const onEnd = () => {
|
|
262
|
-
try {
|
|
263
|
-
const data = chunks.join('');
|
|
264
|
-
const parsed = JSON.parse(data.trim());
|
|
265
|
-
// Validate required fields
|
|
266
|
-
if (!parsed || typeof parsed !== 'object') {
|
|
267
|
-
debugInject('Invalid stdin input: not an object');
|
|
268
|
-
safeResolve(null);
|
|
269
|
-
return;
|
|
270
|
-
}
|
|
271
|
-
const input = parsed;
|
|
272
|
-
if (typeof input.prompt !== 'string' || typeof input.cwd !== 'string') {
|
|
273
|
-
debugInject('Invalid stdin input: missing required fields');
|
|
274
|
-
safeResolve(null);
|
|
275
|
-
return;
|
|
276
|
-
}
|
|
277
|
-
safeResolve(input);
|
|
278
|
-
}
|
|
279
|
-
catch {
|
|
280
|
-
debugInject('Failed to parse stdin JSON');
|
|
281
|
-
safeResolve(null);
|
|
282
|
-
}
|
|
283
|
-
};
|
|
284
|
-
const onError = () => {
|
|
285
|
-
debugInject('stdin error');
|
|
286
|
-
safeResolve(null);
|
|
287
|
-
};
|
|
288
|
-
process.stdin.on('readable', onReadable);
|
|
289
|
-
process.stdin.on('end', onEnd);
|
|
290
|
-
process.stdin.on('error', onError);
|
|
291
|
-
});
|
|
292
|
-
}
|
|
293
|
-
/**
|
|
294
|
-
* Check if a prompt is simple and doesn't need context injection
|
|
295
|
-
*/
|
|
296
|
-
function isSimplePrompt(prompt) {
|
|
297
|
-
const normalized = prompt.trim().toLowerCase();
|
|
298
|
-
// Very short prompts are likely simple
|
|
299
|
-
if (normalized.length < 3)
|
|
300
|
-
return true;
|
|
301
|
-
// Check against simple prompt list
|
|
302
|
-
for (const simple of SIMPLE_PROMPTS) {
|
|
303
|
-
if (normalized === simple ||
|
|
304
|
-
normalized.startsWith(simple + ' ') ||
|
|
305
|
-
normalized.startsWith(simple + ',') ||
|
|
306
|
-
normalized.startsWith(simple + '.')) {
|
|
307
|
-
return true;
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
// If the prompt is very short and doesn't contain code-related words
|
|
311
|
-
if (normalized.length < 20 && !normalized.match(/\.(ts|js|py|go|rs|java|tsx|jsx)/)) {
|
|
312
|
-
// Check if it's just a simple acknowledgment
|
|
313
|
-
const words = normalized.split(/\s+/);
|
|
314
|
-
if (words.length <= 3) {
|
|
315
|
-
return true;
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
return false;
|
|
319
|
-
}
|
|
320
|
-
// PERFORMANCE: Pre-compiled regexes for file path extraction
|
|
321
|
-
const TOKEN_SPLIT_REGEX = /[\s,;:'"``]+/;
|
|
322
|
-
const URL_PATTERN = /^https?:\/\//i;
|
|
323
|
-
const FILE_PATTERN = /^[.\/]?[\w\-\/]+\.\w{1,5}$/;
|
|
324
|
-
const VERSION_PATTERN = /^\d+\.\d+/;
|
|
325
|
-
/**
|
|
326
|
-
* Extract file paths from a prompt.
|
|
327
|
-
* SECURITY: Uses simplified patterns to avoid ReDoS with pathological input.
|
|
328
|
-
* PERFORMANCE: Uses pre-compiled regexes for efficiency.
|
|
329
|
-
*/
|
|
330
|
-
function extractFilePaths(prompt) {
|
|
331
|
-
// SECURITY: Limit input length to prevent ReDoS
|
|
332
|
-
const safePrompt = prompt.length > 10000 ? prompt.substring(0, 10000) : prompt;
|
|
333
|
-
const files = new Set();
|
|
334
|
-
// Split by whitespace and common delimiters for simpler, safer matching
|
|
335
|
-
const tokens = safePrompt.split(TOKEN_SPLIT_REGEX);
|
|
336
|
-
for (const token of tokens) {
|
|
337
|
-
// Skip empty tokens and URLs
|
|
338
|
-
if (!token || URL_PATTERN.test(token))
|
|
339
|
-
continue;
|
|
340
|
-
// Match file-like patterns: must have extension
|
|
341
|
-
if (FILE_PATTERN.test(token)) {
|
|
342
|
-
// Filter out version numbers like 1.0.0
|
|
343
|
-
if (!VERSION_PATTERN.test(token)) {
|
|
344
|
-
files.add(token);
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
return [...files];
|
|
349
|
-
}
|
|
350
|
-
// SECURITY: Maximum keywords to prevent memory exhaustion
|
|
351
|
-
const MAX_KEYWORDS = 100;
|
|
352
|
-
/**
|
|
353
|
-
* Extract keywords from a prompt for matching against tasks.
|
|
354
|
-
* SECURITY: Limits keywords to MAX_KEYWORDS to prevent memory exhaustion.
|
|
355
|
-
*/
|
|
356
|
-
function extractKeywords(prompt) {
|
|
357
|
-
const words = prompt.toLowerCase()
|
|
358
|
-
.replace(/[^\w\s]/g, ' ')
|
|
359
|
-
.split(/\s+/)
|
|
360
|
-
.filter(w => w.length > 2 && !STOP_WORDS.has(w));
|
|
361
|
-
const uniqueWords = [...new Set(words)];
|
|
362
|
-
// SECURITY: Limit keywords to prevent memory exhaustion
|
|
363
|
-
return uniqueWords.length > MAX_KEYWORDS
|
|
364
|
-
? uniqueWords.slice(0, MAX_KEYWORDS)
|
|
365
|
-
: uniqueWords;
|
|
366
|
-
}
|
|
367
|
-
/**
|
|
368
|
-
* Find tasks that match keywords from the prompt.
|
|
369
|
-
* OPTIMIZED: O(n) instead of O(n²) - builds keyword set once.
|
|
370
|
-
*/
|
|
371
|
-
function findKeywordMatches(prompt, tasks) {
|
|
372
|
-
const keywords = extractKeywords(prompt);
|
|
373
|
-
if (keywords.length === 0) {
|
|
374
|
-
return [];
|
|
375
|
-
}
|
|
376
|
-
// Build keyword set for O(1) lookups
|
|
377
|
-
const keywordSet = new Set(keywords);
|
|
378
|
-
return tasks.filter(task => {
|
|
379
|
-
// Match against task tags - O(tags) lookup
|
|
380
|
-
const tagMatch = task.tags.some(tag => {
|
|
381
|
-
const lowerTag = tag.toLowerCase();
|
|
382
|
-
return keywordSet.has(lowerTag) ||
|
|
383
|
-
keywords.some(kw => lowerTag.includes(kw) || kw.includes(lowerTag));
|
|
384
|
-
});
|
|
385
|
-
if (tagMatch)
|
|
386
|
-
return true;
|
|
387
|
-
// Match against goal - extract once per task
|
|
388
|
-
const goalWords = (task.goal || '').toLowerCase().split(/\s+/).filter(w => w.length > 2);
|
|
389
|
-
const goalMatch = goalWords.some(gw => keywordSet.has(gw));
|
|
390
|
-
if (goalMatch)
|
|
391
|
-
return true;
|
|
392
|
-
// Match against original query - extract once per task
|
|
393
|
-
const queryWords = task.original_query.toLowerCase().split(/\s+/).filter(w => w.length > 2);
|
|
394
|
-
const queryMatch = queryWords.some(qw => keywordSet.has(qw));
|
|
395
|
-
return queryMatch;
|
|
396
|
-
});
|
|
397
|
-
}
|
|
398
|
-
/**
|
|
399
|
-
* Deduplicate tasks by ID and limit count
|
|
400
|
-
*/
|
|
401
|
-
function dedupeAndLimit(tasks, limit) {
|
|
402
|
-
const seen = new Set();
|
|
403
|
-
const unique = [];
|
|
404
|
-
for (const task of tasks) {
|
|
405
|
-
if (!seen.has(task.id)) {
|
|
406
|
-
seen.add(task.id);
|
|
407
|
-
unique.push(task);
|
|
408
|
-
if (unique.length >= limit)
|
|
409
|
-
break;
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
return unique;
|
|
413
|
-
}
|
|
414
|
-
/**
|
|
415
|
-
* Build context string for prompt injection
|
|
416
|
-
*/
|
|
417
|
-
function buildPromptContext(tasks, files, fileReasonings) {
|
|
418
|
-
const lines = [];
|
|
419
|
-
lines.push('[GROV CONTEXT - Relevant past reasoning]');
|
|
420
|
-
lines.push('');
|
|
421
|
-
// Add file-specific reasoning if available
|
|
422
|
-
if (fileReasonings.length > 0) {
|
|
423
|
-
lines.push('File-level context:');
|
|
424
|
-
for (const fr of fileReasonings.slice(0, 5)) {
|
|
425
|
-
const anchor = fr.anchor ? ` (${fr.anchor})` : '';
|
|
426
|
-
lines.push(`- ${fr.file_path}${anchor}: ${truncate(fr.reasoning, 100)}`);
|
|
427
|
-
}
|
|
428
|
-
lines.push('');
|
|
429
|
-
}
|
|
430
|
-
// Add task context
|
|
431
|
-
if (tasks.length > 0) {
|
|
432
|
-
lines.push('Related past tasks:');
|
|
433
|
-
for (const task of tasks) {
|
|
434
|
-
lines.push(`- ${truncate(task.original_query, 60)}`);
|
|
435
|
-
if (task.files_touched.length > 0) {
|
|
436
|
-
const fileList = task.files_touched.slice(0, 3).map(f => f.split('/').pop()).join(', ');
|
|
437
|
-
lines.push(` Files: ${fileList}`);
|
|
438
|
-
}
|
|
439
|
-
if (task.reasoning_trace.length > 0) {
|
|
440
|
-
lines.push(` Key: ${truncate(task.reasoning_trace[0], 80)}`);
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
lines.push('');
|
|
444
|
-
}
|
|
445
|
-
// Add instruction
|
|
446
|
-
if (files.length > 0) {
|
|
447
|
-
lines.push(`You may already have context for: ${files.join(', ')}`);
|
|
448
|
-
}
|
|
449
|
-
lines.push('[END GROV CONTEXT]');
|
|
450
|
-
return lines.join('\n');
|
|
451
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function unregister(): Promise<void>;
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
// grov unregister - Remove hooks from Claude Code settings
|
|
2
|
-
import { unregisterGrovHooks, getSettingsPath, setProxyEnv } from '../lib/hooks.js';
|
|
3
|
-
export async function unregister() {
|
|
4
|
-
console.log('Removing grov hooks from Claude Code...\n');
|
|
5
|
-
try {
|
|
6
|
-
const { removed } = unregisterGrovHooks();
|
|
7
|
-
if (removed.length > 0) {
|
|
8
|
-
console.log('Removed hooks:');
|
|
9
|
-
removed.forEach(hook => console.log(` - ${hook}`));
|
|
10
|
-
}
|
|
11
|
-
else {
|
|
12
|
-
console.log('No grov hooks found to remove.');
|
|
13
|
-
}
|
|
14
|
-
// Remove proxy URL from settings
|
|
15
|
-
const proxyResult = setProxyEnv(false);
|
|
16
|
-
if (proxyResult.action === 'removed') {
|
|
17
|
-
console.log('\nProxy configuration:');
|
|
18
|
-
console.log(' - ANTHROPIC_BASE_URL removed');
|
|
19
|
-
}
|
|
20
|
-
console.log(`\nSettings file: ${getSettingsPath()}`);
|
|
21
|
-
console.log('\nGrov hooks have been disabled.');
|
|
22
|
-
console.log('Your stored reasoning data remains in ~/.grov/memory.db');
|
|
23
|
-
}
|
|
24
|
-
catch (error) {
|
|
25
|
-
console.error('Failed to unregister hooks:', error);
|
|
26
|
-
process.exit(1);
|
|
27
|
-
}
|
|
28
|
-
}
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
export type AnchorType = 'function' | 'class' | 'method' | 'variable' | 'unknown';
|
|
2
|
-
export interface AnchorInfo {
|
|
3
|
-
type: AnchorType;
|
|
4
|
-
name: string;
|
|
5
|
-
lineStart: number;
|
|
6
|
-
lineEnd?: number;
|
|
7
|
-
}
|
|
8
|
-
/**
|
|
9
|
-
* Extract all anchors from a source file.
|
|
10
|
-
* PERFORMANCE: Uses single-pass O(n) algorithm instead of O(n²).
|
|
11
|
-
* SECURITY: Limits anchors to prevent DoS with pathological files.
|
|
12
|
-
*/
|
|
13
|
-
export declare function extractAnchors(filePath: string, content: string): AnchorInfo[];
|
|
14
|
-
/**
|
|
15
|
-
* Find which anchor contains a given line number
|
|
16
|
-
*/
|
|
17
|
-
export declare function findAnchorAtLine(anchors: AnchorInfo[], lineNumber: number): AnchorInfo | null;
|
|
18
|
-
/**
|
|
19
|
-
* Compute a hash of a code region for change detection.
|
|
20
|
-
* Uses SHA-256 (truncated) for security scanner compliance.
|
|
21
|
-
*/
|
|
22
|
-
export declare function computeCodeHash(content: string, lineStart: number, lineEnd: number): string;
|
|
23
|
-
/**
|
|
24
|
-
* Estimate the line number where a string appears in content
|
|
25
|
-
*/
|
|
26
|
-
export declare function estimateLineNumber(searchString: string, content: string): number | null;
|
|
27
|
-
/**
|
|
28
|
-
* Get a human-readable description of an anchor
|
|
29
|
-
*/
|
|
30
|
-
export declare function describeAnchor(anchor: AnchorInfo): string;
|