grov 0.1.2 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +73 -88
  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 +431 -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 +120 -0
  35. package/dist/proxy/response-processor.d.ts +14 -0
  36. package/dist/proxy/response-processor.js +138 -0
  37. package/dist/proxy/server.d.ts +9 -0
  38. package/dist/proxy/server.js +904 -0
  39. package/package.json +8 -3
@@ -2,8 +2,18 @@
2
2
  // and Anthropic Claude Haiku for drift detection
3
3
  import OpenAI from 'openai';
4
4
  import Anthropic from '@anthropic-ai/sdk';
5
+ import { config } from 'dotenv';
6
+ import { join } from 'path';
7
+ import { homedir } from 'os';
8
+ import { existsSync } from 'fs';
5
9
  import { debugLLM } from './debug.js';
6
10
  import { truncate } from './utils.js';
11
+ // Load ~/.grov/.env as fallback for API key
12
+ // This allows users to store their API key in a safe location outside any repo
13
+ const grovEnvPath = join(homedir(), '.grov', '.env');
14
+ if (existsSync(grovEnvPath)) {
15
+ config({ path: grovEnvPath });
16
+ }
7
17
  let client = null;
8
18
  let anthropicClient = null;
9
19
  /**
@@ -39,6 +49,133 @@ function getAnthropicClient() {
39
49
  export function isLLMAvailable() {
40
50
  return !!process.env.OPENAI_API_KEY;
41
51
  }
52
+ /**
53
+ * Extract intent from first user prompt using Haiku
54
+ * Called once at session start to populate session_states
55
+ * Falls back to basic extraction if API unavailable (for hook compatibility)
56
+ */
57
+ export async function extractIntent(firstPrompt) {
58
+ // Check availability first - allows hook to work without API key
59
+ if (!isIntentExtractionAvailable()) {
60
+ return createFallbackIntent(firstPrompt);
61
+ }
62
+ try {
63
+ const client = getAnthropicClient();
64
+ const prompt = `Analyze this user request and extract structured intent for a coding assistant session.
65
+
66
+ USER REQUEST:
67
+ ${firstPrompt.substring(0, 2000)}
68
+
69
+ Extract as JSON:
70
+ {
71
+ "goal": "The main objective in 1-2 sentences",
72
+ "expected_scope": ["list", "of", "files/folders", "likely", "to", "be", "modified"],
73
+ "constraints": ["EXPLICIT restrictions from the user - see examples below"],
74
+ "success_criteria": ["How to know when the task is complete"],
75
+ "keywords": ["relevant", "technical", "terms"]
76
+ }
77
+
78
+ ═══════════════════════════════════════════════════════════════
79
+ CONSTRAINTS EXTRACTION - BE VERY THOROUGH
80
+ ═══════════════════════════════════════════════════════════════
81
+
82
+ Look for NEGATIVE constraints (things NOT to do):
83
+ - "NU modifica" / "DON'T modify" / "NEVER change" / "don't touch"
84
+ - "NU rula" / "DON'T run" / "NO commands" / "don't execute"
85
+ - "fără X" / "without X" / "except X" / "not including"
86
+ - "nu scrie cod" / "don't write code" / "just plan"
87
+
88
+ Look for POSITIVE constraints (things MUST do / ONLY do):
89
+ - "ONLY modify X" / "DOAR în X" / "only in folder Y"
90
+ - "must use Y" / "trebuie să folosești Y"
91
+ - "keep it simple" / "no external dependencies"
92
+ - "use TypeScript" / "must be async"
93
+
94
+ EXAMPLES:
95
+ Input: "Fix bug in auth. NU modifica nimic in afara de sandbox/, NU rula comenzi."
96
+ Output constraints: ["DO NOT modify files outside sandbox/", "DO NOT run commands"]
97
+
98
+ Input: "Add feature X. Only use standard library, keep backward compatible."
99
+ Output constraints: ["ONLY use standard library", "Keep backward compatible"]
100
+
101
+ Input: "Analyze code and create plan. Nu scrie cod inca, doar planifica."
102
+ Output constraints: ["DO NOT write code yet", "Only create plan/analysis"]
103
+
104
+ For expected_scope:
105
+ - Include file patterns (e.g., "src/auth/", "*.test.ts", "sandbox/")
106
+ - Include component/module names mentioned
107
+ - Be conservative - only include clearly relevant areas
108
+
109
+ RESPONSE RULES:
110
+ - English only (translate Romanian/other languages to English)
111
+ - No emojis
112
+ - Valid JSON only
113
+ - If no constraints found, return empty array []`;
114
+ const response = await client.messages.create({
115
+ model: 'claude-haiku-4-5-20251001',
116
+ max_tokens: 500,
117
+ messages: [{ role: 'user', content: prompt }],
118
+ });
119
+ const content = response.content?.[0];
120
+ if (!content || content.type !== 'text') {
121
+ return createFallbackIntent(firstPrompt);
122
+ }
123
+ try {
124
+ const jsonMatch = content.text.match(/\{[\s\S]*\}/);
125
+ if (!jsonMatch) {
126
+ return createFallbackIntent(firstPrompt);
127
+ }
128
+ const parsed = JSON.parse(jsonMatch[0]);
129
+ return {
130
+ goal: typeof parsed.goal === 'string' ? parsed.goal : firstPrompt.substring(0, 200),
131
+ expected_scope: Array.isArray(parsed.expected_scope)
132
+ ? parsed.expected_scope.filter((s) => typeof s === 'string')
133
+ : [],
134
+ constraints: Array.isArray(parsed.constraints)
135
+ ? parsed.constraints.filter((c) => typeof c === 'string')
136
+ : [],
137
+ success_criteria: Array.isArray(parsed.success_criteria)
138
+ ? parsed.success_criteria.filter((s) => typeof s === 'string')
139
+ : [],
140
+ keywords: Array.isArray(parsed.keywords)
141
+ ? parsed.keywords.filter((k) => typeof k === 'string')
142
+ : [],
143
+ };
144
+ }
145
+ catch {
146
+ return createFallbackIntent(firstPrompt);
147
+ }
148
+ }
149
+ catch {
150
+ // Outer catch - API errors, network issues, etc.
151
+ return createFallbackIntent(firstPrompt);
152
+ }
153
+ }
154
+ /**
155
+ * Fallback intent extraction without LLM
156
+ */
157
+ function createFallbackIntent(prompt) {
158
+ // Basic keyword extraction
159
+ const words = prompt.toLowerCase().split(/\s+/);
160
+ const techKeywords = words.filter(w => w.length > 3 &&
161
+ /^[a-z]+$/.test(w) &&
162
+ !['this', 'that', 'with', 'from', 'have', 'will', 'would', 'could', 'should'].includes(w));
163
+ // Extract file patterns
164
+ const filePatterns = prompt.match(/[\w\/.-]+\.(ts|js|tsx|jsx|py|go|rs|java|css|html|md)/g) || [];
165
+ return {
166
+ goal: prompt.substring(0, 200),
167
+ expected_scope: [...new Set(filePatterns)].slice(0, 5),
168
+ constraints: [],
169
+ success_criteria: [],
170
+ keywords: [...new Set(techKeywords)].slice(0, 10),
171
+ };
172
+ }
173
+ /**
174
+ * Check if intent extraction is available
175
+ */
176
+ export function isIntentExtractionAvailable() {
177
+ return !!(process.env.ANTHROPIC_API_KEY || process.env.GROV_API_KEY);
178
+ }
42
179
  /**
43
180
  * Check if Anthropic API is available (for drift detection)
44
181
  */
@@ -75,22 +212,37 @@ ${sessionSummary}
75
212
 
76
213
  Extract the following as JSON:
77
214
  {
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"],
215
+ "task": "Brief description (1 sentence)",
216
+ "goal": "The underlying problem being solved",
217
+ "reasoning_trace": [
218
+ "Be SPECIFIC: include file names, function names, line numbers when relevant",
219
+ "Format: '[Action] [target] to/for [purpose]'",
220
+ "Example: 'Read auth.ts:47 to understand token refresh logic'",
221
+ "Example: 'Fixed null check in validateToken() - was causing silent failures'",
222
+ "NOT: 'Investigated auth' or 'Fixed bug'"
223
+ ],
224
+ "decisions": [{"choice": "What was decided", "reason": "Why this over alternatives"}],
225
+ "constraints": ["Discovered limitations, rate limits, incompatibilities"],
83
226
  "status": "complete|partial|question|abandoned",
84
227
  "tags": ["relevant", "domain", "tags"]
85
228
  }
86
229
 
230
+ IMPORTANT for reasoning_trace:
231
+ - Each entry should be ACTIONABLE information for future developers
232
+ - Include specific file:line references when possible
233
+ - Explain WHY not just WHAT (e.g., "Chose JWT over sessions because stateless scales better")
234
+ - Bad: "Fixed the bug" / Good: "Fixed race condition in UserService.save() - was missing await"
235
+
87
236
  Status definitions:
88
237
  - "complete": Task was finished, implementation done
89
238
  - "partial": Work started but not finished
90
239
  - "question": Claude asked a question and is waiting for user response
91
240
  - "abandoned": User interrupted or moved to different topic
92
241
 
93
- Return ONLY valid JSON, no explanation.`
242
+ RESPONSE RULES:
243
+ - English only (translate if input is in other language)
244
+ - No emojis
245
+ - Valid JSON only`
94
246
  }
95
247
  ]
96
248
  });
@@ -290,119 +442,300 @@ function validateStatus(status) {
290
442
  }
291
443
  return 'partial'; // Default
292
444
  }
445
+ // ============================================
446
+ // SESSION SUMMARY FOR CLEAR OPERATION
447
+ // Reference: plan_proxy_local.md Section 2.3, 4.5
448
+ // ============================================
293
449
  /**
294
- * Extract intent from a prompt using Claude Haiku
295
- * Falls back to basic extraction if API unavailable
450
+ * Check if session summary generation is available
296
451
  */
297
- export async function extractIntent(prompt) {
298
- // Try LLM extraction if available
299
- if (isAnthropicAvailable()) {
300
- try {
301
- return await extractIntentWithLLM(prompt);
452
+ export function isSummaryAvailable() {
453
+ return !!(process.env.ANTHROPIC_API_KEY || process.env.GROV_API_KEY);
454
+ }
455
+ /**
456
+ * Generate session summary for CLEAR operation
457
+ * Reference: plan_proxy_local.md Section 2.3, 4.5
458
+ */
459
+ export async function generateSessionSummary(sessionState, steps) {
460
+ const client = getAnthropicClient();
461
+ const stepsText = steps
462
+ .filter(s => s.is_validated)
463
+ .slice(-20)
464
+ .map(step => {
465
+ let desc = `- ${step.action_type}`;
466
+ if (step.files.length > 0) {
467
+ desc += `: ${step.files.join(', ')}`;
302
468
  }
303
- catch (error) {
304
- debugLLM('extractIntent LLM failed, using fallback: %O', error);
305
- return extractIntentBasic(prompt);
469
+ if (step.command) {
470
+ desc += ` (${step.command.substring(0, 50)})`;
306
471
  }
472
+ return desc;
473
+ })
474
+ .join('\n');
475
+ const prompt = `Create a concise summary of this coding session for context continuation.
476
+
477
+ ORIGINAL GOAL: ${sessionState.original_goal || 'Not specified'}
478
+
479
+ EXPECTED SCOPE: ${sessionState.expected_scope.join(', ') || 'Not specified'}
480
+
481
+ CONSTRAINTS: ${sessionState.constraints.join(', ') || 'None'}
482
+
483
+ ACTIONS TAKEN:
484
+ ${stepsText || 'No actions recorded'}
485
+
486
+ Create a summary with these sections (keep total under 500 words):
487
+ 1. ORIGINAL GOAL: (1 sentence)
488
+ 2. PROGRESS: (2-3 bullet points of what was accomplished)
489
+ 3. KEY DECISIONS: (any important choices made)
490
+ 4. FILES MODIFIED: (list of files)
491
+ 5. CURRENT STATE: (where the work left off)
492
+ 6. NEXT STEPS: (recommended next actions)
493
+
494
+ Format as plain text, not JSON.`;
495
+ const response = await client.messages.create({
496
+ model: 'claude-haiku-4-5-20251001',
497
+ max_tokens: 800,
498
+ messages: [{ role: 'user', content: prompt }],
499
+ });
500
+ const content = response.content?.[0];
501
+ if (!content || content.type !== 'text') {
502
+ return createFallbackSummary(sessionState, steps);
307
503
  }
308
- // Fallback to basic extraction
309
- return extractIntentBasic(prompt);
504
+ return `PREVIOUS SESSION CONTEXT (auto-generated after context limit):
505
+
506
+ ${content.text}`;
310
507
  }
311
508
  /**
312
- * Extract intent using Claude Haiku
509
+ * Create fallback summary without LLM
313
510
  */
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.
511
+ function createFallbackSummary(sessionState, steps) {
512
+ const files = [...new Set(steps.flatMap(s => s.files))];
513
+ return `PREVIOUS SESSION CONTEXT (auto-generated after context limit):
324
514
 
325
- USER PROMPT:
326
- ${prompt}
515
+ ORIGINAL GOAL: ${sessionState.original_goal || 'Not specified'}
327
516
 
328
- Extract as JSON:
517
+ PROGRESS: ${steps.length} actions taken
518
+
519
+ FILES MODIFIED:
520
+ ${files.slice(0, 10).map(f => `- ${f}`).join('\n') || '- None recorded'}
521
+
522
+ Please continue from where you left off.`;
523
+ }
524
+ /**
525
+ * Check if task analysis is available
526
+ */
527
+ export function isTaskAnalysisAvailable() {
528
+ return !!(process.env.ANTHROPIC_API_KEY || process.env.GROV_API_KEY);
529
+ }
530
+ /**
531
+ * Analyze task context to determine task status
532
+ * Called after each main model response to orchestrate sessions
533
+ * Also compresses reasoning for steps if assistantResponse > 1000 chars
534
+ */
535
+ export async function analyzeTaskContext(currentSession, latestUserMessage, recentSteps, assistantResponse) {
536
+ const client = getAnthropicClient();
537
+ const stepsText = recentSteps.slice(0, 5).map(s => {
538
+ let desc = `- ${s.action_type}`;
539
+ if (s.files.length > 0) {
540
+ desc += `: ${s.files.slice(0, 3).join(', ')}`;
541
+ }
542
+ return desc;
543
+ }).join('\n') || 'None';
544
+ // Check if we need to compress reasoning
545
+ const needsCompression = assistantResponse.length > 1000;
546
+ const compressionInstruction = needsCompression
547
+ ? `\n "step_reasoning": "Extract CONCLUSIONS and SPECIFIC RECOMMENDATIONS only. Include: exact file paths (e.g., src/lib/utils.ts), function/component names, architectural patterns discovered, and WHY decisions were made. DO NOT write process descriptions like 'explored' or 'analyzed'. Max 800 chars."`
548
+ : '';
549
+ const compressionRule = needsCompression
550
+ ? '\n- step_reasoning: Extract CONCLUSIONS (specific files, patterns, decisions) NOT process descriptions. Example GOOD: "Utilities belong in src/lib/utils.ts alongside cn(), formatDate()". Example BAD: "Explored codebase structure".'
551
+ : '';
552
+ // Extract topic keywords from goal for comparison
553
+ const currentGoalKeywords = currentSession?.original_goal
554
+ ? currentSession.original_goal.toLowerCase().match(/\b\w{4,}\b/g)?.slice(0, 10).join(', ') || ''
555
+ : '';
556
+ const prompt = `You are a task orchestrator. Your PRIMARY job is to detect when the user starts a NEW, DIFFERENT task.
557
+
558
+ CURRENT SESSION:
559
+ - Current Goal: "${currentSession?.original_goal || 'No active task'}"
560
+ - Goal Keywords: [${currentGoalKeywords}]
561
+
562
+ LATEST USER MESSAGE:
563
+ "${latestUserMessage.substring(0, 500)}"
564
+
565
+ RECENT ACTIONS (last 5):
566
+ ${stepsText}
567
+
568
+ ASSISTANT RESPONSE (truncated):
569
+ "${assistantResponse.substring(0, 1500)}${assistantResponse.length > 1500 ? '...' : ''}"
570
+
571
+ ═══════════════════════════════════════════════════════════════
572
+ CRITICAL: Compare the TOPIC of "Current Goal" vs "Latest User Message"
573
+ ═══════════════════════════════════════════════════════════════
574
+
575
+ Ask yourself:
576
+ 1. Is the user message about the SAME subject/feature/file as the current goal?
577
+ 2. Or is it about something COMPLETELY DIFFERENT?
578
+
579
+ EXAMPLES of NEW_TASK (different topic):
580
+ - Goal: "implement authentication" → User: "fix the database migration" → NEW_TASK
581
+ - Goal: "analyze security layer" → User: "create hello.ts script" → NEW_TASK
582
+ - Goal: "refactor user service" → User: "add dark mode to UI" → NEW_TASK
583
+ - Goal: "fix login bug" → User: "write unit tests for payments" → NEW_TASK
584
+
585
+ EXAMPLES of CONTINUE (same topic):
586
+ - Goal: "implement authentication" → User: "now add the logout button" → CONTINUE
587
+ - Goal: "fix login bug" → User: "also check the session timeout" → CONTINUE
588
+ - Goal: "analyze security" → User: "what about rate limiting?" → CONTINUE
589
+
590
+ Return JSON:
329
591
  {
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"]
592
+ "action": "continue|new_task|subtask|parallel_task|task_complete|subtask_complete",
593
+ "topic_match": "YES if same topic, NO if different topic",
594
+ "task_id": "existing session_id or 'NEW' for new task",
595
+ "current_goal": "the goal based on LATEST user message",
596
+ "reasoning": "1 sentence explaining topic comparison"${compressionInstruction}
335
597
  }
336
598
 
337
- Return ONLY valid JSON.`
338
- }
339
- ]
599
+ DECISION RULES:
600
+ 1. NO current session → "new_task"
601
+ 2. topic_match=NO (different subject) → "new_task"
602
+ 3. topic_match=YES + user following up → "continue"
603
+ 4. Claude said "done/complete/finished" → "task_complete"
604
+ 5. Prerequisite work identified → "subtask"${compressionRule}
605
+
606
+ RESPONSE RULES:
607
+ - English only (translate if input is in other language)
608
+ - No emojis
609
+ - Valid JSON only`;
610
+ debugLLM('analyzeTaskContext', `Calling Haiku for task analysis (needsCompression=${needsCompression})`);
611
+ const response = await client.messages.create({
612
+ model: 'claude-haiku-4-5-20251001',
613
+ max_tokens: needsCompression ? 600 : 300,
614
+ messages: [{ role: 'user', content: prompt }],
340
615
  });
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');
616
+ const text = response.content[0].type === 'text' ? response.content[0].text : '';
617
+ try {
618
+ // Try to parse JSON from response (may have extra text)
619
+ const jsonMatch = text.match(/\{[\s\S]*\}/);
620
+ if (!jsonMatch) {
621
+ throw new Error('No JSON found in response');
622
+ }
623
+ const analysis = JSON.parse(jsonMatch[0]);
624
+ // If we didn't need compression but have short response, use it directly
625
+ if (!needsCompression && assistantResponse.length > 0) {
626
+ analysis.step_reasoning = assistantResponse.substring(0, 1000);
627
+ }
628
+ debugLLM('analyzeTaskContext', `Result: action=${analysis.action}, topic_match=${analysis.topic_match}, goal=${analysis.current_goal.substring(0, 50)}`);
629
+ return analysis;
630
+ }
631
+ catch (parseError) {
632
+ debugLLM('analyzeTaskContext', `Parse error: ${String(parseError)}, using fallback`);
633
+ // Fallback: continue existing session or create new
634
+ return {
635
+ action: currentSession ? 'continue' : 'new_task',
636
+ task_id: currentSession?.session_id || 'NEW',
637
+ current_goal: latestUserMessage.substring(0, 200),
638
+ reasoning: 'Fallback due to parse error',
639
+ step_reasoning: assistantResponse.substring(0, 1000),
640
+ };
345
641
  }
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
642
  }
355
643
  /**
356
- * Basic intent extraction without LLM
644
+ * Check if reasoning extraction is available
357
645
  */
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
- };
646
+ export function isReasoningExtractionAvailable() {
647
+ return !!process.env.ANTHROPIC_API_KEY || !!process.env.GROV_API_KEY;
366
648
  }
367
649
  /**
368
- * Extract file paths from prompt text
650
+ * Extract reasoning trace and decisions from steps
651
+ * Called at task_complete to populate team memory with rich context
369
652
  */
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
- }
653
+ export async function extractReasoningAndDecisions(stepsReasoning, originalGoal) {
654
+ const client = getAnthropicClient();
655
+ // Combine all steps reasoning into one text
656
+ const combinedReasoning = stepsReasoning
657
+ .filter(r => r && r.length > 10)
658
+ .join('\n\n---\n\n')
659
+ .substring(0, 8000);
660
+ if (combinedReasoning.length < 50) {
661
+ return { reasoning_trace: [], decisions: [] };
386
662
  }
387
- return [...files];
663
+ const prompt = `Extract CONCLUSIONS and KNOWLEDGE from Claude's work - NOT process descriptions.
664
+
665
+ ORIGINAL GOAL:
666
+ ${originalGoal || 'Not specified'}
667
+
668
+ CLAUDE'S RESPONSE:
669
+ ${combinedReasoning}
670
+
671
+ ═══════════════════════════════════════════════════════════════
672
+ EXTRACT ACTIONABLE CONCLUSIONS - NOT PROCESS
673
+ ═══════════════════════════════════════════════════════════════
674
+
675
+ GOOD examples (specific, reusable knowledge):
676
+ - "Utility functions belong in frontend/lib/utils.ts - existing utils: cn(), formatDate(), debounce()"
677
+ - "Auth tokens stored in localStorage with 15min expiry for long form sessions"
678
+ - "API routes follow REST pattern in /api/v1/ with Zod validation"
679
+ - "Database migrations go in prisma/migrations/ using prisma migrate"
680
+
681
+ BAD examples (process descriptions - DO NOT EXTRACT THESE):
682
+ - "Explored the codebase structure"
683
+ - "Analyzed several approaches"
684
+ - "Searched for utility directories"
685
+ - "Looked at the file organization"
686
+
687
+ 1. REASONING TRACE (conclusions and recommendations):
688
+ - WHAT was discovered or decided (specific file paths, patterns)
689
+ - WHY this is the right approach
690
+ - WHERE this applies in the codebase
691
+ - Max 10 entries, prioritize specific file/function recommendations
692
+
693
+ 2. DECISIONS (architectural choices):
694
+ - Only significant choices that affect future work
695
+ - What was chosen and why
696
+ - Max 5 decisions
697
+
698
+ Return JSON:
699
+ {
700
+ "reasoning_trace": [
701
+ "Utility functions belong in frontend/lib/utils.ts alongside cn(), formatDate(), debounce(), generateId()",
702
+ "Backend utilities go in backend/app/utils/ with domain-specific files like validation.py",
703
+ "The @/lib/utils import alias is configured for frontend utility access"
704
+ ],
705
+ "decisions": [
706
+ {"choice": "Add to existing utils.ts rather than new file", "reason": "Maintains established pattern, easier discoverability"},
707
+ {"choice": "Use frontend/lib/ over src/utils/", "reason": "Follows Next.js conventions used throughout project"}
708
+ ]
388
709
  }
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);
710
+
711
+ RESPONSE RULES:
712
+ - English only
713
+ - No emojis
714
+ - Valid JSON only
715
+ - Extract WHAT and WHERE, not just WHAT was done
716
+ - If no specific conclusions found, return empty arrays`;
717
+ debugLLM('extractReasoningAndDecisions', `Analyzing ${stepsReasoning.length} steps, ${combinedReasoning.length} chars`);
718
+ try {
719
+ const response = await client.messages.create({
720
+ model: 'claude-haiku-4-5-20251001',
721
+ max_tokens: 800,
722
+ messages: [{ role: 'user', content: prompt }],
723
+ });
724
+ const text = response.content[0].type === 'text' ? response.content[0].text : '';
725
+ const jsonMatch = text.match(/\{[\s\S]*\}/);
726
+ if (!jsonMatch) {
727
+ debugLLM('extractReasoningAndDecisions', 'No JSON found in response');
728
+ return { reasoning_trace: [], decisions: [] };
729
+ }
730
+ const result = JSON.parse(jsonMatch[0]);
731
+ debugLLM('extractReasoningAndDecisions', `Extracted ${result.reasoning_trace?.length || 0} traces, ${result.decisions?.length || 0} decisions`);
732
+ return {
733
+ reasoning_trace: result.reasoning_trace || [],
734
+ decisions: result.decisions || [],
735
+ };
736
+ }
737
+ catch (error) {
738
+ debugLLM('extractReasoningAndDecisions', `Error: ${String(error)}`);
739
+ return { reasoning_trace: [], decisions: [] };
740
+ }
408
741
  }
@@ -0,0 +1,19 @@
1
+ interface ClaudeSettings {
2
+ hooks?: Record<string, unknown>;
3
+ env?: {
4
+ ANTHROPIC_BASE_URL?: string;
5
+ [key: string]: string | undefined;
6
+ };
7
+ [key: string]: unknown;
8
+ }
9
+ export declare function readClaudeSettings(): ClaudeSettings;
10
+ export declare function writeClaudeSettings(settings: ClaudeSettings): void;
11
+ export declare function getSettingsPath(): string;
12
+ /**
13
+ * Set or remove ANTHROPIC_BASE_URL in settings.json env section.
14
+ * This allows users to just type 'claude' instead of setting env var manually.
15
+ */
16
+ export declare function setProxyEnv(enable: boolean): {
17
+ action: 'added' | 'removed' | 'unchanged';
18
+ };
19
+ export {};