@skroyc/librarian 0.1.0 → 0.2.1

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 (76) hide show
  1. package/README.md +4 -16
  2. package/dist/agents/context-schema.d.ts +1 -1
  3. package/dist/agents/context-schema.d.ts.map +1 -1
  4. package/dist/agents/context-schema.js +5 -2
  5. package/dist/agents/context-schema.js.map +1 -1
  6. package/dist/agents/react-agent.d.ts.map +1 -1
  7. package/dist/agents/react-agent.js +63 -170
  8. package/dist/agents/react-agent.js.map +1 -1
  9. package/dist/agents/tool-runtime.d.ts.map +1 -1
  10. package/dist/cli.d.ts +1 -1
  11. package/dist/cli.d.ts.map +1 -1
  12. package/dist/cli.js +53 -49
  13. package/dist/cli.js.map +1 -1
  14. package/dist/config.d.ts +1 -1
  15. package/dist/config.d.ts.map +1 -1
  16. package/dist/config.js +115 -69
  17. package/dist/config.js.map +1 -1
  18. package/dist/index.d.ts +1 -1
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +246 -150
  21. package/dist/index.js.map +1 -1
  22. package/dist/tools/file-finding.tool.d.ts +1 -1
  23. package/dist/tools/file-finding.tool.d.ts.map +1 -1
  24. package/dist/tools/file-finding.tool.js +70 -130
  25. package/dist/tools/file-finding.tool.js.map +1 -1
  26. package/dist/tools/file-listing.tool.d.ts +7 -1
  27. package/dist/tools/file-listing.tool.d.ts.map +1 -1
  28. package/dist/tools/file-listing.tool.js +96 -80
  29. package/dist/tools/file-listing.tool.js.map +1 -1
  30. package/dist/tools/file-reading.tool.d.ts +4 -1
  31. package/dist/tools/file-reading.tool.d.ts.map +1 -1
  32. package/dist/tools/file-reading.tool.js +107 -45
  33. package/dist/tools/file-reading.tool.js.map +1 -1
  34. package/dist/tools/grep-content.tool.d.ts +13 -1
  35. package/dist/tools/grep-content.tool.d.ts.map +1 -1
  36. package/dist/tools/grep-content.tool.js +186 -144
  37. package/dist/tools/grep-content.tool.js.map +1 -1
  38. package/dist/utils/error-utils.d.ts +9 -0
  39. package/dist/utils/error-utils.d.ts.map +1 -0
  40. package/dist/utils/error-utils.js +61 -0
  41. package/dist/utils/error-utils.js.map +1 -0
  42. package/dist/utils/file-utils.d.ts +1 -0
  43. package/dist/utils/file-utils.d.ts.map +1 -1
  44. package/dist/utils/file-utils.js +81 -9
  45. package/dist/utils/file-utils.js.map +1 -1
  46. package/dist/utils/format-utils.d.ts +25 -0
  47. package/dist/utils/format-utils.d.ts.map +1 -0
  48. package/dist/utils/format-utils.js +111 -0
  49. package/dist/utils/format-utils.js.map +1 -0
  50. package/dist/utils/gitignore-service.d.ts +10 -0
  51. package/dist/utils/gitignore-service.d.ts.map +1 -0
  52. package/dist/utils/gitignore-service.js +91 -0
  53. package/dist/utils/gitignore-service.js.map +1 -0
  54. package/dist/utils/logger.d.ts +2 -2
  55. package/dist/utils/logger.d.ts.map +1 -1
  56. package/dist/utils/logger.js +35 -34
  57. package/dist/utils/logger.js.map +1 -1
  58. package/dist/utils/path-utils.js +3 -3
  59. package/dist/utils/path-utils.js.map +1 -1
  60. package/package.json +1 -1
  61. package/src/agents/context-schema.ts +5 -2
  62. package/src/agents/react-agent.ts +694 -784
  63. package/src/agents/tool-runtime.ts +4 -4
  64. package/src/cli.ts +95 -57
  65. package/src/config.ts +192 -90
  66. package/src/index.ts +402 -180
  67. package/src/tools/file-finding.tool.ts +198 -310
  68. package/src/tools/file-listing.tool.ts +245 -202
  69. package/src/tools/file-reading.tool.ts +225 -138
  70. package/src/tools/grep-content.tool.ts +387 -307
  71. package/src/utils/error-utils.ts +95 -0
  72. package/src/utils/file-utils.ts +104 -19
  73. package/src/utils/format-utils.ts +190 -0
  74. package/src/utils/gitignore-service.ts +123 -0
  75. package/src/utils/logger.ts +112 -77
  76. package/src/utils/path-utils.ts +3 -3
@@ -1,253 +1,139 @@
1
+ import { spawn } from "node:child_process";
2
+ import { mkdir, rm } from "node:fs/promises";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { Readable } from "node:stream";
6
+ import { ChatAnthropic } from "@langchain/anthropic";
7
+ import { HumanMessage } from "@langchain/core/messages";
8
+ import { ChatGoogleGenerativeAI } from "@langchain/google-genai";
9
+ import { ChatOpenAI } from "@langchain/openai";
1
10
  import {
2
- createAgent,
3
11
  anthropicPromptCachingMiddleware,
12
+ createAgent,
13
+ type DynamicStructuredTool,
4
14
  todoListMiddleware,
5
- tool as createTool,
6
- type DynamicStructuredTool
7
15
  } from "langchain";
8
16
  import type { z } from "zod";
9
- import { fileListTool } from "../tools/file-listing.tool.js";
10
- import { fileReadTool } from "../tools/file-reading.tool.js";
11
- import { grepContentTool } from "../tools/grep-content.tool.js";
12
- import { fileFindTool } from "../tools/file-finding.tool.js";
13
- import { ChatOpenAI } from "@langchain/openai";
14
- import { ChatAnthropic } from "@langchain/anthropic";
15
- import { ChatGoogleGenerativeAI } from "@langchain/google-genai";
16
- import { HumanMessage } from "@langchain/core/messages";
17
+ import { findTool } from "../tools/file-finding.tool.js";
18
+ import { listTool } from "../tools/file-listing.tool.js";
19
+ import { viewTool } from "../tools/file-reading.tool.js";
20
+ import { grepTool } from "../tools/grep-content.tool.js";
17
21
  import { logger } from "../utils/logger.js";
18
- import os from "node:os";
19
- import { mkdir, rm } from "node:fs/promises";
20
- import path from "node:path";
21
- import { spawn } from "node:child_process";
22
- import { Readable } from "node:stream";
23
22
  import type { AgentContext } from "./context-schema.js";
24
23
 
25
24
  /**
26
25
  * Configuration interface for ReactAgent
27
26
  */
28
27
  export interface ReactAgentConfig {
29
- /** AI provider configuration including type, API key, and optional model/base URL */
30
- aiProvider: {
31
- type:
32
- | "openai"
33
- | "anthropic"
34
- | "google"
35
- | "openai-compatible"
36
- | "anthropic-compatible"
37
- | "claude-code"
38
- | "gemini-cli";
39
- apiKey: string;
40
- model?: string;
41
- baseURL?: string;
42
- };
43
- /** Working directory where the agent operates */
44
- workingDir: string;
45
- /** Optional technology context for dynamic system prompt construction */
46
- technology?: {
47
- name: string;
48
- repository: string;
49
- branch: string;
50
- };
51
- /** Optional context schema for runtime context validation */
52
- contextSchema?: z.ZodType | undefined;
28
+ /** AI provider configuration including type, API key, and optional model/base URL */
29
+ aiProvider: {
30
+ type:
31
+ | "openai"
32
+ | "anthropic"
33
+ | "google"
34
+ | "openai-compatible"
35
+ | "anthropic-compatible"
36
+ | "claude-code"
37
+ | "gemini-cli";
38
+ apiKey: string;
39
+ model?: string;
40
+ baseURL?: string;
41
+ };
42
+ /** Working directory where the agent operates */
43
+ workingDir: string;
44
+ /** Optional technology context for dynamic system prompt construction */
45
+ technology?: {
46
+ name: string;
47
+ repository: string;
48
+ branch: string;
49
+ };
50
+ /** Optional context schema for runtime context validation */
51
+ contextSchema?: z.ZodType | undefined;
53
52
  }
54
53
 
55
54
  export class ReactAgent {
56
- private readonly aiModel?: ChatOpenAI | ChatAnthropic | ChatGoogleGenerativeAI;
57
- private readonly tools: DynamicStructuredTool[];
58
- private agent?: ReturnType<typeof createAgent>;
59
- private readonly config: ReactAgentConfig;
60
- private readonly contextSchema?: z.ZodType | undefined;
61
-
62
- constructor(config: ReactAgentConfig) {
63
- this.config = config;
64
- this.contextSchema = config.contextSchema;
65
-
66
- if (
67
- config.aiProvider.type !== "claude-code" &&
68
- config.aiProvider.type !== "gemini-cli"
69
- ) {
70
- this.aiModel = this.createAIModel(config.aiProvider);
71
- }
72
-
73
- // Initialize tools - modernized tool pattern
74
- this.tools = [fileListTool, fileReadTool, grepContentTool, fileFindTool];
75
-
76
- logger.info("AGENT", "Initializing ReactAgent", {
77
- aiProviderType: config.aiProvider.type,
78
- model: config.aiProvider.model,
79
- workingDir: config.workingDir.replace(os.homedir(), "~"),
80
- toolCount: this.tools.length,
81
- hasContextSchema: !!this.contextSchema,
82
- });
83
- }
84
-
85
- /**
86
- * Creates a dynamic system prompt based on current configuration and technology context
87
- * @returns A context-aware system prompt string
88
- */
89
- createDynamicSystemPrompt(): string {
90
- const { workingDir, technology } = this.config;
91
-
92
- // Dynamic system prompt generation code
93
- let prompt = `
94
- You are a **Codebase Investigator** specializing in technology exploration and architectural analysis. Your core purpose is to provide deep technical insights grounded in actual source code evidence. You approach every question as an investigation, requiring verification before drawing conclusions.
95
-
96
- **Your Key Traits:**
97
- - Methodical exploration of codebases
98
- - Evidence-based conclusions backed by specific source citations
99
- - Clear and accessible technical communication
100
- - Intellectual honesty about knowledge boundaries
55
+ private readonly aiModel?:
56
+ | ChatOpenAI
57
+ | ChatAnthropic
58
+ | ChatGoogleGenerativeAI;
59
+ private readonly tools: DynamicStructuredTool[];
60
+ private agent?: ReturnType<typeof createAgent>;
61
+ private readonly config: ReactAgentConfig;
62
+ private readonly contextSchema?: z.ZodType | undefined;
63
+
64
+ constructor(config: ReactAgentConfig) {
65
+ this.config = config;
66
+ this.contextSchema = config.contextSchema;
67
+
68
+ if (
69
+ config.aiProvider.type !== "claude-code" &&
70
+ config.aiProvider.type !== "gemini-cli"
71
+ ) {
72
+ this.aiModel = this.createAIModel(config.aiProvider);
73
+ }
74
+
75
+ // Initialize tools - modernized tool pattern
76
+ this.tools = [listTool, viewTool, grepTool, findTool];
77
+
78
+ logger.info("AGENT", "Initializing ReactAgent", {
79
+ aiProviderType: config.aiProvider.type,
80
+ model: config.aiProvider.model,
81
+ workingDir: config.workingDir.replace(os.homedir(), "~"),
82
+ toolCount: this.tools.length,
83
+ hasContextSchema: !!this.contextSchema,
84
+ });
85
+ }
86
+
87
+ /**
88
+ * Creates a dynamic system prompt based on current configuration and technology context
89
+ * @returns A context-aware system prompt string
90
+ */
91
+ createDynamicSystemPrompt(): string {
92
+ const { workingDir, technology } = this.config;
93
+
94
+ // Dynamic system prompt generation code
95
+ let prompt = `
96
+ You are a **Codebase Investigator**. Your mission is to provide technical insights grounded in source code evidence. You approach every query as a methodical investigation, prioritizing verification over assumptions and ensuring every conclusion is backed by specific file citations.
101
97
 
102
98
  # Instructions
103
99
 
104
100
  ## Investigation Protocol
105
101
 
106
- **INVESTIGATION RULE 1 - Boundaries:**
107
- - Work only within read-only exploration of the sandboxed working directory
108
- - Every technical claim must be tied to specific source code evidence
109
- - Admit uncertainty when code hasn't been verified—read files rather than speculate
110
-
111
- **INVESTIGATION RULE 2 - Methodology:**
112
- - Start by mapping the codebase structure (directories, key files)
113
- - Trace how components connect through imports, exports, and function calls
114
- - Validate assumptions by reading actual implementations
115
- - Build your answer from verified source evidence, not assumptions
116
-
117
- **INVESTIGATION RULE 3 - User Focus:**
118
- - Prioritize complete answers over asking follow-up questions
119
- - Provide context that helps users understand patterns, not just individual functions
120
- - Bridge the gap between code behavior and practical application
121
-
122
- ## Verification Threshold
123
-
124
- **DECISION RULE 1 - Action Threshold:**
125
- - If seeing a file would improve your answer, read it immediately—do not ask the user first
126
- - If asked about an unseen component, investigate it before responding
127
-
128
- **DECISION RULE 2 - Confidence Check:**
129
- - Before finalizing any answer, verify: "Am I relying on external libraries or modules I haven't confirmed in this codebase?"
130
- - If yes: either read the local source or explicitly state the limitation
131
-
132
- **DECISION RULE 3 - Ambiguity Protocol:**
133
- - When multiple interpretations exist, state the uncertainty
134
- - Provide the most likely answer with supporting evidence
135
- - Note alternative possibilities and their conditions
102
+ - **Evidence First**: Every claim must be tied to specific source evidence in the sandboxed directory. Read files rather than speculate.
103
+ - **Methodical Mapping**: Start by understanding the codebase structure and component connections (imports, exports, calls).
104
+ - **Execution Over Inquiry**: Prioritize completing the investigation and providing a comprehensive answer over asking follow-up questions.
105
+ - **Pattern Recognition**: Bridge the gap between raw code behavior and practical application, helping users understand general patterns.
136
106
 
137
- ## Diagnostic Reasoning
107
+ ## Verification Standards
138
108
 
139
- **DIAGNOSTIC RULE 1 - Generation:**
140
- - For complex logic, list multiple possible explanations
141
- - Do not settle on the first explanation you find
109
+ - **Autonomous Action**: If a file would improve your answer, read it immediately. Do not ask for permission.
110
+ - **Internal Validation**: Distinguish between confirmed local implementations and external library behavior. Verify local code before assuming standard library defaults.
142
111
 
143
- **DIAGNOSTIC RULE 2 - Validation:**
144
- - Use file reads to confirm which explanation matches reality
145
- - Look for contradictory evidence in other files
112
+ ## Critical Thinking
146
113
 
147
- **DIAGNOSTIC RULE 3 - Reporting:**
148
- - Present the winning explanation with supporting citations
149
- - Explain why other options don't fit the evidence
150
- - Note any questions that remain unanswered
114
+ - **Exhaustive Exploration**: Never declare something missing without checking configuration files, related directories, and alternative naming patterns. If initial searches fail, pivot your strategy.
115
+ - **Hypothesis Testing**: For complex logic, consider multiple explanations. Use file reads to confirm which matches reality and look for contradictory evidence.
116
+ - **Self-Correction**: Regularly challenge your own findings. If new evidence contradicts your planned response, update your mental model immediately.
117
+ - **Handling Ambiguity**: When multiple interpretations exist, state the uncertainty and provide the most likely answer with supporting evidence.
151
118
 
152
- ## Adaptive Validation Protocol
119
+ ## Evidence & Citations
153
120
 
154
- **VALIDATION RULE 1 - Self-Correction Loop:**
155
- - After examining any file, challenge your planned explanation
156
- - Ask: "Does this contradict what I was about to say?"
121
+ - **Source of Truth**: The local working directory is the definitive truth. If external documentation contradicts local code, the local code is correct.
122
+ - **Mandatory Citations**: Every technical claim must cite specific repository-relative file paths (e.g., \`src/utils/logger.ts\`) and, where possible, line numbers or function names. Vague references are insufficient.
123
+ - **Knowledge Gaps**: If information is missing from the directory, explicitly state what you couldn't find before providing general industry standard alternatives.
124
+ - **Fact vs. Inference**: Distinguish clearly between verified code behavior (citing files) and inferred patterns or conventions.
157
125
 
158
- **VALIDATION RULE 2 - Pivot Strategy:**
159
- - When initial searches fail, expand your approach
160
- - Check configuration files, related directories, or alternative naming patterns
161
- - Never declare something missing without exhaustive exploration
126
+ ## Thoroughness & Constraints
162
127
 
163
- **VALIDATION RULE 3 - Integration Check:**
164
- - Ensure new findings integrate with your existing understanding
165
- - Update your mental model rather than ignoring contradictory evidence
128
+ - **Complete Coverage**: Address every part of the user's question, explaining both specific implementations and the general patterns they follow.
129
+ - **Contextual Awareness**: Always consider configuration files (e.g., \`package.json\`, \`.env\`, yaml configs) that might affect the behavior of the code you are analyzing.
130
+ - **Graceful Failure**: If tools fail or files are inaccessible, document the issue clearly. Attempt alternative discovery methods (e.g., searching for related terms) before acknowledging a gap in evidence.
166
131
 
167
- ## Information Scoping Rules
132
+ ## Output Standards
168
133
 
169
- **SCOPE 1 - Primary Source:**
170
- The working directory contains the definitive truth. Start and end here.
171
-
172
- **SCOPE 2 - Supporting Context:**
173
- - Language documentation explains expected behavior
174
- - Configuration files set constraints and options
175
- - Use these to interpret what you find
176
-
177
- **SCOPE 3 - Inferred Patterns:**
178
- - Consistent patterns across files suggest conventions
179
- - Use patterns to guide interpretation, not as definitive proof
180
-
181
- **NOTE:** If external documentation contradicts local code, the local code is always correct for this repository.
182
-
183
- ## Citation Standards Protocol
184
-
185
- **CITATION RULE 1 - Evidence Requirement:**
186
- - Every technical claim must cite specific file paths and, where possible, line numbers or function names
187
- - Vague references like "the code" or "this file" are insufficient
188
-
189
- **CITATION RULE 2 - Acknowledgment Protocol:**
190
- - When information is not found in the directory, explicitly state: "Based on the accessible files, I cannot find [X], but typically [Y] applies."
191
-
192
- **CITATION RULE 3 - Confidence Calibration:**
193
- - Distinguish between verified facts (citing files) and inferred patterns (noting the distinction)
194
- - Never present inference as fact without clear labeling
195
-
196
- ## Thoroughness Verification System
197
-
198
- **VERIFICATION RULE 1 - Configuration Check:**
199
- - Have you considered all config files that might affect this behavior?
200
- - Do not explain code in isolation from its configuration context
201
-
202
- **VERIFICATION RULE 2 - Principle Coverage:**
203
- - Does your answer explain both the specific case AND the general pattern?
204
- - Help users apply this knowledge beyond the immediate example
205
-
206
- **VERIFICATION RULE 3 - Question Coverage:**
207
- - Have you addressed every part of the user's question?
208
- - Note any intentional limitations or scope boundaries
209
-
210
- ## Failure Response System
211
-
212
- **RESPONSE RULE 1 - Temporary Failures:**
213
- - Timeouts and transient issues warrant retry (max 3 attempts)
214
- - After retries exhaust, document the access issue
215
-
216
- **RESPONSE RULE 2 - Permanent Failures:**
217
- - Missing files, permission issues: stop retrying immediately
218
- - Attempt alternative discovery methods or acknowledge the gap
219
-
220
- **RESPONSE RULE 3 - Best Effort Resolution:**
221
- - For obfuscated, missing, or inaccessible code:
222
- - Provide answers grounded in standard practices
223
- - Explicitly note confidence levels and knowledge boundaries
224
-
225
- ## Response Integrity Standard
226
-
227
- **INTEGRITY RULE 1 - No Premature Responses:**
228
- - Complete your full investigation before answering
229
- - Resist the urge to respond before verification
230
-
231
- **INTEGRITY RULE 2 - Evidence Compilation:**
232
- - Gather all relevant file evidence before synthesizing
233
- - Confirm no stone has been left unturned
234
-
235
- **INTEGRITY RULE 3 - Final Validation:**
236
- - Deliver your answer only when:
237
- - All tools have been exhausted
238
- - Evidence supports your conclusions
239
- - You can cite specific sources for every claim
240
-
241
- **INTEGRITY RULE 4 - Developer Consumption Focus (Default Behavior):**
242
- - Frame explanations around how a developer WOULD USE this code, not how they might EXTEND it
243
- - Focus on APIs, parameters, return values, and integration patterns
244
- - Provide usage examples that show calling code, not implementation code
245
- - When explaining implementation details, contextualize them for consumption use cases
246
-
247
- **EXCEPTION - Architecture/Extension Queries:**
248
- - ONLY deviate from the consumption focus when the user explicitly asks for it
249
- - Examples: "What is the architecture of X?", "How can we extend X?", "How is X structured?"
250
- - In these cases, provide architectural perspective as requested
134
+ - **Developer Consumption Focus**: By default, frame explanations around how a developer would **use** the code (APIs, parameters, usage examples) rather than how it is implemented internally. Provide usage examples showing calling code, not implementation logic.
135
+ - **Architectural Exception**: Only provide deep architectural or implementation analysis if the user explicitly asks for it (e.g., "How is X structured?", "Explain the architecture of Y").
136
+ - **Integrity**: Complete your full investigation and verify all evidence before delivering your final answer. Ensure citations support every claim.
251
137
 
252
138
  # Reasoning Steps
253
139
 
@@ -345,584 +231,608 @@ Remember: ALL tool calls MUST be executed using absolute path in \`[WORKING_DIRE
345
231
 
346
232
  ---
347
233
 
348
- **Before responding to any user query, verify you have sufficient evidence to support your claims. When in doubt, read more files rather than speculate.**
234
+ **Final Reminder: Every claim must be backed by evidence. If you haven't verified it in the code, don't say it.**
349
235
  `;
350
236
 
351
- // Add technology context if available
352
- if (technology) {
353
- prompt = prompt.replace(
354
- "<context_block>",
355
- `You have been provided the **${technology.name}** repository.
237
+ // Add technology context if available
238
+ if (technology) {
239
+ prompt = prompt.replace(
240
+ "<context_block>",
241
+ `You have been provided the **${technology.name}** repository.
356
242
  Repository: ${technology.repository}
357
243
  Your Working Directory: ${workingDir}
358
244
 
359
- Remember that ALL tool calls MUST be executed using absolute path in \`${workingDir}\``,
360
- );
361
- prompt = prompt.replace("</context_block>", "");
362
- } else {
363
- prompt = prompt.replace(
364
- "<context_block>",
365
- `You have been provided several related repositories to work with grouped in the following working directory: ${workingDir}
366
-
367
- Remember that ALL tool calls MUST be executed using absolute path in \`${workingDir}\``,
368
- );
369
- prompt = prompt.replace("</context_block>", "");
370
- }
371
-
372
- logger.debug("AGENT", "Dynamic system prompt generated", {
373
- hasTechnologyContext: !!technology,
374
- promptLength: prompt.length,
375
- });
376
-
377
- return prompt;
378
- }
379
-
380
- private async createGeminiTempDir(): Promise<string> {
381
- const tempDir = path.join(os.tmpdir(), `librarian-gemini-${Date.now()}`);
382
- await mkdir(tempDir, { recursive: true });
383
- return tempDir;
384
- }
385
-
386
- private async setupGeminiConfig(
387
- tempDir: string,
388
- systemPrompt: string,
389
- _model: string,
390
- ): Promise<{ systemPromptPath: string; settingsPath: string }> {
391
- const systemPromptPath = path.join(tempDir, "system.md");
392
- const settingsPath = path.join(tempDir, "settings.json");
393
-
394
- await Bun.write(systemPromptPath, systemPrompt);
395
-
396
- const settings = {
397
- tools: {
398
- core: ["list_directory", "read_file", "glob", "search_file_content"],
399
- autoAccept: true,
400
- },
401
- mcpServers: {},
402
- mcp: {
403
- excluded: ["*"],
404
- },
405
- experimental: {
406
- enableAgents: false,
407
- },
408
- output: {
409
- format: "json",
410
- },
411
- };
412
- await Bun.write(settingsPath, JSON.stringify(settings, null, 2));
413
-
414
- return { systemPromptPath, settingsPath };
415
- }
416
-
417
- private buildGeminiEnv(tempDir: string, model: string): Record<string, string | undefined> {
418
- const settingsPath = path.join(tempDir, "settings.json");
419
- const systemPromptPath = path.join(tempDir, "system.md");
420
-
421
- return {
422
- ...Bun.env,
423
- GEMINI_SYSTEM_MD: systemPromptPath,
424
- GEMINI_CLI_SYSTEM_DEFAULTS_PATH: settingsPath,
425
- GEMINI_CLI_SYSTEM_SETTINGS_PATH: settingsPath,
426
- GEMINI_MODEL: model,
427
- };
428
- }
429
-
430
- private async cleanupGeminiTempDir(tempDir: string): Promise<void> {
431
- try {
432
- await rm(tempDir, { recursive: true, force: true });
433
- } catch (err) {
434
- logger.warn("AGENT", "Failed to cleanup Gemini temp files", {
435
- error: err,
436
- });
437
- }
438
- }
439
-
440
- private async *streamClaudeCli(
441
- query: string,
442
- context?: AgentContext,
443
- ): AsyncGenerator<string, void, unknown> {
444
- const workingDir = context?.workingDir || this.config.workingDir;
445
- const systemPrompt = this.createDynamicSystemPrompt();
446
-
447
- const args = [
448
- "-p",
449
- query,
450
- "--system-prompt",
451
- systemPrompt,
452
- "--tools",
453
- "Read,Glob,Grep",
454
- "--dangerously-skip-permissions",
455
- "--output-format",
456
- "stream-json",
457
- ];
458
-
459
- const env = {
460
- ...Bun.env,
461
- CLAUDE_PROJECT_DIR: workingDir,
462
- ...(this.config.aiProvider.model && {
463
- ANTHROPIC_MODEL: this.config.aiProvider.model,
464
- }),
465
- };
466
-
467
- logger.debug("AGENT", "Spawning Claude CLI", {
468
- args: args.map((a) => (a.length > 100 ? a.substring(0, 100) + "..." : a)),
469
- workingDir,
470
- });
471
-
472
- const proc = spawn("claude", args, {
473
- cwd: workingDir,
474
- env,
475
- });
476
-
477
- let buffer = "";
478
-
479
- if (!proc.stdout) {
480
- throw new Error("Failed to capture Claude CLI output");
481
- }
482
-
483
- const readable = Readable.from(proc.stdout);
484
-
485
- for await (const chunk of readable) {
486
- buffer += chunk.toString();
487
- const lines = buffer.split("\n");
488
- buffer = lines.pop() || "";
489
-
490
- for (const line of lines) {
491
- if (!line.trim()) continue;
492
- try {
493
- const data = JSON.parse(line);
494
- // Filter for text content blocks in the stream
495
- if (data.type === "text" && data.content) {
496
- yield data.content;
497
- } else if (data.type === "content_block_delta" && data.delta?.text) {
498
- yield data.delta.text;
499
- } else if (data.type === "message" && data.content) {
500
- // Final message might come as a whole
501
- if (Array.isArray(data.content)) {
502
- for (const block of data.content) {
503
- if (block.type === "text" && block.text) {
504
- yield block.text;
505
- }
506
- }
507
- }
508
- }
509
- } catch {
510
- // Silent fail for non-JSON or partial lines
511
- }
512
- }
513
- }
514
-
515
- // Wait for process to exit
516
- await new Promise<void>((resolve, reject) => {
517
- proc.on("exit", (code) => {
518
- if (code === 0) resolve();
519
- else reject(new Error(`Claude CLI exited with code ${code}`));
520
- });
521
- proc.on("error", reject);
522
- });
523
- }
524
-
525
- private async *streamGeminiCli(
526
- query: string,
527
- context?: AgentContext,
528
- ): AsyncGenerator<string, void, unknown> {
529
- const workingDir = context?.workingDir || this.config.workingDir;
530
- const systemPrompt = this.createDynamicSystemPrompt();
531
-
532
- const tempDir = await this.createGeminiTempDir();
533
- const model = this.config.aiProvider.model || "gemini-2.5-flash";
534
-
535
- try {
536
- await this.setupGeminiConfig(tempDir, systemPrompt, model);
537
-
538
- const args = [
539
- "gemini",
540
- "-p",
541
- query,
542
- "--output-format",
543
- "stream-json",
544
- "--yolo",
545
- ];
546
-
547
- const env = this.buildGeminiEnv(tempDir, model);
548
-
549
- logger.debug("AGENT", "Spawning Gemini CLI", {
550
- args,
551
- workingDir,
552
- model,
553
- });
554
-
555
- const proc = Bun.spawn(args, {
556
- cwd: workingDir,
557
- env,
558
- stdout: "pipe",
559
- stderr: "pipe",
560
- });
561
-
562
- const reader = proc.stdout.getReader();
563
- let buffer = "";
564
-
565
- while (true) {
566
- const { done, value } = await reader.read();
567
- if (done) {
568
- break;
569
- }
570
-
571
- buffer += new TextDecoder().decode(value);
572
- const lines = buffer.split("\n");
573
- buffer = lines.pop() || "";
574
-
575
- for (const line of lines) {
576
- if (!line.trim()) {
577
- continue;
578
- }
579
- try {
580
- const data = JSON.parse(line);
581
- const text = this.parseGeminiStreamLine(data);
582
- if (text) {
583
- yield text;
584
- }
585
- } catch {
586
- // Silent fail for non-JSON or partial lines
587
- }
588
- }
589
- }
590
-
591
- const exitCode = await proc.exited;
592
- if (exitCode !== 0) {
593
- throw new Error(`Gemini CLI exited with code ${exitCode}`);
594
- }
595
- } finally {
596
- await this.cleanupGeminiTempDir(tempDir);
597
- }
598
- }
599
-
600
- private parseGeminiStreamLine(data: unknown): string | null {
601
- if (data && typeof data === "object" && "type" in data && "role" in data && "content" in data) {
602
- const typedData = data as { type: string; role: string; content: string };
603
- if (typedData.type === "message" && typedData.role === "assistant" && typedData.content) {
604
- return typedData.content;
605
- }
606
- }
607
- return null;
608
- }
609
-
610
- private createAIModel(
611
- aiProvider: ReactAgentConfig["aiProvider"],
612
- ): ChatOpenAI | ChatAnthropic | ChatGoogleGenerativeAI {
613
- const { type, apiKey, model, baseURL } = aiProvider;
614
-
615
- logger.debug("AGENT", "Creating AI model instance", {
616
- type,
617
- model,
618
- hasBaseURL: !!baseURL,
619
- });
620
-
621
- switch (type) {
622
- case "openai":
623
- return new ChatOpenAI({
624
- apiKey,
625
- modelName: model || "gpt-5.2",
626
- });
627
- case "openai-compatible":
628
- return new ChatOpenAI({
629
- apiKey,
630
- modelName: model || "gpt-5.2",
631
- configuration: {
632
- baseURL: baseURL || "https://api.openai.com/v1",
633
- },
634
- });
635
- case "anthropic":
636
- return new ChatAnthropic({
637
- apiKey,
638
- modelName: model || "claude-sonnet-4-5",
639
- });
640
- case "anthropic-compatible":
641
- if (!baseURL) {
642
- throw new Error(
643
- "baseURL is required for anthropic-compatible provider",
644
- );
645
- }
646
- if (!model) {
647
- throw new Error(
648
- "model is required for anthropic-compatible provider",
649
- );
650
- }
651
- return new ChatAnthropic({
652
- apiKey,
653
- modelName: model,
654
- anthropicApiUrl: baseURL,
655
- });
656
- case "google":
657
- return new ChatGoogleGenerativeAI({
658
- apiKey,
659
- model: model || "gemini-3-flash-preview",
660
- });
661
- default:
662
- logger.error(
663
- "AGENT",
664
- "Unsupported AI provider type",
665
- new Error(`Unsupported AI provider type: ${type}`),
666
- { type },
667
- );
668
- throw new Error(`Unsupported AI provider type: ${type}`);
669
- }
670
- }
671
-
672
- initialize(): Promise<void> {
673
- if (
674
- this.config.aiProvider.type === "claude-code" ||
675
- this.config.aiProvider.type === "gemini-cli"
676
- ) {
677
- logger.info(
678
- "AGENT",
679
- `${this.config.aiProvider.type} CLI mode initialized (skipping LangChain setup)`,
680
- );
681
- return Promise.resolve();
682
- }
683
-
684
- if (!this.aiModel) {
685
- throw new Error("AI model not created for non-CLI provider");
686
- }
687
-
688
- // Create the agent using LangChain's createAgent function with dynamic system prompt
689
- this.agent = createAgent({
690
- model: this.aiModel,
691
- tools: this.tools,
692
- systemPrompt: this.createDynamicSystemPrompt(),
693
- middleware: [
694
- todoListMiddleware(),
695
- ...(this.config.aiProvider.type === "anthropic" ||
696
- this.config.aiProvider.type === "anthropic-compatible"
697
- ? [anthropicPromptCachingMiddleware()]
698
- : []),
699
- ],
700
- });
701
-
702
- logger.info("AGENT", "Agent initialized successfully", {
703
- toolCount: this.tools.length,
704
- hasContextSchema: !!this.contextSchema,
705
- });
706
-
707
- return Promise.resolve();
708
- }
709
-
710
- /**
711
- * Query repository with a given query and optional context
712
- *
713
- * @param repoPath - The repository path (deprecated, for compatibility)
714
- * @param query - The query string
715
- * @param context - Optional context object containing working directory and metadata
716
- * @returns The agent's response as a string
717
- */
718
- async queryRepository(
719
- _repoPath: string,
720
- query: string,
721
- context?: AgentContext,
722
- ): Promise<string> {
723
- logger.info("AGENT", "Query started", {
724
- queryLength: query.length,
725
- hasContext: !!context,
726
- });
727
-
728
- if (this.config.aiProvider.type === "claude-code") {
729
- let fullContent = "";
730
- for await (const chunk of this.streamClaudeCli(query, context)) {
731
- fullContent += chunk;
732
- }
733
- return fullContent;
734
- }
735
-
736
- if (this.config.aiProvider.type === "gemini-cli") {
737
- let fullContent = "";
738
- for await (const chunk of this.streamGeminiCli(query, context)) {
739
- fullContent += chunk;
740
- }
741
- return fullContent;
742
- }
743
-
744
- const timingId = logger.timingStart("agentQuery");
745
-
746
- if (!this.agent) {
747
- logger.error(
748
- "AGENT",
749
- "Agent not initialized",
750
- new Error("Agent not initialized. Call initialize() first."),
751
- );
752
- throw new Error("Agent not initialized. Call initialize() first.");
753
- }
754
-
755
- // Prepare the messages for the agent - system prompt already set during initialization
756
- const messages = [new HumanMessage(query)];
757
-
758
- logger.debug("AGENT", "Invoking agent with messages", {
759
- messageCount: messages.length,
760
- hasContext: !!context,
761
- });
762
-
763
- // Execute the agent with optional context
764
- const result = await this.agent.invoke(
765
- {
766
- messages,
767
- },
768
- context ? { context, recursionLimit: 100 } : { recursionLimit: 100 },
769
- );
770
-
771
- // Extract the last message content from the state
772
- const lastMessage = result.messages.at(-1);
773
- const content =
774
- typeof lastMessage.content === "string"
775
- ? lastMessage.content
776
- : JSON.stringify(lastMessage.content);
777
-
778
- logger.timingEnd(timingId, "AGENT", "Query completed");
779
- logger.info("AGENT", "Query result received", {
780
- responseLength: content.length,
781
- });
782
-
783
- return content;
784
- }
785
-
786
- /**
787
- * Stream repository query with optional context
788
- *
789
- * @param repoPath - The repository path (deprecated, for compatibility)
790
- * @param query - The query string
791
- * @param context - Optional context object containing working directory and metadata
792
- * @returns Async generator yielding string chunks
793
- */
794
- async *streamRepository(
795
- _repoPath: string,
796
- query: string,
797
- context?: AgentContext,
798
- ): AsyncGenerator<string, void, unknown> {
799
- logger.info("AGENT", "Stream started", {
800
- queryLength: query.length,
801
- hasContext: !!context,
802
- });
803
-
804
- if (this.config.aiProvider.type === "claude-code") {
805
- yield* this.streamClaudeCli(query, context);
806
- return;
807
- }
808
-
809
- if (this.config.aiProvider.type === "gemini-cli") {
810
- yield* this.streamGeminiCli(query, context);
811
- return;
812
- }
813
-
814
- const timingId = logger.timingStart("agentStream");
815
-
816
- if (!this.agent) {
817
- logger.error(
818
- "AGENT",
819
- "Agent not initialized",
820
- new Error("Agent not initialized. Call initialize() first."),
821
- );
822
- throw new Error("Agent not initialized. Call initialize() first.");
823
- }
824
-
825
- const messages = [new HumanMessage(query)];
826
-
827
- logger.debug("AGENT", "Invoking agent stream with messages", {
828
- messageCount: messages.length,
829
- hasContext: !!context,
830
- });
831
-
832
- const cleanup = () => {
833
- // Signal interruption for potential future use
834
- };
835
-
836
- logger.debug("AGENT", "Setting up interruption handlers for streaming");
837
- process.on("SIGINT", cleanup);
838
- process.on("SIGTERM", cleanup);
839
-
840
- try {
841
- const result = await this.agent.invoke(
842
- { messages },
843
- context ? { context, recursionLimit: 100 } : { recursionLimit: 100 }
844
- );
845
-
846
- const content = extractMessageContent(result);
847
- if (content) {
848
- yield content;
849
- }
850
-
851
- yield "\n";
852
- } catch (error) {
853
- const errorMessage = getStreamingErrorMessage(error);
854
- logger.error(
855
- "AGENT",
856
- "Streaming error",
857
- error instanceof Error ? error : new Error(errorMessage),
858
- );
859
- yield `\n\n[Error: ${errorMessage}]`;
860
- throw error;
861
- } finally {
862
- process.removeListener("SIGINT", cleanup);
863
- process.removeListener("SIGTERM", cleanup);
864
- logger.timingEnd(timingId, "AGENT", "Streaming completed");
865
- }
866
- }
245
+ Remember that ALL tool calls MUST be executed using absolute path in \`${workingDir}\``
246
+ );
247
+ prompt = prompt.replace("</context_block>", "");
248
+ } else {
249
+ prompt = prompt.replace(
250
+ "<context_block>",
251
+ `You have been provided several related repositories to work with grouped in the following working directory: ${workingDir}
252
+
253
+ Remember that ALL tool calls MUST be executed using absolute path in \`${workingDir}\``
254
+ );
255
+ prompt = prompt.replace("</context_block>", "");
256
+ }
257
+
258
+ logger.debug("AGENT", "Dynamic system prompt generated", {
259
+ hasTechnologyContext: !!technology,
260
+ promptLength: prompt.length,
261
+ });
262
+
263
+ return prompt;
264
+ }
265
+
266
+ private async createGeminiTempDir(): Promise<string> {
267
+ const tempDir = path.join(os.tmpdir(), `librarian-gemini-${Date.now()}`);
268
+ await mkdir(tempDir, { recursive: true });
269
+ return tempDir;
270
+ }
271
+
272
+ private async setupGeminiConfig(
273
+ tempDir: string,
274
+ systemPrompt: string,
275
+ _model: string
276
+ ): Promise<{ systemPromptPath: string; settingsPath: string }> {
277
+ const systemPromptPath = path.join(tempDir, "system.md");
278
+ const settingsPath = path.join(tempDir, "settings.json");
279
+
280
+ await Bun.write(systemPromptPath, systemPrompt);
281
+
282
+ const settings = {
283
+ tools: {
284
+ core: ["list_directory", "read_file", "glob", "search_file_content"],
285
+ autoAccept: true,
286
+ },
287
+ mcpServers: {},
288
+ mcp: {
289
+ excluded: ["*"],
290
+ },
291
+ experimental: {
292
+ enableAgents: false,
293
+ },
294
+ output: {
295
+ format: "json",
296
+ },
297
+ };
298
+ await Bun.write(settingsPath, JSON.stringify(settings, null, 2));
299
+
300
+ return { systemPromptPath, settingsPath };
301
+ }
302
+
303
+ private buildGeminiEnv(
304
+ tempDir: string,
305
+ model: string
306
+ ): Record<string, string | undefined> {
307
+ const settingsPath = path.join(tempDir, "settings.json");
308
+ const systemPromptPath = path.join(tempDir, "system.md");
309
+
310
+ return {
311
+ ...Bun.env,
312
+ GEMINI_SYSTEM_MD: systemPromptPath,
313
+ GEMINI_CLI_SYSTEM_DEFAULTS_PATH: settingsPath,
314
+ GEMINI_CLI_SYSTEM_SETTINGS_PATH: settingsPath,
315
+ GEMINI_MODEL: model,
316
+ };
317
+ }
318
+
319
+ private async cleanupGeminiTempDir(tempDir: string): Promise<void> {
320
+ try {
321
+ await rm(tempDir, { recursive: true, force: true });
322
+ } catch (err) {
323
+ logger.warn("AGENT", "Failed to cleanup Gemini temp files", {
324
+ error: err,
325
+ });
326
+ }
327
+ }
328
+
329
+ private async *streamClaudeCli(
330
+ query: string,
331
+ context?: AgentContext
332
+ ): AsyncGenerator<string, void, unknown> {
333
+ const workingDir = context?.workingDir || this.config.workingDir;
334
+ const systemPrompt = this.createDynamicSystemPrompt();
335
+
336
+ const args = [
337
+ "-p",
338
+ query,
339
+ "--system-prompt",
340
+ systemPrompt,
341
+ "--tools",
342
+ "Read,Glob,Grep",
343
+ "--dangerously-skip-permissions",
344
+ "--output-format",
345
+ "stream-json",
346
+ ];
347
+
348
+ const env = {
349
+ ...Bun.env,
350
+ CLAUDE_PROJECT_DIR: workingDir,
351
+ ...(this.config.aiProvider.model && {
352
+ ANTHROPIC_MODEL: this.config.aiProvider.model,
353
+ }),
354
+ };
355
+
356
+ logger.debug("AGENT", "Spawning Claude CLI", {
357
+ args: args.map((a) => (a.length > 100 ? `${a.substring(0, 100)}...` : a)),
358
+ workingDir,
359
+ });
360
+
361
+ const proc = spawn("claude", args, {
362
+ cwd: workingDir,
363
+ env,
364
+ });
365
+
366
+ let buffer = "";
367
+
368
+ if (!proc.stdout) {
369
+ throw new Error("Failed to capture Claude CLI output");
370
+ }
371
+
372
+ const readable = Readable.from(proc.stdout);
373
+
374
+ for await (const chunk of readable) {
375
+ buffer += chunk.toString();
376
+ const lines = buffer.split("\n");
377
+ buffer = lines.pop() || "";
378
+
379
+ for (const line of lines) {
380
+ if (!line.trim()) {
381
+ continue;
382
+ }
383
+ try {
384
+ const data = JSON.parse(line);
385
+ // Filter for text content blocks in the stream
386
+ if (data.type === "text" && data.content) {
387
+ yield data.content;
388
+ } else if (data.type === "content_block_delta" && data.delta?.text) {
389
+ yield data.delta.text;
390
+ } else if (data.type === "message" && Array.isArray(data.content)) {
391
+ // Final message might come as a whole
392
+ for (const block of data.content) {
393
+ if (block.type === "text" && block.text) {
394
+ yield block.text;
395
+ }
396
+ }
397
+ }
398
+ } catch {
399
+ // Silent fail for non-JSON or partial lines
400
+ }
401
+ }
402
+ }
403
+
404
+ // Wait for process to exit
405
+ await new Promise<void>((resolve, reject) => {
406
+ proc.on("exit", (code) => {
407
+ if (code === 0) {
408
+ resolve();
409
+ } else {
410
+ reject(new Error(`Claude CLI exited with code ${code}`));
411
+ }
412
+ });
413
+ proc.on("error", reject);
414
+ });
415
+ }
416
+
417
+ private async *streamGeminiCli(
418
+ query: string,
419
+ context?: AgentContext
420
+ ): AsyncGenerator<string, void, unknown> {
421
+ const workingDir = context?.workingDir || this.config.workingDir;
422
+ const systemPrompt = this.createDynamicSystemPrompt();
423
+
424
+ const tempDir = await this.createGeminiTempDir();
425
+ const model = this.config.aiProvider.model || "gemini-2.5-flash";
426
+
427
+ try {
428
+ await this.setupGeminiConfig(tempDir, systemPrompt, model);
429
+
430
+ const args = [
431
+ "gemini",
432
+ "-p",
433
+ query,
434
+ "--output-format",
435
+ "stream-json",
436
+ "--yolo",
437
+ ];
438
+
439
+ const env = this.buildGeminiEnv(tempDir, model);
440
+
441
+ logger.debug("AGENT", "Spawning Gemini CLI", {
442
+ args,
443
+ workingDir,
444
+ model,
445
+ });
446
+
447
+ const proc = Bun.spawn(args, {
448
+ cwd: workingDir,
449
+ env,
450
+ stdout: "pipe",
451
+ stderr: "pipe",
452
+ });
453
+
454
+ const reader = proc.stdout.getReader();
455
+ let buffer = "";
456
+
457
+ while (true) {
458
+ const { done, value } = await reader.read();
459
+ if (done) {
460
+ break;
461
+ }
462
+
463
+ buffer += new TextDecoder().decode(value);
464
+ const lines = buffer.split("\n");
465
+ buffer = lines.pop() || "";
466
+
467
+ for (const line of lines) {
468
+ if (!line.trim()) {
469
+ continue;
470
+ }
471
+ try {
472
+ const data = JSON.parse(line);
473
+ const text = this.parseGeminiStreamLine(data);
474
+ if (text) {
475
+ yield text;
476
+ }
477
+ } catch {
478
+ // Silent fail for non-JSON or partial lines
479
+ }
480
+ }
481
+ }
482
+
483
+ const exitCode = await proc.exited;
484
+ if (exitCode !== 0) {
485
+ throw new Error(`Gemini CLI exited with code ${exitCode}`);
486
+ }
487
+ } finally {
488
+ await this.cleanupGeminiTempDir(tempDir);
489
+ }
490
+ }
491
+
492
+ private parseGeminiStreamLine(data: unknown): string | null {
493
+ if (
494
+ data &&
495
+ typeof data === "object" &&
496
+ "type" in data &&
497
+ "role" in data &&
498
+ "content" in data
499
+ ) {
500
+ const typedData = data as { type: string; role: string; content: string };
501
+ if (
502
+ typedData.type === "message" &&
503
+ typedData.role === "assistant" &&
504
+ typedData.content
505
+ ) {
506
+ return typedData.content;
507
+ }
508
+ }
509
+ return null;
510
+ }
511
+
512
+ private createAIModel(
513
+ aiProvider: ReactAgentConfig["aiProvider"]
514
+ ): ChatOpenAI | ChatAnthropic | ChatGoogleGenerativeAI {
515
+ const { type, apiKey, model, baseURL } = aiProvider;
516
+
517
+ logger.debug("AGENT", "Creating AI model instance", {
518
+ type,
519
+ model,
520
+ hasBaseURL: !!baseURL,
521
+ });
522
+
523
+ switch (type) {
524
+ case "openai":
525
+ return new ChatOpenAI({
526
+ apiKey,
527
+ modelName: model || "gpt-5.2",
528
+ });
529
+ case "openai-compatible":
530
+ return new ChatOpenAI({
531
+ apiKey,
532
+ modelName: model || "gpt-5.2",
533
+ configuration: {
534
+ baseURL: baseURL || "https://api.openai.com/v1",
535
+ },
536
+ });
537
+ case "anthropic":
538
+ return new ChatAnthropic({
539
+ apiKey,
540
+ modelName: model || "claude-sonnet-4-5",
541
+ });
542
+ case "anthropic-compatible":
543
+ if (!baseURL) {
544
+ throw new Error(
545
+ "baseURL is required for anthropic-compatible provider"
546
+ );
547
+ }
548
+ if (!model) {
549
+ throw new Error(
550
+ "model is required for anthropic-compatible provider"
551
+ );
552
+ }
553
+ return new ChatAnthropic({
554
+ apiKey,
555
+ modelName: model,
556
+ anthropicApiUrl: baseURL,
557
+ });
558
+ case "google":
559
+ return new ChatGoogleGenerativeAI({
560
+ apiKey,
561
+ model: model || "gemini-3-flash-preview",
562
+ });
563
+ default:
564
+ logger.error(
565
+ "AGENT",
566
+ "Unsupported AI provider type",
567
+ new Error(`Unsupported AI provider type: ${type}`),
568
+ { type }
569
+ );
570
+ throw new Error(`Unsupported AI provider type: ${type}`);
571
+ }
572
+ }
573
+
574
+ initialize(): Promise<void> {
575
+ if (
576
+ this.config.aiProvider.type === "claude-code" ||
577
+ this.config.aiProvider.type === "gemini-cli"
578
+ ) {
579
+ logger.info(
580
+ "AGENT",
581
+ `${this.config.aiProvider.type} CLI mode initialized (skipping LangChain setup)`
582
+ );
583
+ return Promise.resolve();
584
+ }
585
+
586
+ if (!this.aiModel) {
587
+ throw new Error("AI model not created for non-CLI provider");
588
+ }
589
+
590
+ // Create the agent using LangChain's createAgent function with dynamic system prompt
591
+ this.agent = createAgent({
592
+ model: this.aiModel,
593
+ tools: this.tools,
594
+ systemPrompt: this.createDynamicSystemPrompt(),
595
+ middleware: [
596
+ todoListMiddleware(),
597
+ ...(this.config.aiProvider.type === "anthropic" ||
598
+ this.config.aiProvider.type === "anthropic-compatible"
599
+ ? [anthropicPromptCachingMiddleware()]
600
+ : []),
601
+ ],
602
+ });
603
+
604
+ logger.info("AGENT", "Agent initialized successfully", {
605
+ toolCount: this.tools.length,
606
+ hasContextSchema: !!this.contextSchema,
607
+ });
608
+
609
+ return Promise.resolve();
610
+ }
611
+
612
+ /**
613
+ * Query repository with a given query and optional context
614
+ *
615
+ * @param repoPath - The repository path (deprecated, for compatibility)
616
+ * @param query - The query string
617
+ * @param context - Optional context object containing working directory and metadata
618
+ * @returns The agent's response as a string
619
+ */
620
+ async queryRepository(
621
+ _repoPath: string,
622
+ query: string,
623
+ context?: AgentContext
624
+ ): Promise<string> {
625
+ logger.info("AGENT", "Query started", {
626
+ queryLength: query.length,
627
+ hasContext: !!context,
628
+ });
629
+
630
+ if (this.config.aiProvider.type === "claude-code") {
631
+ let fullContent = "";
632
+ for await (const chunk of this.streamClaudeCli(query, context)) {
633
+ fullContent += chunk;
634
+ }
635
+ return fullContent;
636
+ }
637
+
638
+ if (this.config.aiProvider.type === "gemini-cli") {
639
+ let fullContent = "";
640
+ for await (const chunk of this.streamGeminiCli(query, context)) {
641
+ fullContent += chunk;
642
+ }
643
+ return fullContent;
644
+ }
645
+
646
+ const timingId = logger.timingStart("agentQuery");
647
+
648
+ if (!this.agent) {
649
+ logger.error(
650
+ "AGENT",
651
+ "Agent not initialized",
652
+ new Error("Agent not initialized. Call initialize() first.")
653
+ );
654
+ throw new Error("Agent not initialized. Call initialize() first.");
655
+ }
656
+
657
+ // Prepare the messages for the agent - system prompt already set during initialization
658
+ const messages = [new HumanMessage(query)];
659
+
660
+ logger.debug("AGENT", "Invoking agent with messages", {
661
+ messageCount: messages.length,
662
+ hasContext: !!context,
663
+ });
664
+
665
+ // Execute the agent with optional context
666
+ const result = await this.agent.invoke(
667
+ {
668
+ messages,
669
+ },
670
+ context ? { context, recursionLimit: 100 } : { recursionLimit: 100 }
671
+ );
672
+
673
+ // Extract the last message content from the state
674
+ const lastMessage = result.messages.at(-1);
675
+ const content =
676
+ typeof lastMessage.content === "string"
677
+ ? lastMessage.content
678
+ : JSON.stringify(lastMessage.content);
679
+
680
+ logger.timingEnd(timingId, "AGENT", "Query completed");
681
+ logger.info("AGENT", "Query result received", {
682
+ responseLength: content.length,
683
+ });
684
+
685
+ return content;
686
+ }
687
+
688
+ /**
689
+ * Stream repository query with optional context
690
+ *
691
+ * @param repoPath - The repository path (deprecated, for compatibility)
692
+ * @param query - The query string
693
+ * @param context - Optional context object containing working directory and metadata
694
+ * @returns Async generator yielding string chunks
695
+ */
696
+ async *streamRepository(
697
+ _repoPath: string,
698
+ query: string,
699
+ context?: AgentContext
700
+ ): AsyncGenerator<string, void, unknown> {
701
+ logger.info("AGENT", "Stream started", {
702
+ queryLength: query.length,
703
+ hasContext: !!context,
704
+ });
705
+
706
+ if (this.config.aiProvider.type === "claude-code") {
707
+ yield* this.streamClaudeCli(query, context);
708
+ return;
709
+ }
710
+
711
+ if (this.config.aiProvider.type === "gemini-cli") {
712
+ yield* this.streamGeminiCli(query, context);
713
+ return;
714
+ }
715
+
716
+ const timingId = logger.timingStart("agentStream");
717
+
718
+ if (!this.agent) {
719
+ logger.error(
720
+ "AGENT",
721
+ "Agent not initialized",
722
+ new Error("Agent not initialized. Call initialize() first.")
723
+ );
724
+ throw new Error("Agent not initialized. Call initialize() first.");
725
+ }
726
+
727
+ const messages = [new HumanMessage(query)];
728
+
729
+ logger.debug("AGENT", "Invoking agent stream with messages", {
730
+ messageCount: messages.length,
731
+ hasContext: !!context,
732
+ });
733
+
734
+ const cleanup = () => {
735
+ // Signal interruption for potential future use
736
+ };
737
+
738
+ logger.debug("AGENT", "Setting up interruption handlers for streaming");
739
+ process.on("SIGINT", cleanup);
740
+ process.on("SIGTERM", cleanup);
741
+
742
+ try {
743
+ const result = await this.agent.invoke(
744
+ { messages },
745
+ context ? { context, recursionLimit: 100 } : { recursionLimit: 100 }
746
+ );
747
+
748
+ const content = extractMessageContent(result);
749
+ if (content) {
750
+ yield content;
751
+ }
752
+
753
+ yield "\n";
754
+ } catch (error) {
755
+ const errorMessage = getStreamingErrorMessage(error);
756
+ logger.error(
757
+ "AGENT",
758
+ "Streaming error",
759
+ error instanceof Error ? error : new Error(errorMessage)
760
+ );
761
+ yield `\n\n[Error: ${errorMessage}]`;
762
+ throw error;
763
+ } finally {
764
+ process.removeListener("SIGINT", cleanup);
765
+ process.removeListener("SIGTERM", cleanup);
766
+ logger.timingEnd(timingId, "AGENT", "Streaming completed");
767
+ }
768
+ }
867
769
  }
868
770
 
869
771
  /**
870
772
  * Extract content from the last message in the result
871
773
  */
872
- function extractMessageContent(result: { messages?: Array<{ content: unknown }> }): string | null {
873
- if (!result.messages || result.messages.length === 0) {
874
- return null;
875
- }
876
-
877
- const lastMessage = result.messages.at(-1);
878
- if (!lastMessage?.content) {
879
- return null;
880
- }
881
-
882
- const content = lastMessage.content;
883
- if (typeof content === "string") {
884
- return content;
885
- }
886
-
887
- if (Array.isArray(content)) {
888
- const parts: string[] = [];
889
- for (const block of content) {
890
- if (block && typeof block === "object") {
891
- const blockObj = block as { type?: string; text?: unknown };
892
- if (blockObj.type === "text" && typeof blockObj.text === "string") {
893
- parts.push(blockObj.text);
894
- }
895
- }
896
- }
897
- return parts.length > 0 ? parts.join("") : null;
898
- }
899
-
900
- return null;
774
+ function extractMessageContent(result: {
775
+ messages?: Array<{ content: unknown }>;
776
+ }): string | null {
777
+ if (!result.messages || result.messages.length === 0) {
778
+ return null;
779
+ }
780
+
781
+ const lastMessage = result.messages.at(-1);
782
+ if (!lastMessage?.content) {
783
+ return null;
784
+ }
785
+
786
+ const content = lastMessage.content;
787
+ if (typeof content === "string") {
788
+ return content;
789
+ }
790
+
791
+ if (Array.isArray(content)) {
792
+ const parts: string[] = [];
793
+ for (const block of content) {
794
+ if (block && typeof block === "object") {
795
+ const blockObj = block as { type?: string; text?: unknown };
796
+ if (blockObj.type === "text" && typeof blockObj.text === "string") {
797
+ parts.push(blockObj.text);
798
+ }
799
+ }
800
+ }
801
+ return parts.length > 0 ? parts.join("") : null;
802
+ }
803
+
804
+ return null;
901
805
  }
902
806
 
903
807
  /**
904
808
  * Get user-friendly error message for streaming errors
905
809
  */
906
810
  function getStreamingErrorMessage(error: unknown): string {
907
- if (!(error instanceof Error)) {
908
- return "Unknown streaming error";
909
- }
910
-
911
- if (error.message.includes("timeout")) {
912
- return "Streaming timeout - request took too long to complete";
913
- }
914
-
915
- if (error.message.includes("network") || error.message.includes("ENOTFOUND")) {
916
- return "Network error - unable to connect to AI provider";
917
- }
918
-
919
- if (error.message.includes("rate limit")) {
920
- return "Rate limit exceeded - please try again later";
921
- }
922
-
923
- if (error.message.includes("authentication") || error.message.includes("unauthorized")) {
924
- return "Authentication error - check your API credentials";
925
- }
926
-
927
- return `Streaming error: ${error.message}`;
811
+ if (!(error instanceof Error)) {
812
+ return "Unknown streaming error";
813
+ }
814
+
815
+ if (error.message.includes("timeout")) {
816
+ return "Streaming timeout - request took too long to complete";
817
+ }
818
+
819
+ if (
820
+ error.message.includes("network") ||
821
+ error.message.includes("ENOTFOUND")
822
+ ) {
823
+ return "Network error - unable to connect to AI provider";
824
+ }
825
+
826
+ if (error.message.includes("rate limit")) {
827
+ return "Rate limit exceeded - please try again later";
828
+ }
829
+
830
+ if (
831
+ error.message.includes("authentication") ||
832
+ error.message.includes("unauthorized")
833
+ ) {
834
+ return "Authentication error - check your API credentials";
835
+ }
836
+
837
+ return `Streaming error: ${error.message}`;
928
838
  }