@rahul05ranjan/dhruv-cli 1.3.0 → 1.4.6

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 (103) hide show
  1. package/.github/ENTERPRISE.md +275 -0
  2. package/.github/ISSUE_TEMPLATE/bug_report.md +45 -17
  3. package/.github/ISSUE_TEMPLATE/documentation_issue.md +61 -0
  4. package/.github/ISSUE_TEMPLATE/feature_request.md +61 -9
  5. package/.github/ISSUE_TEMPLATE/security_vulnerability.md +74 -0
  6. package/.github/dependabot.yml +42 -2
  7. package/.github/pull_request_template.md +13 -0
  8. package/.github/workflows/build-publish.yml +154 -0
  9. package/.github/workflows/ci.yml +251 -18
  10. package/.github/workflows/contribution.yml +169 -27
  11. package/.github/workflows/dependabot-auto-merge.yml +61 -2
  12. package/.github/workflows/deploy.yml +336 -0
  13. package/.github/workflows/monitoring.yml +270 -0
  14. package/.github/workflows/release.yml +229 -0
  15. package/.github/workflows/security.yml +198 -0
  16. package/.releaserc.json +50 -0
  17. package/AGENTS.md +13 -0
  18. package/CHANGELOG.md +8 -0
  19. package/PUBLISHING_FIX.md +92 -0
  20. package/__tests__/core.test.ts +318 -0
  21. package/__tests__/setup.ts +61 -0
  22. package/__tests__/workflows.test.ts +95 -0
  23. package/dist/commands/explain.js +13 -44
  24. package/dist/commands/fix.js +13 -38
  25. package/dist/commands/generate.js +45 -54
  26. package/dist/commands/health.d.ts +1 -0
  27. package/dist/commands/health.js +376 -0
  28. package/dist/commands/init.js +47 -42
  29. package/dist/commands/menu.js +90 -2
  30. package/dist/commands/metrics.d.ts +1 -0
  31. package/dist/commands/metrics.js +51 -0
  32. package/dist/commands/optimize.js +42 -34
  33. package/dist/commands/review.js +52 -48
  34. package/dist/commands/security-check.js +52 -42
  35. package/dist/commands/status.d.ts +1 -0
  36. package/dist/commands/status.js +45 -0
  37. package/dist/commands/suggest.js +13 -39
  38. package/dist/config/config.js +30 -2
  39. package/dist/core/ai.d.ts +77 -2
  40. package/dist/core/ai.js +207 -30
  41. package/dist/core/command-runner.d.ts +17 -0
  42. package/dist/core/command-runner.js +78 -0
  43. package/dist/core/logger.d.ts +40 -0
  44. package/dist/core/logger.js +138 -0
  45. package/dist/core/metrics.d.ts +34 -0
  46. package/dist/core/metrics.js +206 -0
  47. package/dist/core/prompts.d.ts +1 -0
  48. package/dist/core/prompts.js +121 -0
  49. package/dist/core/security.d.ts +34 -0
  50. package/dist/core/security.js +197 -0
  51. package/dist/index.js +43 -3
  52. package/dist/utils/ux.d.ts +3 -0
  53. package/dist/utils/ux.js +15 -0
  54. package/docs/agents/domain.md +51 -0
  55. package/docs/agents/issue-tracker.md +45 -0
  56. package/docs/agents/triage-labels.md +15 -0
  57. package/docs/api/.nojekyll +1 -0
  58. package/docs/api/assets/hierarchy.js +1 -0
  59. package/docs/api/assets/highlight.css +71 -0
  60. package/docs/api/assets/icons.js +18 -0
  61. package/docs/api/assets/icons.svg +1 -0
  62. package/docs/api/assets/main.js +60 -0
  63. package/docs/api/assets/navigation.js +1 -0
  64. package/docs/api/assets/search.js +1 -0
  65. package/docs/api/assets/style.css +1633 -0
  66. package/docs/api/hierarchy.html +1 -0
  67. package/docs/api/index.html +39 -0
  68. package/docs/api/modules.html +1 -0
  69. package/eslint.config.js +170 -0
  70. package/jest.config.json +37 -0
  71. package/lighthouserc.json +22 -0
  72. package/logs/.8a99b6cf655346317fdbf29f4fffcf91131432f3-audit.json +15 -0
  73. package/logs/.eee104bf8fff5ecd38a6a2842df260de6470a7c3-audit.json +15 -0
  74. package/package.json +62 -8
  75. package/src/commands/explain.ts +13 -42
  76. package/src/commands/fix.ts +13 -31
  77. package/src/commands/generate.ts +47 -46
  78. package/src/commands/health.ts +440 -0
  79. package/src/commands/init.ts +48 -42
  80. package/src/commands/menu.ts +86 -2
  81. package/src/commands/metrics.ts +65 -0
  82. package/src/commands/optimize.ts +40 -28
  83. package/src/commands/review.ts +54 -40
  84. package/src/commands/security-check.ts +54 -34
  85. package/src/commands/status.ts +47 -0
  86. package/src/commands/suggest.ts +13 -32
  87. package/src/config/config.ts +35 -2
  88. package/src/core/ai.ts +237 -26
  89. package/src/core/command-runner.ts +105 -0
  90. package/src/core/logger.ts +194 -0
  91. package/src/core/metrics.ts +232 -0
  92. package/src/core/prompts.ts +128 -0
  93. package/src/core/security.ts +243 -0
  94. package/src/index.ts +50 -3
  95. package/src/utils/ux.ts +18 -0
  96. package/test-suite.sh +147 -0
  97. package/tsconfig.json +3 -2
  98. package/typedoc.json +44 -0
  99. package/types/global.d.ts +13 -0
  100. package/validate-workflows.sh +270 -0
  101. package/.eslintignore +0 -1
  102. package/.eslintrc.cjs +0 -43
  103. package/src/core/ai.test.js +0 -40
package/src/core/ai.ts CHANGED
@@ -1,41 +1,252 @@
1
+ /**
2
+ * The AI module: everything about talking to a local model lives here.
3
+ *
4
+ * Interface (the only surface callers and tests cross):
5
+ * ask(request) -> full response, optionally streaming tokens via onToken
6
+ * listModels() -> available model names
7
+ *
8
+ * Everything else — connection handling, streaming, caching, error
9
+ * translation — is implementation. Two adapters satisfy the interface:
10
+ * the HTTP adapter (production, talks to the local Ollama server) and the
11
+ * in-memory adapter (tests). No third adapter exists.
12
+ */
1
13
  import { Ollama } from 'ollama';
2
14
  import fs from 'fs';
3
15
  import path from 'path';
4
16
  import crypto from 'crypto';
17
+ import { loadConfig } from '../config/config.js';
5
18
 
6
- const CACHE_DIR = path.join(process.cwd(), '.dhruv-cache');
7
- if (!fs.existsSync(CACHE_DIR)) fs.mkdirSync(CACHE_DIR);
19
+ /** Typed errors: the runner maps these to user-facing hints, never by string matching. */
20
+ export type AIError =
21
+ | { kind: 'connection'; cause: string }
22
+ | { kind: 'model-not-found'; model: string }
23
+ | { kind: 'empty-response'; model: string }
24
+ | { kind: 'request'; cause: string };
8
25
 
9
- function getCacheKey(prompt: string, model?: string) {
10
- const hash = crypto.createHash('sha256').update(`${model || 'default'}:${prompt}`).digest('hex');
11
- return path.join(CACHE_DIR, hash);
26
+ export interface AIRequest {
27
+ prompt: string;
28
+ systemMessage?: string;
29
+ context?: string;
30
+ model?: string;
31
+ onToken?: (token: string) => void;
12
32
  }
13
33
 
14
- export async function askOllama({ prompt, model, onToken }: { prompt: string; model?: string; onToken?: (token: string) => void }) {
15
- const cacheKey = getCacheKey(prompt, model);
16
- if (fs.existsSync(cacheKey)) {
17
- const cached = fs.readFileSync(cacheKey, 'utf-8');
18
- if (onToken) onToken(cached);
34
+ /** The seam. Both adapters implement this; commands and tests depend on it, never on Ollama. */
35
+ export interface AIClient {
36
+ ask(request: AIRequest): Promise<string>;
37
+ listModels(): Promise<string[]>;
38
+ }
39
+
40
+ const CACHE_EXPIRY_MS = 24 * 60 * 60 * 1000; // 24 hours
41
+ const MAX_CACHE_FILES = 100;
42
+
43
+ function cacheDir(): string {
44
+ return path.join(process.cwd(), '.dhruv-cache');
45
+ }
46
+
47
+ function cacheKey(request: AIRequest, model: string): string {
48
+ const hash = crypto
49
+ .createHash('sha256')
50
+ .update(`${model}:${request.systemMessage ?? ''}:${request.context ?? ''}:${request.prompt}`)
51
+ .digest('hex');
52
+ return path.join(cacheDir(), hash);
53
+ }
54
+
55
+ function readCache(request: AIRequest, model: string): string | undefined {
56
+ const file = cacheKey(request, model);
57
+ try {
58
+ const cached = fs.readFileSync(file, 'utf-8');
59
+ const stats = fs.statSync(file);
60
+ if (Date.now() - stats.mtimeMs > CACHE_EXPIRY_MS) {
61
+ fs.unlinkSync(file);
62
+ return undefined;
63
+ }
19
64
  return cached;
65
+ } catch {
66
+ return undefined;
20
67
  }
68
+ }
69
+
70
+ function writeCache(request: AIRequest, model: string, response: string): void {
71
+ try {
72
+ const dir = cacheDir();
73
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
74
+ fs.writeFileSync(cacheKey(request, model), response);
75
+ } catch {
76
+ // Cache write failures never fail the request.
77
+ }
78
+ }
79
+
80
+ /** Wired up (the old cleanupCache was never called); runs opportunistically after a cache write. */
81
+ function cleanupCache(): void {
21
82
  try {
22
- let result = '';
23
- const ollama = new Ollama();
24
- // Await the iterator, then stream
25
- const iterator = await ollama.generate({ model: model || 'codellama', prompt, stream: true });
26
- for await (const chunk of iterator) {
27
- let token = '';
28
- if (typeof chunk === 'object' && chunk !== null && 'response' in chunk) {
29
- token = (chunk as { response: string }).response;
30
- } else if (typeof chunk === 'string') {
31
- token = chunk;
83
+ const dir = cacheDir();
84
+ if (!fs.existsSync(dir)) return;
85
+ const entries = fs
86
+ .readdirSync(dir)
87
+ .map((name) => {
88
+ const file = path.join(dir, name);
89
+ return { file, mtimeMs: fs.statSync(file).mtimeMs };
90
+ })
91
+ .filter((entry) => {
92
+ if (Date.now() - entry.mtimeMs > CACHE_EXPIRY_MS) {
93
+ fs.unlinkSync(entry.file);
94
+ return false;
95
+ }
96
+ return true;
97
+ })
98
+ .sort((a, b) => a.mtimeMs - b.mtimeMs);
99
+ const excess = entries.length - MAX_CACHE_FILES;
100
+ if (excess > 0) {
101
+ for (const entry of entries.slice(0, excess)) fs.unlinkSync(entry.file);
102
+ }
103
+ } catch {
104
+ // Ignore cleanup errors.
105
+ }
106
+ }
107
+
108
+ function toAIError(err: unknown, model: string): AIError {
109
+ const message = err instanceof Error ? err.message : String(err);
110
+ if (message.includes('ECONNREFUSED') || message.includes('fetch failed') || message.includes('ENOTFOUND')) {
111
+ return { kind: 'connection', cause: message };
112
+ }
113
+ if (message.includes('not found')) {
114
+ return { kind: 'model-not-found', model };
115
+ }
116
+ return { kind: 'request', cause: message };
117
+ }
118
+
119
+ /**
120
+ * HTTP adapter: production. Standardizes on the Ollama client package —
121
+ * the raw-HTTP path existed only to work around a LangChain prompt-template
122
+ * issue, and that integration is gone.
123
+ */
124
+ export class OllamaAIClient implements AIClient {
125
+ private client: Ollama;
126
+
127
+ constructor(client?: Ollama) {
128
+ this.client = client ?? new Ollama();
129
+ }
130
+
131
+ async ask(request: AIRequest): Promise<string> {
132
+ const model = request.model ?? loadConfig().model;
133
+ const fullPrompt = request.systemMessage
134
+ ? `System: ${request.systemMessage}\n\n${request.context ? `Context: ${request.context}\n\n` : ''}Query: ${request.prompt}`
135
+ : request.prompt;
136
+
137
+ const cached = readCache(request, model);
138
+ if (cached !== undefined) {
139
+ if (request.onToken) request.onToken(cached);
140
+ return cached;
141
+ }
142
+
143
+ try {
144
+ const streaming = Boolean(request.onToken);
145
+ let result = '';
146
+
147
+ if (streaming) {
148
+ const stream = await this.client.generate({ model, prompt: fullPrompt, stream: true });
149
+ for await (const chunk of stream) {
150
+ const token = typeof chunk === 'object' && chunk !== null && 'response' in chunk ? chunk.response : '';
151
+ if (!token) continue;
152
+ result += token;
153
+ if (request.onToken) request.onToken(token);
154
+ }
155
+ } else {
156
+ const response = await this.client.generate({ model, prompt: fullPrompt, stream: false });
157
+ result = response.response ?? '';
158
+ }
159
+
160
+ if (!result.trim()) {
161
+ throw new Error(`Model '${model}' not found or returned empty response`);
32
162
  }
33
- if (onToken) onToken(token);
34
- result += token;
163
+
164
+ writeCache(request, model, result.trim());
165
+ cleanupCache();
166
+ return result.trim();
167
+ } catch (err) {
168
+ throw toAIError(err, model);
35
169
  }
36
- fs.writeFileSync(cacheKey, result.trim());
37
- return result.trim();
38
- } catch (err) {
39
- throw new Error('Ollama AI error: ' + (err as Error).message);
40
170
  }
171
+
172
+ async listModels(): Promise<string[]> {
173
+ try {
174
+ const models = await this.client.list();
175
+ return (models.models ?? []).map((m) => m.name);
176
+ } catch (err) {
177
+ throw toAIError(err, 'unknown');
178
+ }
179
+ }
180
+ }
181
+
182
+ /**
183
+ * In-memory adapter: tests. Satisfies the same interface with no network,
184
+ * which is what makes AI behavior testable without Ollama installed.
185
+ */
186
+ export class InMemoryAIClient implements AIClient {
187
+ private store = new Map<string, { response: string; createdAt: number }>();
188
+ /** Failures to simulate, keyed by prompt substring. */
189
+ failures = new Map<string, AIError>();
190
+ /** Count of computations performed (not cache reads) — lets tests observe cache hits. */
191
+ computations = 0;
192
+ /** Simulated clock for expiry tests. */
193
+ now = () => Date.now();
194
+ /** TTL override for tests; defaults to the production expiry. */
195
+ ttlMs: number = CACHE_EXPIRY_MS;
196
+
197
+ constructor(private responses: Map<string, string> = new Map()) {}
198
+
199
+ async ask(request: AIRequest): Promise<string> {
200
+ const model = request.model ?? 'test-model';
201
+ for (const [substring, failure] of this.failures) {
202
+ if (request.prompt.includes(substring)) throw failure;
203
+ }
204
+
205
+ const key = `${model}:${request.systemMessage ?? ''}:${request.context ?? ''}:${request.prompt}`;
206
+ const hit = this.store.get(key);
207
+ if (hit && this.now() - hit.createdAt <= this.ttlMs) {
208
+ if (request.onToken) request.onToken(hit.response);
209
+ return hit.response;
210
+ }
211
+
212
+ this.computations += 1;
213
+ const response = this.responses.get(request.prompt) ?? `response:${request.prompt}`;
214
+ this.store.set(key, { response, createdAt: this.now() });
215
+ if (request.onToken) request.onToken(response);
216
+ return response;
217
+ }
218
+
219
+ async listModels(): Promise<string[]> {
220
+ return ['test-model', 'other-model'];
221
+ }
222
+ }
223
+
224
+ /** Default client: the HTTP adapter. Tests inject InMemoryAIClient instead. */
225
+ let defaultClient: AIClient | undefined;
226
+
227
+ export function getAIClient(): AIClient {
228
+ if (!defaultClient) defaultClient = new OllamaAIClient();
229
+ return defaultClient;
230
+ }
231
+
232
+ /** Test seam setter: swaps the adapter the module hands out. */
233
+ export function setAIClient(client: AIClient): void {
234
+ defaultClient = client;
235
+ }
236
+
237
+ /**
238
+ * The interface commands call. Kept as module functions so callers don't
239
+ * reach for a client object; the client is resolved internally.
240
+ */
241
+ export async function ask(request: AIRequest): Promise<string> {
242
+ return getAIClient().ask(request);
243
+ }
244
+
245
+ export async function listModels(): Promise<string[]> {
246
+ return getAIClient().listModels();
247
+ }
248
+
249
+ /** Default model, from configuration — one source of truth. */
250
+ export function defaultModel(): string {
251
+ return loadConfig().model;
41
252
  }
@@ -0,0 +1,105 @@
1
+ /**
2
+ * The command runner: everything about executing an AI-backed command lives
3
+ * here. Commands supply their specifics (prompt, file reading, footer text,
4
+ * post-processing) and call the runner through one interface; the pipeline —
5
+ * validation, rate limiting, spinner, header, AI call, streaming, logging,
6
+ * metrics, error mapping — fires identically for every command.
7
+ */
8
+ import ora from 'ora';
9
+ import chalk from 'chalk';
10
+ import { loadConfig } from '../config/config.js';
11
+ import { printError } from '../utils/ux.js';
12
+ import { ask, AIError, AIRequest } from '../core/ai.js';
13
+ import { logger, logCommand, logPerformance, logError } from '../core/logger.js';
14
+ import { metricsCollector } from '../core/metrics.js';
15
+ import { securityManager } from '../core/security.js';
16
+
17
+ /** What makes a command distinct. The runner owns everything else. */
18
+ export interface CommandSpec {
19
+ /** Commander command name, for validation, logging, and metrics. */
20
+ name: string;
21
+ /** The user-facing arguments, validated as a unit. */
22
+ input: Record<string, string>;
23
+ /** Builds the AI request from validated input. */
24
+ buildRequest: (input: Record<string, string>, model: string) => AIRequest;
25
+ /** Header line under the banner, e.g. "📚 Explanation:". */
26
+ header: string;
27
+ /** Footer hint shown after success. */
28
+ footer?: string;
29
+ /** Post-processing on the full response (e.g. saving generated test files). */
30
+ onComplete?: (response: string, input: Record<string, string>) => void;
31
+ }
32
+
33
+ /** Maps typed AI errors to user-facing hints — once, not per command. */
34
+ function describeAIError(error: AIError, model: string): string {
35
+ switch (error.kind) {
36
+ case 'connection':
37
+ return `💡 Make sure Ollama is running: ollama serve`;
38
+ case 'model-not-found':
39
+ return `💡 Install the model: ollama pull ${error.model || model}`;
40
+ case 'empty-response':
41
+ return `💡 Model returned nothing. Install it: ollama pull ${error.model || model}`;
42
+ default:
43
+ return error.cause;
44
+ }
45
+ }
46
+
47
+ export async function runCommand(spec: CommandSpec): Promise<void> {
48
+ const startTime = Date.now();
49
+ const { name, input } = spec;
50
+ const config = loadConfig();
51
+
52
+ const fail = (error: string): void => {
53
+ printError(error);
54
+ logCommand(name, startTime, false, { error });
55
+ metricsCollector.recordCommand(name, Date.now() - startTime, false);
56
+ };
57
+
58
+ // Validation and rate limiting — every command, uniformly.
59
+ const securityCheck = securityManager.validateInput(name, input);
60
+ if (!securityCheck.valid) {
61
+ fail(securityCheck.error!);
62
+ return;
63
+ }
64
+
65
+ const rateLimitCheck = securityManager.checkRateLimit('user');
66
+ if (!rateLimitCheck.allowed) {
67
+ fail('Rate limit exceeded. Please try again later.');
68
+ return;
69
+ }
70
+
71
+ const spinner = ora('Thinking...').start();
72
+ try {
73
+ spinner.stop();
74
+
75
+ console.log(chalk.yellowBright('🤖 Dhruv CLI: AI-powered developer assistant'));
76
+ console.log(chalk.green.bold(spec.header));
77
+ console.log();
78
+
79
+ const response = await ask(spec.buildRequest(input, config.model));
80
+
81
+ console.log('\n');
82
+ if (spec.footer) {
83
+ console.log(chalk.dim(spec.footer));
84
+ }
85
+
86
+ if (spec.onComplete) spec.onComplete(response, input);
87
+
88
+ const duration = Date.now() - startTime;
89
+ logCommand(name, startTime, true, { model: config.model });
90
+ logPerformance(name, duration);
91
+ metricsCollector.recordCommand(name, duration, true);
92
+ logger.info(`${name} command completed successfully`, { duration });
93
+ } catch (err) {
94
+ spinner.stop();
95
+ const duration = Date.now() - startTime;
96
+
97
+ printError(`Command failed.`);
98
+ console.log(chalk.yellow(describeAIError(err as AIError, config.model)));
99
+
100
+ logError(`${name} command failed`, err as Error, { command: name });
101
+ logCommand(name, startTime, false, { error: (err as Error).message });
102
+ metricsCollector.recordCommand(name, duration, false);
103
+ metricsCollector.recordError('ai_request_failed', name);
104
+ }
105
+ }
@@ -0,0 +1,194 @@
1
+ import winston from 'winston';
2
+ import DailyRotateFile from 'winston-daily-rotate-file';
3
+ import path from 'path';
4
+ import fs from 'fs';
5
+ import { loadConfig } from '../config/config.js';
6
+
7
+ export interface LogEntry {
8
+ timestamp: string;
9
+ level: 'error' | 'warn' | 'info' | 'debug';
10
+ message: string;
11
+ command?: string;
12
+ userId?: string;
13
+ sessionId?: string;
14
+ duration?: number;
15
+ metadata?: Record<string, any>;
16
+ error?: {
17
+ name: string;
18
+ message: string;
19
+ stack?: string;
20
+ };
21
+ }
22
+
23
+ class Logger {
24
+ private logger: winston.Logger;
25
+ private sessionId: string;
26
+
27
+ constructor() {
28
+ this.sessionId = this.generateSessionId();
29
+ this.logger = this.createLogger();
30
+ }
31
+
32
+ private generateSessionId(): string {
33
+ return `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
34
+ }
35
+
36
+ private createLogger(): winston.Logger {
37
+ const config = loadConfig();
38
+ const logDir = path.join(process.cwd(), 'logs');
39
+
40
+ // Ensure log directory exists
41
+ if (!fs.existsSync(logDir)) {
42
+ fs.mkdirSync(logDir, { recursive: true });
43
+ }
44
+
45
+ const logFormat = winston.format.combine(
46
+ winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
47
+ winston.format.errors({ stack: true }),
48
+ winston.format.json(),
49
+ winston.format.printf(({ timestamp, level, message, ...meta }) => {
50
+ return JSON.stringify({
51
+ timestamp,
52
+ level,
53
+ message,
54
+ sessionId: this.sessionId,
55
+ ...meta
56
+ });
57
+ })
58
+ );
59
+
60
+ const transports: winston.transport[] = [
61
+ // Console transport for development
62
+ new winston.transports.Console({
63
+ level: config.verbose ? 'debug' : 'info',
64
+ format: winston.format.combine(
65
+ winston.format.colorize(),
66
+ winston.format.simple(),
67
+ winston.format.printf(({ timestamp, level, message }) => {
68
+ return `${timestamp} ${level}: ${message}`;
69
+ })
70
+ )
71
+ }),
72
+
73
+ // Daily rotating file for all logs
74
+ new DailyRotateFile({
75
+ filename: path.join(logDir, 'dhruv-%DATE%.log'),
76
+ datePattern: 'YYYY-MM-DD',
77
+ maxSize: '20m',
78
+ maxFiles: '14d',
79
+ level: 'debug'
80
+ }),
81
+
82
+ // Separate error log
83
+ new DailyRotateFile({
84
+ filename: path.join(logDir, 'dhruv-error-%DATE%.log'),
85
+ datePattern: 'YYYY-MM-DD',
86
+ maxSize: '20m',
87
+ maxFiles: '30d',
88
+ level: 'error'
89
+ })
90
+ ];
91
+
92
+ return winston.createLogger({
93
+ level: 'debug',
94
+ format: logFormat,
95
+ defaultMeta: { sessionId: this.sessionId },
96
+ transports
97
+ });
98
+ }
99
+
100
+ public info(message: string, meta?: Record<string, any>): void {
101
+ this.logger.info(message, meta);
102
+ }
103
+
104
+ public warn(message: string, meta?: Record<string, any>): void {
105
+ this.logger.warn(message, meta);
106
+ }
107
+
108
+ public error(message: string, error?: Error, meta?: Record<string, any>): void {
109
+ const errorInfo = error ? {
110
+ name: error.name,
111
+ message: error.message,
112
+ stack: error.stack
113
+ } : undefined;
114
+
115
+ this.logger.error(message, { error: errorInfo, ...meta });
116
+ }
117
+
118
+ public debug(message: string, meta?: Record<string, any>): void {
119
+ this.logger.debug(message, meta);
120
+ }
121
+
122
+ public command(command: string, startTime: number, success: boolean, meta?: Record<string, any>): void {
123
+ const duration = Date.now() - startTime;
124
+ const level = success ? 'info' : 'error';
125
+ const message = `Command ${command} ${success ? 'completed' : 'failed'} in ${duration}ms`;
126
+
127
+ this.logger.log(level, message, {
128
+ command,
129
+ duration,
130
+ success,
131
+ ...meta
132
+ });
133
+ }
134
+
135
+ public performance(metric: string, value: number, unit: string = 'ms', meta?: Record<string, any>): void {
136
+ this.logger.info(`Performance: ${metric} = ${value}${unit}`, {
137
+ metric,
138
+ value,
139
+ unit,
140
+ ...meta
141
+ });
142
+ }
143
+
144
+ public security(event: string, details: Record<string, any>): void {
145
+ this.logger.warn(`Security Event: ${event}`, {
146
+ security: true,
147
+ event,
148
+ ...details
149
+ });
150
+ }
151
+
152
+ public getSessionId(): string {
153
+ return this.sessionId;
154
+ }
155
+
156
+ public async flush(): Promise<void> {
157
+ return new Promise((resolve) => {
158
+ this.logger.on('finish', resolve);
159
+ this.logger.end();
160
+ });
161
+ }
162
+ }
163
+
164
+ // Export singleton instance
165
+ export const logger = new Logger();
166
+
167
+ // Helper functions for common logging patterns
168
+ export const logCommand = (command: string, startTime: number, success: boolean, meta?: Record<string, any>) => {
169
+ logger.command(command, startTime, success, meta);
170
+ };
171
+
172
+ export const logPerformance = (metric: string, value: number, unit: string = 'ms', meta?: Record<string, any>) => {
173
+ logger.performance(metric, value, unit, meta);
174
+ };
175
+
176
+ export const logSecurity = (event: string, details: Record<string, any>) => {
177
+ logger.security(event, details);
178
+ };
179
+
180
+ export const logError = (message: string, error?: Error, meta?: Record<string, any>) => {
181
+ logger.error(message, error, meta);
182
+ };
183
+
184
+ export const logInfo = (message: string, meta?: Record<string, any>) => {
185
+ logger.info(message, meta);
186
+ };
187
+
188
+ export const logWarn = (message: string, meta?: Record<string, any>) => {
189
+ logger.warn(message, meta);
190
+ };
191
+
192
+ export const logDebug = (message: string, meta?: Record<string, any>) => {
193
+ logger.debug(message, meta);
194
+ };