grov 0.1.1 → 0.2.2

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/README.md +66 -87
  2. package/dist/cli.js +23 -37
  3. package/dist/commands/capture.js +1 -1
  4. package/dist/commands/disable.d.ts +1 -0
  5. package/dist/commands/disable.js +14 -0
  6. package/dist/commands/drift-test.js +56 -68
  7. package/dist/commands/init.js +29 -17
  8. package/dist/commands/proxy-status.d.ts +1 -0
  9. package/dist/commands/proxy-status.js +32 -0
  10. package/dist/commands/unregister.js +7 -1
  11. package/dist/lib/correction-builder-proxy.d.ts +16 -0
  12. package/dist/lib/correction-builder-proxy.js +125 -0
  13. package/dist/lib/correction-builder.js +1 -1
  14. package/dist/lib/drift-checker-proxy.d.ts +63 -0
  15. package/dist/lib/drift-checker-proxy.js +373 -0
  16. package/dist/lib/drift-checker.js +1 -1
  17. package/dist/lib/hooks.d.ts +11 -0
  18. package/dist/lib/hooks.js +33 -0
  19. package/dist/lib/llm-extractor.d.ts +60 -11
  20. package/dist/lib/llm-extractor.js +419 -98
  21. package/dist/lib/settings.d.ts +19 -0
  22. package/dist/lib/settings.js +63 -0
  23. package/dist/lib/store.d.ts +201 -43
  24. package/dist/lib/store.js +653 -90
  25. package/dist/proxy/action-parser.d.ts +58 -0
  26. package/dist/proxy/action-parser.js +196 -0
  27. package/dist/proxy/config.d.ts +26 -0
  28. package/dist/proxy/config.js +67 -0
  29. package/dist/proxy/forwarder.d.ts +24 -0
  30. package/dist/proxy/forwarder.js +119 -0
  31. package/dist/proxy/index.d.ts +1 -0
  32. package/dist/proxy/index.js +30 -0
  33. package/dist/proxy/request-processor.d.ts +12 -0
  34. package/dist/proxy/request-processor.js +94 -0
  35. package/dist/proxy/response-processor.d.ts +14 -0
  36. package/dist/proxy/response-processor.js +128 -0
  37. package/dist/proxy/server.d.ts +9 -0
  38. package/dist/proxy/server.js +911 -0
  39. package/package.json +10 -4
@@ -0,0 +1,58 @@
1
+ import type { StepActionType } from '../lib/store.js';
2
+ export interface AnthropicResponse {
3
+ id: string;
4
+ type: 'message';
5
+ role: 'assistant';
6
+ content: ContentBlock[];
7
+ model: string;
8
+ stop_reason: string | null;
9
+ stop_sequence: string | null;
10
+ usage: {
11
+ input_tokens: number;
12
+ output_tokens: number;
13
+ };
14
+ }
15
+ export type ContentBlock = TextBlock | ToolUseBlock;
16
+ export interface TextBlock {
17
+ type: 'text';
18
+ text: string;
19
+ }
20
+ export interface ToolUseBlock {
21
+ type: 'tool_use';
22
+ id: string;
23
+ name: string;
24
+ input: Record<string, unknown>;
25
+ }
26
+ export interface ParsedAction {
27
+ toolName: string;
28
+ toolId: string;
29
+ actionType: StepActionType;
30
+ files: string[];
31
+ folders: string[];
32
+ command?: string;
33
+ rawInput: Record<string, unknown>;
34
+ }
35
+ /**
36
+ * Parse tool_use blocks from Anthropic API response
37
+ */
38
+ export declare function parseToolUseBlocks(response: AnthropicResponse): ParsedAction[];
39
+ /**
40
+ * Extract token usage from response
41
+ */
42
+ export declare function extractTokenUsage(response: AnthropicResponse): {
43
+ inputTokens: number;
44
+ outputTokens: number;
45
+ totalTokens: number;
46
+ };
47
+ /**
48
+ * Check if response contains any file-modifying actions
49
+ */
50
+ export declare function hasModifyingActions(actions: ParsedAction[]): boolean;
51
+ /**
52
+ * Get all unique files from actions
53
+ */
54
+ export declare function getAllFiles(actions: ParsedAction[]): string[];
55
+ /**
56
+ * Get all unique folders from actions
57
+ */
58
+ export declare function getAllFolders(actions: ParsedAction[]): string[];
@@ -0,0 +1,196 @@
1
+ // Parse tool_use blocks from Anthropic API response
2
+ // Replaces JSONL parsing - works with API response JSON directly
3
+ // Tool name to action type mapping
4
+ const TOOL_ACTION_MAP = {
5
+ 'Edit': 'edit',
6
+ 'Write': 'write',
7
+ 'Read': 'read',
8
+ 'Bash': 'bash',
9
+ 'Glob': 'glob',
10
+ 'Grep': 'grep',
11
+ 'Task': 'task',
12
+ 'MultiEdit': 'edit',
13
+ 'NotebookEdit': 'edit',
14
+ };
15
+ /**
16
+ * Parse tool_use blocks from Anthropic API response
17
+ */
18
+ export function parseToolUseBlocks(response) {
19
+ const actions = [];
20
+ for (const block of response.content) {
21
+ if (block.type === 'tool_use') {
22
+ const action = parseToolUseBlock(block);
23
+ if (action) {
24
+ actions.push(action);
25
+ }
26
+ }
27
+ }
28
+ return actions;
29
+ }
30
+ /**
31
+ * Parse a single tool_use block
32
+ */
33
+ function parseToolUseBlock(block) {
34
+ const actionType = TOOL_ACTION_MAP[block.name] || 'other';
35
+ const files = [];
36
+ const folders = [];
37
+ let command;
38
+ // Extract file paths based on tool type
39
+ switch (block.name) {
40
+ case 'Edit':
41
+ case 'Write':
42
+ case 'Read':
43
+ case 'NotebookEdit':
44
+ if (typeof block.input.file_path === 'string') {
45
+ files.push(block.input.file_path);
46
+ }
47
+ if (typeof block.input.notebook_path === 'string') {
48
+ files.push(block.input.notebook_path);
49
+ }
50
+ break;
51
+ case 'MultiEdit':
52
+ // MultiEdit has an array of edits
53
+ if (Array.isArray(block.input.edits)) {
54
+ for (const edit of block.input.edits) {
55
+ if (typeof edit === 'object' && edit && typeof edit.file_path === 'string') {
56
+ files.push(edit.file_path);
57
+ }
58
+ }
59
+ }
60
+ break;
61
+ case 'Bash':
62
+ if (typeof block.input.command === 'string') {
63
+ command = block.input.command;
64
+ // Try to extract file paths from common command patterns
65
+ const bashFiles = extractFilesFromBashCommand(command);
66
+ files.push(...bashFiles);
67
+ }
68
+ break;
69
+ case 'Glob':
70
+ if (typeof block.input.path === 'string') {
71
+ folders.push(block.input.path);
72
+ }
73
+ if (typeof block.input.pattern === 'string') {
74
+ // pattern might contain path info
75
+ const patternPath = extractPathFromGlobPattern(block.input.pattern);
76
+ if (patternPath) {
77
+ folders.push(patternPath);
78
+ }
79
+ }
80
+ break;
81
+ case 'Grep':
82
+ if (typeof block.input.path === 'string') {
83
+ folders.push(block.input.path);
84
+ }
85
+ break;
86
+ }
87
+ return {
88
+ toolName: block.name,
89
+ toolId: block.id,
90
+ actionType,
91
+ files: [...new Set(files)],
92
+ folders: [...new Set(folders)],
93
+ command,
94
+ rawInput: block.input
95
+ };
96
+ }
97
+ /**
98
+ * Extract file paths from bash command
99
+ */
100
+ function extractFilesFromBashCommand(command) {
101
+ const files = [];
102
+ // Match absolute paths
103
+ const absolutePathRegex = /(?:^|\s)(\/[^\s"']+)/g;
104
+ let match;
105
+ while ((match = absolutePathRegex.exec(command)) !== null) {
106
+ const path = match[1];
107
+ // Filter out common non-file paths
108
+ if (!path.startsWith('/dev/') && !path.startsWith('/proc/') && !path.startsWith('/sys/')) {
109
+ files.push(path);
110
+ }
111
+ }
112
+ // Match quoted paths
113
+ const quotedPathRegex = /["'](\/[^"']+)["']/g;
114
+ while ((match = quotedPathRegex.exec(command)) !== null) {
115
+ files.push(match[1]);
116
+ }
117
+ return files;
118
+ }
119
+ /**
120
+ * Extract base path from glob pattern
121
+ */
122
+ function extractPathFromGlobPattern(pattern) {
123
+ // e.g., "src/**/*.ts" -> "src"
124
+ const parts = pattern.split('/');
125
+ const nonGlobParts = [];
126
+ for (const part of parts) {
127
+ if (part.includes('*') || part.includes('?') || part.includes('[')) {
128
+ break;
129
+ }
130
+ nonGlobParts.push(part);
131
+ }
132
+ return nonGlobParts.length > 0 ? nonGlobParts.join('/') : null;
133
+ }
134
+ /**
135
+ * Extract token usage from response
136
+ */
137
+ export function extractTokenUsage(response) {
138
+ return {
139
+ inputTokens: response.usage.input_tokens,
140
+ outputTokens: response.usage.output_tokens,
141
+ totalTokens: response.usage.input_tokens + response.usage.output_tokens
142
+ };
143
+ }
144
+ /**
145
+ * Check if response contains any file-modifying actions
146
+ */
147
+ export function hasModifyingActions(actions) {
148
+ return actions.some(a => a.actionType === 'edit' ||
149
+ a.actionType === 'write' ||
150
+ (a.actionType === 'bash' && a.command && isModifyingBashCommand(a.command)));
151
+ }
152
+ /**
153
+ * Check if bash command modifies files
154
+ */
155
+ function isModifyingBashCommand(command) {
156
+ const modifyingPatterns = [
157
+ /\brm\b/,
158
+ /\bmv\b/,
159
+ /\bcp\b/,
160
+ /\bmkdir\b/,
161
+ /\btouch\b/,
162
+ /\bchmod\b/,
163
+ /\bchown\b/,
164
+ /\bsed\b.*-i/,
165
+ /\btee\b/,
166
+ />/, // redirect
167
+ /\bgit\s+(add|commit|push|checkout|reset)/,
168
+ /\bnpm\s+(install|uninstall)/,
169
+ /\byarn\s+(add|remove)/,
170
+ ];
171
+ return modifyingPatterns.some(p => p.test(command));
172
+ }
173
+ /**
174
+ * Get all unique files from actions
175
+ */
176
+ export function getAllFiles(actions) {
177
+ const files = new Set();
178
+ for (const action of actions) {
179
+ for (const file of action.files) {
180
+ files.add(file);
181
+ }
182
+ }
183
+ return [...files];
184
+ }
185
+ /**
186
+ * Get all unique folders from actions
187
+ */
188
+ export function getAllFolders(actions) {
189
+ const folders = new Set();
190
+ for (const action of actions) {
191
+ for (const folder of action.folders) {
192
+ folders.add(folder);
193
+ }
194
+ }
195
+ return [...folders];
196
+ }
@@ -0,0 +1,26 @@
1
+ export declare const config: {
2
+ HOST: string;
3
+ PORT: number;
4
+ ANTHROPIC_BASE_URL: string;
5
+ REQUEST_TIMEOUT: number;
6
+ BODY_LIMIT: number;
7
+ DRIFT_CHECK_INTERVAL: number;
8
+ TOKEN_WARNING_THRESHOLD: number;
9
+ TOKEN_CLEAR_THRESHOLD: number;
10
+ ENABLE_AUTH: boolean;
11
+ ENABLE_RATE_LIMIT: boolean;
12
+ ENABLE_TLS: boolean;
13
+ LOG_LEVEL: string;
14
+ LOG_REQUESTS: boolean;
15
+ };
16
+ export declare const FORWARD_HEADERS: string[];
17
+ export declare const SENSITIVE_HEADERS: string[];
18
+ /**
19
+ * Mask sensitive header value for logging
20
+ */
21
+ export declare function maskSensitiveValue(key: string, value: string): string;
22
+ /**
23
+ * Build safe headers for forwarding
24
+ * Handles case-insensitive header matching
25
+ */
26
+ export declare function buildSafeHeaders(incomingHeaders: Record<string, string | string[] | undefined>): Record<string, string>;
@@ -0,0 +1,67 @@
1
+ // Proxy configuration
2
+ export const config = {
3
+ // Server
4
+ HOST: process.env.PROXY_HOST || '127.0.0.1',
5
+ PORT: parseInt(process.env.PROXY_PORT || '8080', 10),
6
+ // Anthropic target
7
+ ANTHROPIC_BASE_URL: process.env.ANTHROPIC_TARGET || 'https://api.anthropic.com',
8
+ // Timeouts
9
+ REQUEST_TIMEOUT: parseInt(process.env.REQUEST_TIMEOUT || '300000', 10), // 5 minutes
10
+ BODY_LIMIT: parseInt(process.env.BODY_LIMIT || '10485760', 10), // 10MB
11
+ // Drift settings
12
+ DRIFT_CHECK_INTERVAL: parseInt(process.env.DRIFT_CHECK_INTERVAL || '3', 10),
13
+ TOKEN_WARNING_THRESHOLD: parseInt(process.env.TOKEN_WARNING_THRESHOLD || '160000', 10), // 80%
14
+ TOKEN_CLEAR_THRESHOLD: parseInt(process.env.TOKEN_CLEAR_THRESHOLD || '180000', 10), // 90%
15
+ // Security (Phase 2 - disabled for local)
16
+ ENABLE_AUTH: process.env.ENABLE_AUTH === 'true',
17
+ ENABLE_RATE_LIMIT: process.env.ENABLE_RATE_LIMIT === 'true',
18
+ ENABLE_TLS: process.env.ENABLE_TLS === 'true',
19
+ // Logging
20
+ LOG_LEVEL: process.env.LOG_LEVEL || 'info',
21
+ LOG_REQUESTS: process.env.LOG_REQUESTS !== 'false',
22
+ };
23
+ // Headers to forward to Anthropic (whitelist approach)
24
+ export const FORWARD_HEADERS = [
25
+ 'x-api-key',
26
+ 'authorization', // Claude Code uses this instead of x-api-key
27
+ 'anthropic-version',
28
+ 'content-type',
29
+ 'anthropic-beta',
30
+ ];
31
+ // Headers to never log
32
+ export const SENSITIVE_HEADERS = [
33
+ 'x-api-key',
34
+ 'authorization',
35
+ ];
36
+ /**
37
+ * Mask sensitive header value for logging
38
+ */
39
+ export function maskSensitiveValue(key, value) {
40
+ const lowerKey = key.toLowerCase();
41
+ if (SENSITIVE_HEADERS.includes(lowerKey)) {
42
+ if (value.length <= 10) {
43
+ return '***';
44
+ }
45
+ return value.substring(0, 7) + '...' + value.substring(value.length - 4);
46
+ }
47
+ return value;
48
+ }
49
+ /**
50
+ * Build safe headers for forwarding
51
+ * Handles case-insensitive header matching
52
+ */
53
+ export function buildSafeHeaders(incomingHeaders) {
54
+ const safe = {};
55
+ // Create lowercase map of incoming headers
56
+ const lowerHeaders = {};
57
+ for (const [key, value] of Object.entries(incomingHeaders)) {
58
+ lowerHeaders[key.toLowerCase()] = value;
59
+ }
60
+ for (const header of FORWARD_HEADERS) {
61
+ const value = lowerHeaders[header.toLowerCase()];
62
+ if (value) {
63
+ safe[header] = Array.isArray(value) ? value[0] : value;
64
+ }
65
+ }
66
+ return safe;
67
+ }
@@ -0,0 +1,24 @@
1
+ import type { AnthropicResponse } from './action-parser.js';
2
+ export interface ForwardResult {
3
+ statusCode: number;
4
+ headers: Record<string, string | string[]>;
5
+ body: AnthropicResponse | Record<string, unknown>;
6
+ rawBody: string;
7
+ }
8
+ export interface ForwardError {
9
+ type: 'timeout' | 'network' | 'parse' | 'unknown';
10
+ message: string;
11
+ statusCode?: number;
12
+ }
13
+ /**
14
+ * Forward request to Anthropic API
15
+ * Buffers full response for processing
16
+ */
17
+ export declare function forwardToAnthropic(body: Record<string, unknown>, headers: Record<string, string | string[] | undefined>, logger?: {
18
+ info: (msg: string, data?: Record<string, unknown>) => void;
19
+ error: (msg: string, data?: Record<string, unknown>) => void;
20
+ }): Promise<ForwardResult>;
21
+ /**
22
+ * Check if error is a ForwardError
23
+ */
24
+ export declare function isForwardError(error: unknown): error is ForwardError;
@@ -0,0 +1,119 @@
1
+ // Forward requests to Anthropic API using undici
2
+ import { request, Agent } from 'undici';
3
+ // Custom agent with longer connect timeout and better IPv4/IPv6 handling
4
+ const agent = new Agent({
5
+ connect: {
6
+ timeout: 30000, // 30s connect timeout
7
+ },
8
+ // autoSelectFamily helps when IPv6 isn't working properly
9
+ autoSelectFamily: true,
10
+ autoSelectFamilyAttemptTimeout: 500, // Try next address family after 500ms
11
+ });
12
+ import { config, buildSafeHeaders, maskSensitiveValue } from './config.js';
13
+ /**
14
+ * Forward request to Anthropic API
15
+ * Buffers full response for processing
16
+ */
17
+ export async function forwardToAnthropic(body, headers, logger) {
18
+ const targetUrl = `${config.ANTHROPIC_BASE_URL}/v1/messages`;
19
+ const safeHeaders = buildSafeHeaders(headers);
20
+ // Log request (mask sensitive data)
21
+ if (logger && config.LOG_REQUESTS) {
22
+ const maskedHeaders = {};
23
+ for (const [key, value] of Object.entries(safeHeaders)) {
24
+ maskedHeaders[key] = maskSensitiveValue(key, value);
25
+ }
26
+ logger.info('Forwarding to Anthropic', {
27
+ url: targetUrl,
28
+ model: body.model,
29
+ messageCount: Array.isArray(body.messages) ? body.messages.length : 0,
30
+ headers: maskedHeaders,
31
+ });
32
+ }
33
+ try {
34
+ const response = await request(targetUrl, {
35
+ method: 'POST',
36
+ headers: {
37
+ ...safeHeaders,
38
+ 'content-type': 'application/json',
39
+ },
40
+ body: JSON.stringify(body),
41
+ bodyTimeout: config.REQUEST_TIMEOUT,
42
+ headersTimeout: config.REQUEST_TIMEOUT,
43
+ dispatcher: agent,
44
+ });
45
+ // Buffer the full response
46
+ const chunks = [];
47
+ for await (const chunk of response.body) {
48
+ chunks.push(Buffer.from(chunk));
49
+ }
50
+ const rawBody = Buffer.concat(chunks).toString('utf-8');
51
+ // Parse response
52
+ let parsedBody;
53
+ try {
54
+ parsedBody = JSON.parse(rawBody);
55
+ }
56
+ catch {
57
+ // Return raw body if not JSON
58
+ parsedBody = { error: 'Invalid JSON response', raw: rawBody.substring(0, 500) };
59
+ }
60
+ // Convert headers to record
61
+ const responseHeaders = {};
62
+ for (const [key, value] of Object.entries(response.headers)) {
63
+ if (value !== undefined) {
64
+ responseHeaders[key] = value;
65
+ }
66
+ }
67
+ if (logger && config.LOG_REQUESTS) {
68
+ logger.info('Received from Anthropic', {
69
+ statusCode: response.statusCode,
70
+ bodyLength: rawBody.length,
71
+ hasUsage: 'usage' in parsedBody,
72
+ });
73
+ }
74
+ return {
75
+ statusCode: response.statusCode,
76
+ headers: responseHeaders,
77
+ body: parsedBody,
78
+ rawBody,
79
+ };
80
+ }
81
+ catch (error) {
82
+ const err = error;
83
+ if (logger) {
84
+ logger.error('Forward error', {
85
+ message: err.message,
86
+ code: err.code,
87
+ });
88
+ }
89
+ // Handle specific error types
90
+ if (err.code === 'UND_ERR_HEADERS_TIMEOUT' || err.code === 'UND_ERR_BODY_TIMEOUT') {
91
+ throw createForwardError('timeout', 'Request to Anthropic timed out', 504);
92
+ }
93
+ if (err.code === 'ECONNREFUSED' || err.code === 'ENOTFOUND') {
94
+ throw createForwardError('network', 'Cannot connect to Anthropic API', 502);
95
+ }
96
+ if (err.code === 'ECONNRESET' || err.message?.includes('ECONNRESET')) {
97
+ throw createForwardError('network', 'Connection reset by Anthropic API', 502);
98
+ }
99
+ if (err.code === 'UND_ERR_CONNECT_TIMEOUT' || err.message?.includes('Connect Timeout')) {
100
+ throw createForwardError('timeout', 'Connection to Anthropic API timed out', 504);
101
+ }
102
+ throw createForwardError('unknown', err.message || 'Unknown error', 502);
103
+ }
104
+ }
105
+ /**
106
+ * Create a typed forward error
107
+ */
108
+ function createForwardError(type, message, statusCode) {
109
+ return { type, message, statusCode };
110
+ }
111
+ /**
112
+ * Check if error is a ForwardError
113
+ */
114
+ export function isForwardError(error) {
115
+ return (typeof error === 'object' &&
116
+ error !== null &&
117
+ 'type' in error &&
118
+ 'message' in error);
119
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,30 @@
1
+ // Grov Proxy CLI entry point
2
+ // Load .env file for API keys
3
+ import { config } from 'dotenv';
4
+ import { join } from 'path';
5
+ import { homedir } from 'os';
6
+ import { existsSync } from 'fs';
7
+ // Load from current directory .env first
8
+ config();
9
+ // Also load ~/.grov/.env as fallback
10
+ const grovEnvPath = join(homedir(), '.grov', '.env');
11
+ if (existsSync(grovEnvPath)) {
12
+ config({ path: grovEnvPath });
13
+ }
14
+ import { startServer } from './server.js';
15
+ // Check for API key before starting proxy
16
+ if (!process.env.ANTHROPIC_API_KEY) {
17
+ console.error('Error: ANTHROPIC_API_KEY is required to run the proxy.\n');
18
+ console.error('To set it up:\n');
19
+ console.error(' 1. Get your API key at:');
20
+ console.error(' https://console.anthropic.com/settings/keys\n');
21
+ console.error(' 2. Add to ~/.zshrc (or ~/.bashrc):');
22
+ console.error(' export ANTHROPIC_API_KEY=sk-ant-...\n');
23
+ console.error(' 3. Restart terminal or run: source ~/.zshrc\n');
24
+ console.error('Then try again: grov proxy');
25
+ process.exit(1);
26
+ }
27
+ startServer().catch((err) => {
28
+ console.error('Proxy failed:', err);
29
+ process.exit(1);
30
+ });
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Build context from team memory for injection
3
+ * Queries tasks and file_reasoning tables
4
+ */
5
+ export declare function buildTeamMemoryContext(projectPath: string, mentionedFiles: string[]): string | null;
6
+ /**
7
+ * Extract file paths from messages
8
+ */
9
+ export declare function extractFilesFromMessages(messages: Array<{
10
+ role: string;
11
+ content: unknown;
12
+ }>): string[];
@@ -0,0 +1,94 @@
1
+ // Request processor - handles context injection from team memory
2
+ // Reference: plan_proxy_local.md Section 2.1
3
+ import { getTasksForProject, getTasksByFiles, getStepsReasoningByPath, } from '../lib/store.js';
4
+ import { truncate } from '../lib/utils.js';
5
+ /**
6
+ * Build context from team memory for injection
7
+ * Queries tasks and file_reasoning tables
8
+ */
9
+ export function buildTeamMemoryContext(projectPath, mentionedFiles) {
10
+ // Get recent completed tasks for this project
11
+ const tasks = getTasksForProject(projectPath, {
12
+ status: 'complete',
13
+ limit: 10,
14
+ });
15
+ // Get tasks that touched mentioned files
16
+ const fileTasks = mentionedFiles.length > 0
17
+ ? getTasksByFiles(projectPath, mentionedFiles, { status: 'complete', limit: 5 })
18
+ : [];
19
+ // Get file-level reasoning from steps table (proxy version)
20
+ const fileReasonings = mentionedFiles.length > 0
21
+ ? mentionedFiles.flatMap(f => getStepsReasoningByPath(f, 3))
22
+ : [];
23
+ // Combine unique tasks
24
+ const allTasks = [...new Map([...tasks, ...fileTasks].map(t => [t.id, t])).values()];
25
+ if (allTasks.length === 0 && fileReasonings.length === 0) {
26
+ return null;
27
+ }
28
+ return formatTeamMemoryContext(allTasks, fileReasonings, mentionedFiles);
29
+ }
30
+ /**
31
+ * Format team memory context for injection
32
+ */
33
+ function formatTeamMemoryContext(tasks, fileReasonings, files) {
34
+ const lines = [];
35
+ lines.push('[GROV CONTEXT - Relevant past reasoning]');
36
+ lines.push('');
37
+ // File-level context
38
+ if (fileReasonings.length > 0) {
39
+ lines.push('File-level context:');
40
+ for (const fr of fileReasonings.slice(0, 5)) {
41
+ const anchor = fr.anchor ? ` (${fr.anchor})` : '';
42
+ lines.push(`- ${fr.file_path}${anchor}: ${truncate(fr.reasoning, 100)}`);
43
+ }
44
+ lines.push('');
45
+ }
46
+ // Task context with decisions and constraints
47
+ if (tasks.length > 0) {
48
+ lines.push('Related past tasks:');
49
+ for (const task of tasks.slice(0, 5)) {
50
+ lines.push(`- ${truncate(task.original_query, 60)}`);
51
+ if (task.files_touched.length > 0) {
52
+ const fileList = task.files_touched.slice(0, 3).map(f => f.split('/').pop()).join(', ');
53
+ lines.push(` Files: ${fileList}`);
54
+ }
55
+ if (task.reasoning_trace.length > 0) {
56
+ lines.push(` Key: ${truncate(task.reasoning_trace[0], 80)}`);
57
+ }
58
+ // Include decisions if available
59
+ if (task.decisions && task.decisions.length > 0) {
60
+ lines.push(` Decision: ${task.decisions[0].choice} (${truncate(task.decisions[0].reason, 50)})`);
61
+ }
62
+ // Include constraints if available
63
+ if (task.constraints && task.constraints.length > 0) {
64
+ lines.push(` Constraints: ${task.constraints.slice(0, 2).join(', ')}`);
65
+ }
66
+ }
67
+ lines.push('');
68
+ }
69
+ if (files.length > 0) {
70
+ lines.push(`You may already have context for: ${files.join(', ')}`);
71
+ }
72
+ lines.push('[END GROV CONTEXT]');
73
+ return lines.join('\n');
74
+ }
75
+ /**
76
+ * Extract file paths from messages
77
+ */
78
+ export function extractFilesFromMessages(messages) {
79
+ const files = [];
80
+ const filePattern = /(?:^|\s|["'`])([\/\w.-]+\.[a-zA-Z]{1,10})(?:["'`]|\s|$|:|\))/g;
81
+ for (const msg of messages) {
82
+ if (typeof msg.content === 'string') {
83
+ let match;
84
+ while ((match = filePattern.exec(msg.content)) !== null) {
85
+ const path = match[1];
86
+ // Filter out common false positives
87
+ if (!path.includes('http') && !path.startsWith('.') && path.length > 3) {
88
+ files.push(path);
89
+ }
90
+ }
91
+ }
92
+ }
93
+ return [...new Set(files)];
94
+ }
@@ -0,0 +1,14 @@
1
+ import { type TriggerReason } from '../lib/store.js';
2
+ /**
3
+ * Save session to team memory
4
+ * Called on: task complete, subtask complete, session abandoned
5
+ */
6
+ export declare function saveToTeamMemory(sessionId: string, triggerReason: TriggerReason): Promise<void>;
7
+ /**
8
+ * Clean up session data after save
9
+ */
10
+ export declare function cleanupSession(sessionId: string): void;
11
+ /**
12
+ * Save and cleanup session (for session end)
13
+ */
14
+ export declare function saveAndCleanupSession(sessionId: string, triggerReason: TriggerReason): Promise<void>;