@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/dist/core/ai.d.ts CHANGED
@@ -1,5 +1,80 @@
1
- export declare function askOllama({ prompt, model, onToken }: {
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
+ */
13
+ import { Ollama } from 'ollama';
14
+ /** Typed errors: the runner maps these to user-facing hints, never by string matching. */
15
+ export type AIError = {
16
+ kind: 'connection';
17
+ cause: string;
18
+ } | {
19
+ kind: 'model-not-found';
20
+ model: string;
21
+ } | {
22
+ kind: 'empty-response';
23
+ model: string;
24
+ } | {
25
+ kind: 'request';
26
+ cause: string;
27
+ };
28
+ export interface AIRequest {
2
29
  prompt: string;
30
+ systemMessage?: string;
31
+ context?: string;
3
32
  model?: string;
4
33
  onToken?: (token: string) => void;
5
- }): Promise<string>;
34
+ }
35
+ /** The seam. Both adapters implement this; commands and tests depend on it, never on Ollama. */
36
+ export interface AIClient {
37
+ ask(request: AIRequest): Promise<string>;
38
+ listModels(): Promise<string[]>;
39
+ }
40
+ /**
41
+ * HTTP adapter: production. Standardizes on the Ollama client package —
42
+ * the raw-HTTP path existed only to work around a LangChain prompt-template
43
+ * issue, and that integration is gone.
44
+ */
45
+ export declare class OllamaAIClient implements AIClient {
46
+ private client;
47
+ constructor(client?: Ollama);
48
+ ask(request: AIRequest): Promise<string>;
49
+ listModels(): Promise<string[]>;
50
+ }
51
+ /**
52
+ * In-memory adapter: tests. Satisfies the same interface with no network,
53
+ * which is what makes AI behavior testable without Ollama installed.
54
+ */
55
+ export declare class InMemoryAIClient implements AIClient {
56
+ private responses;
57
+ private store;
58
+ /** Failures to simulate, keyed by prompt substring. */
59
+ failures: Map<string, AIError>;
60
+ /** Count of computations performed (not cache reads) — lets tests observe cache hits. */
61
+ computations: number;
62
+ /** Simulated clock for expiry tests. */
63
+ now: () => number;
64
+ /** TTL override for tests; defaults to the production expiry. */
65
+ ttlMs: number;
66
+ constructor(responses?: Map<string, string>);
67
+ ask(request: AIRequest): Promise<string>;
68
+ listModels(): Promise<string[]>;
69
+ }
70
+ export declare function getAIClient(): AIClient;
71
+ /** Test seam setter: swaps the adapter the module hands out. */
72
+ export declare function setAIClient(client: AIClient): void;
73
+ /**
74
+ * The interface commands call. Kept as module functions so callers don't
75
+ * reach for a client object; the client is resolved internally.
76
+ */
77
+ export declare function ask(request: AIRequest): Promise<string>;
78
+ export declare function listModels(): Promise<string[]>;
79
+ /** Default model, from configuration — one source of truth. */
80
+ export declare function defaultModel(): string;
package/dist/core/ai.js CHANGED
@@ -1,43 +1,220 @@
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';
5
- const CACHE_DIR = path.join(process.cwd(), '.dhruv-cache');
6
- if (!fs.existsSync(CACHE_DIR))
7
- fs.mkdirSync(CACHE_DIR);
8
- function getCacheKey(prompt, model) {
9
- const hash = crypto.createHash('sha256').update(`${model || 'default'}:${prompt}`).digest('hex');
10
- return path.join(CACHE_DIR, hash);
11
- }
12
- export async function askOllama({ prompt, model, onToken }) {
13
- const cacheKey = getCacheKey(prompt, model);
14
- if (fs.existsSync(cacheKey)) {
15
- const cached = fs.readFileSync(cacheKey, 'utf-8');
16
- if (onToken)
17
- onToken(cached);
17
+ import { loadConfig } from '../config/config.js';
18
+ const CACHE_EXPIRY_MS = 24 * 60 * 60 * 1000; // 24 hours
19
+ const MAX_CACHE_FILES = 100;
20
+ function cacheDir() {
21
+ return path.join(process.cwd(), '.dhruv-cache');
22
+ }
23
+ function cacheKey(request, model) {
24
+ const hash = crypto
25
+ .createHash('sha256')
26
+ .update(`${model}:${request.systemMessage ?? ''}:${request.context ?? ''}:${request.prompt}`)
27
+ .digest('hex');
28
+ return path.join(cacheDir(), hash);
29
+ }
30
+ function readCache(request, model) {
31
+ const file = cacheKey(request, model);
32
+ try {
33
+ const cached = fs.readFileSync(file, 'utf-8');
34
+ const stats = fs.statSync(file);
35
+ if (Date.now() - stats.mtimeMs > CACHE_EXPIRY_MS) {
36
+ fs.unlinkSync(file);
37
+ return undefined;
38
+ }
18
39
  return cached;
19
40
  }
41
+ catch {
42
+ return undefined;
43
+ }
44
+ }
45
+ function writeCache(request, model, response) {
46
+ try {
47
+ const dir = cacheDir();
48
+ if (!fs.existsSync(dir))
49
+ fs.mkdirSync(dir, { recursive: true });
50
+ fs.writeFileSync(cacheKey(request, model), response);
51
+ }
52
+ catch {
53
+ // Cache write failures never fail the request.
54
+ }
55
+ }
56
+ /** Wired up (the old cleanupCache was never called); runs opportunistically after a cache write. */
57
+ function cleanupCache() {
20
58
  try {
21
- let result = '';
22
- const ollama = new Ollama();
23
- // Await the iterator, then stream
24
- const iterator = await ollama.generate({ model: model || 'codellama', prompt, stream: true });
25
- for await (const chunk of iterator) {
26
- let token = '';
27
- if (typeof chunk === 'object' && chunk !== null && 'response' in chunk) {
28
- token = chunk.response;
59
+ const dir = cacheDir();
60
+ if (!fs.existsSync(dir))
61
+ return;
62
+ const entries = fs
63
+ .readdirSync(dir)
64
+ .map((name) => {
65
+ const file = path.join(dir, name);
66
+ return { file, mtimeMs: fs.statSync(file).mtimeMs };
67
+ })
68
+ .filter((entry) => {
69
+ if (Date.now() - entry.mtimeMs > CACHE_EXPIRY_MS) {
70
+ fs.unlinkSync(entry.file);
71
+ return false;
29
72
  }
30
- else if (typeof chunk === 'string') {
31
- token = chunk;
73
+ return true;
74
+ })
75
+ .sort((a, b) => a.mtimeMs - b.mtimeMs);
76
+ const excess = entries.length - MAX_CACHE_FILES;
77
+ if (excess > 0) {
78
+ for (const entry of entries.slice(0, excess))
79
+ fs.unlinkSync(entry.file);
80
+ }
81
+ }
82
+ catch {
83
+ // Ignore cleanup errors.
84
+ }
85
+ }
86
+ function toAIError(err, model) {
87
+ const message = err instanceof Error ? err.message : String(err);
88
+ if (message.includes('ECONNREFUSED') || message.includes('fetch failed') || message.includes('ENOTFOUND')) {
89
+ return { kind: 'connection', cause: message };
90
+ }
91
+ if (message.includes('not found')) {
92
+ return { kind: 'model-not-found', model };
93
+ }
94
+ return { kind: 'request', cause: message };
95
+ }
96
+ /**
97
+ * HTTP adapter: production. Standardizes on the Ollama client package —
98
+ * the raw-HTTP path existed only to work around a LangChain prompt-template
99
+ * issue, and that integration is gone.
100
+ */
101
+ export class OllamaAIClient {
102
+ constructor(client) {
103
+ this.client = client ?? new Ollama();
104
+ }
105
+ async ask(request) {
106
+ const model = request.model ?? loadConfig().model;
107
+ const fullPrompt = request.systemMessage
108
+ ? `System: ${request.systemMessage}\n\n${request.context ? `Context: ${request.context}\n\n` : ''}Query: ${request.prompt}`
109
+ : request.prompt;
110
+ const cached = readCache(request, model);
111
+ if (cached !== undefined) {
112
+ if (request.onToken)
113
+ request.onToken(cached);
114
+ return cached;
115
+ }
116
+ try {
117
+ const streaming = Boolean(request.onToken);
118
+ let result = '';
119
+ if (streaming) {
120
+ const stream = await this.client.generate({ model, prompt: fullPrompt, stream: true });
121
+ for await (const chunk of stream) {
122
+ const token = typeof chunk === 'object' && chunk !== null && 'response' in chunk ? chunk.response : '';
123
+ if (!token)
124
+ continue;
125
+ result += token;
126
+ if (request.onToken)
127
+ request.onToken(token);
128
+ }
129
+ }
130
+ else {
131
+ const response = await this.client.generate({ model, prompt: fullPrompt, stream: false });
132
+ result = response.response ?? '';
133
+ }
134
+ if (!result.trim()) {
135
+ throw new Error(`Model '${model}' not found or returned empty response`);
32
136
  }
33
- if (onToken)
34
- onToken(token);
35
- result += token;
137
+ writeCache(request, model, result.trim());
138
+ cleanupCache();
139
+ return result.trim();
140
+ }
141
+ catch (err) {
142
+ throw toAIError(err, model);
36
143
  }
37
- fs.writeFileSync(cacheKey, result.trim());
38
- return result.trim();
39
144
  }
40
- catch (err) {
41
- throw new Error('Ollama AI error: ' + err.message);
145
+ async listModels() {
146
+ try {
147
+ const models = await this.client.list();
148
+ return (models.models ?? []).map((m) => m.name);
149
+ }
150
+ catch (err) {
151
+ throw toAIError(err, 'unknown');
152
+ }
153
+ }
154
+ }
155
+ /**
156
+ * In-memory adapter: tests. Satisfies the same interface with no network,
157
+ * which is what makes AI behavior testable without Ollama installed.
158
+ */
159
+ export class InMemoryAIClient {
160
+ constructor(responses = new Map()) {
161
+ this.responses = responses;
162
+ this.store = new Map();
163
+ /** Failures to simulate, keyed by prompt substring. */
164
+ this.failures = new Map();
165
+ /** Count of computations performed (not cache reads) — lets tests observe cache hits. */
166
+ this.computations = 0;
167
+ /** Simulated clock for expiry tests. */
168
+ this.now = () => Date.now();
169
+ /** TTL override for tests; defaults to the production expiry. */
170
+ this.ttlMs = CACHE_EXPIRY_MS;
42
171
  }
172
+ async ask(request) {
173
+ const model = request.model ?? 'test-model';
174
+ for (const [substring, failure] of this.failures) {
175
+ if (request.prompt.includes(substring))
176
+ throw failure;
177
+ }
178
+ const key = `${model}:${request.systemMessage ?? ''}:${request.context ?? ''}:${request.prompt}`;
179
+ const hit = this.store.get(key);
180
+ if (hit && this.now() - hit.createdAt <= this.ttlMs) {
181
+ if (request.onToken)
182
+ request.onToken(hit.response);
183
+ return hit.response;
184
+ }
185
+ this.computations += 1;
186
+ const response = this.responses.get(request.prompt) ?? `response:${request.prompt}`;
187
+ this.store.set(key, { response, createdAt: this.now() });
188
+ if (request.onToken)
189
+ request.onToken(response);
190
+ return response;
191
+ }
192
+ async listModels() {
193
+ return ['test-model', 'other-model'];
194
+ }
195
+ }
196
+ /** Default client: the HTTP adapter. Tests inject InMemoryAIClient instead. */
197
+ let defaultClient;
198
+ export function getAIClient() {
199
+ if (!defaultClient)
200
+ defaultClient = new OllamaAIClient();
201
+ return defaultClient;
202
+ }
203
+ /** Test seam setter: swaps the adapter the module hands out. */
204
+ export function setAIClient(client) {
205
+ defaultClient = client;
206
+ }
207
+ /**
208
+ * The interface commands call. Kept as module functions so callers don't
209
+ * reach for a client object; the client is resolved internally.
210
+ */
211
+ export async function ask(request) {
212
+ return getAIClient().ask(request);
213
+ }
214
+ export async function listModels() {
215
+ return getAIClient().listModels();
216
+ }
217
+ /** Default model, from configuration — one source of truth. */
218
+ export function defaultModel() {
219
+ return loadConfig().model;
43
220
  }
@@ -0,0 +1,17 @@
1
+ import { AIRequest } from '../core/ai.js';
2
+ /** What makes a command distinct. The runner owns everything else. */
3
+ export interface CommandSpec {
4
+ /** Commander command name, for validation, logging, and metrics. */
5
+ name: string;
6
+ /** The user-facing arguments, validated as a unit. */
7
+ input: Record<string, string>;
8
+ /** Builds the AI request from validated input. */
9
+ buildRequest: (input: Record<string, string>, model: string) => AIRequest;
10
+ /** Header line under the banner, e.g. "📚 Explanation:". */
11
+ header: string;
12
+ /** Footer hint shown after success. */
13
+ footer?: string;
14
+ /** Post-processing on the full response (e.g. saving generated test files). */
15
+ onComplete?: (response: string, input: Record<string, string>) => void;
16
+ }
17
+ export declare function runCommand(spec: CommandSpec): Promise<void>;
@@ -0,0 +1,78 @@
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 } 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
+ /** Maps typed AI errors to user-facing hints — once, not per command. */
17
+ function describeAIError(error, model) {
18
+ switch (error.kind) {
19
+ case 'connection':
20
+ return `💡 Make sure Ollama is running: ollama serve`;
21
+ case 'model-not-found':
22
+ return `💡 Install the model: ollama pull ${error.model || model}`;
23
+ case 'empty-response':
24
+ return `💡 Model returned nothing. Install it: ollama pull ${error.model || model}`;
25
+ default:
26
+ return error.cause;
27
+ }
28
+ }
29
+ export async function runCommand(spec) {
30
+ const startTime = Date.now();
31
+ const { name, input } = spec;
32
+ const config = loadConfig();
33
+ const fail = (error) => {
34
+ printError(error);
35
+ logCommand(name, startTime, false, { error });
36
+ metricsCollector.recordCommand(name, Date.now() - startTime, false);
37
+ };
38
+ // Validation and rate limiting — every command, uniformly.
39
+ const securityCheck = securityManager.validateInput(name, input);
40
+ if (!securityCheck.valid) {
41
+ fail(securityCheck.error);
42
+ return;
43
+ }
44
+ const rateLimitCheck = securityManager.checkRateLimit('user');
45
+ if (!rateLimitCheck.allowed) {
46
+ fail('Rate limit exceeded. Please try again later.');
47
+ return;
48
+ }
49
+ const spinner = ora('Thinking...').start();
50
+ try {
51
+ spinner.stop();
52
+ console.log(chalk.yellowBright('🤖 Dhruv CLI: AI-powered developer assistant'));
53
+ console.log(chalk.green.bold(spec.header));
54
+ console.log();
55
+ const response = await ask(spec.buildRequest(input, config.model));
56
+ console.log('\n');
57
+ if (spec.footer) {
58
+ console.log(chalk.dim(spec.footer));
59
+ }
60
+ if (spec.onComplete)
61
+ spec.onComplete(response, input);
62
+ const duration = Date.now() - startTime;
63
+ logCommand(name, startTime, true, { model: config.model });
64
+ logPerformance(name, duration);
65
+ metricsCollector.recordCommand(name, duration, true);
66
+ logger.info(`${name} command completed successfully`, { duration });
67
+ }
68
+ catch (err) {
69
+ spinner.stop();
70
+ const duration = Date.now() - startTime;
71
+ printError(`Command failed.`);
72
+ console.log(chalk.yellow(describeAIError(err, config.model)));
73
+ logError(`${name} command failed`, err, { command: name });
74
+ logCommand(name, startTime, false, { error: err.message });
75
+ metricsCollector.recordCommand(name, duration, false);
76
+ metricsCollector.recordError('ai_request_failed', name);
77
+ }
78
+ }
@@ -0,0 +1,40 @@
1
+ export interface LogEntry {
2
+ timestamp: string;
3
+ level: 'error' | 'warn' | 'info' | 'debug';
4
+ message: string;
5
+ command?: string;
6
+ userId?: string;
7
+ sessionId?: string;
8
+ duration?: number;
9
+ metadata?: Record<string, any>;
10
+ error?: {
11
+ name: string;
12
+ message: string;
13
+ stack?: string;
14
+ };
15
+ }
16
+ declare class Logger {
17
+ private logger;
18
+ private sessionId;
19
+ constructor();
20
+ private generateSessionId;
21
+ private createLogger;
22
+ info(message: string, meta?: Record<string, any>): void;
23
+ warn(message: string, meta?: Record<string, any>): void;
24
+ error(message: string, error?: Error, meta?: Record<string, any>): void;
25
+ debug(message: string, meta?: Record<string, any>): void;
26
+ command(command: string, startTime: number, success: boolean, meta?: Record<string, any>): void;
27
+ performance(metric: string, value: number, unit?: string, meta?: Record<string, any>): void;
28
+ security(event: string, details: Record<string, any>): void;
29
+ getSessionId(): string;
30
+ flush(): Promise<void>;
31
+ }
32
+ export declare const logger: Logger;
33
+ export declare const logCommand: (command: string, startTime: number, success: boolean, meta?: Record<string, any>) => void;
34
+ export declare const logPerformance: (metric: string, value: number, unit?: string, meta?: Record<string, any>) => void;
35
+ export declare const logSecurity: (event: string, details: Record<string, any>) => void;
36
+ export declare const logError: (message: string, error?: Error, meta?: Record<string, any>) => void;
37
+ export declare const logInfo: (message: string, meta?: Record<string, any>) => void;
38
+ export declare const logWarn: (message: string, meta?: Record<string, any>) => void;
39
+ export declare const logDebug: (message: string, meta?: Record<string, any>) => void;
40
+ export {};
@@ -0,0 +1,138 @@
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
+ class Logger {
7
+ constructor() {
8
+ this.sessionId = this.generateSessionId();
9
+ this.logger = this.createLogger();
10
+ }
11
+ generateSessionId() {
12
+ return `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
13
+ }
14
+ createLogger() {
15
+ const config = loadConfig();
16
+ const logDir = path.join(process.cwd(), 'logs');
17
+ // Ensure log directory exists
18
+ if (!fs.existsSync(logDir)) {
19
+ fs.mkdirSync(logDir, { recursive: true });
20
+ }
21
+ const logFormat = winston.format.combine(winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), winston.format.errors({ stack: true }), winston.format.json(), winston.format.printf(({ timestamp, level, message, ...meta }) => {
22
+ return JSON.stringify({
23
+ timestamp,
24
+ level,
25
+ message,
26
+ sessionId: this.sessionId,
27
+ ...meta
28
+ });
29
+ }));
30
+ const transports = [
31
+ // Console transport for development
32
+ new winston.transports.Console({
33
+ level: config.verbose ? 'debug' : 'info',
34
+ format: winston.format.combine(winston.format.colorize(), winston.format.simple(), winston.format.printf(({ timestamp, level, message }) => {
35
+ return `${timestamp} ${level}: ${message}`;
36
+ }))
37
+ }),
38
+ // Daily rotating file for all logs
39
+ new DailyRotateFile({
40
+ filename: path.join(logDir, 'dhruv-%DATE%.log'),
41
+ datePattern: 'YYYY-MM-DD',
42
+ maxSize: '20m',
43
+ maxFiles: '14d',
44
+ level: 'debug'
45
+ }),
46
+ // Separate error log
47
+ new DailyRotateFile({
48
+ filename: path.join(logDir, 'dhruv-error-%DATE%.log'),
49
+ datePattern: 'YYYY-MM-DD',
50
+ maxSize: '20m',
51
+ maxFiles: '30d',
52
+ level: 'error'
53
+ })
54
+ ];
55
+ return winston.createLogger({
56
+ level: 'debug',
57
+ format: logFormat,
58
+ defaultMeta: { sessionId: this.sessionId },
59
+ transports
60
+ });
61
+ }
62
+ info(message, meta) {
63
+ this.logger.info(message, meta);
64
+ }
65
+ warn(message, meta) {
66
+ this.logger.warn(message, meta);
67
+ }
68
+ error(message, error, meta) {
69
+ const errorInfo = error ? {
70
+ name: error.name,
71
+ message: error.message,
72
+ stack: error.stack
73
+ } : undefined;
74
+ this.logger.error(message, { error: errorInfo, ...meta });
75
+ }
76
+ debug(message, meta) {
77
+ this.logger.debug(message, meta);
78
+ }
79
+ command(command, startTime, success, meta) {
80
+ const duration = Date.now() - startTime;
81
+ const level = success ? 'info' : 'error';
82
+ const message = `Command ${command} ${success ? 'completed' : 'failed'} in ${duration}ms`;
83
+ this.logger.log(level, message, {
84
+ command,
85
+ duration,
86
+ success,
87
+ ...meta
88
+ });
89
+ }
90
+ performance(metric, value, unit = 'ms', meta) {
91
+ this.logger.info(`Performance: ${metric} = ${value}${unit}`, {
92
+ metric,
93
+ value,
94
+ unit,
95
+ ...meta
96
+ });
97
+ }
98
+ security(event, details) {
99
+ this.logger.warn(`Security Event: ${event}`, {
100
+ security: true,
101
+ event,
102
+ ...details
103
+ });
104
+ }
105
+ getSessionId() {
106
+ return this.sessionId;
107
+ }
108
+ async flush() {
109
+ return new Promise((resolve) => {
110
+ this.logger.on('finish', resolve);
111
+ this.logger.end();
112
+ });
113
+ }
114
+ }
115
+ // Export singleton instance
116
+ export const logger = new Logger();
117
+ // Helper functions for common logging patterns
118
+ export const logCommand = (command, startTime, success, meta) => {
119
+ logger.command(command, startTime, success, meta);
120
+ };
121
+ export const logPerformance = (metric, value, unit = 'ms', meta) => {
122
+ logger.performance(metric, value, unit, meta);
123
+ };
124
+ export const logSecurity = (event, details) => {
125
+ logger.security(event, details);
126
+ };
127
+ export const logError = (message, error, meta) => {
128
+ logger.error(message, error, meta);
129
+ };
130
+ export const logInfo = (message, meta) => {
131
+ logger.info(message, meta);
132
+ };
133
+ export const logWarn = (message, meta) => {
134
+ logger.warn(message, meta);
135
+ };
136
+ export const logDebug = (message, meta) => {
137
+ logger.debug(message, meta);
138
+ };
@@ -0,0 +1,34 @@
1
+ import promClient from 'prom-client';
2
+ export declare const metrics: {
3
+ commandDuration: promClient.Histogram<"success" | "command">;
4
+ commandCount: promClient.Counter<"success" | "command">;
5
+ aiRequestDuration: promClient.Histogram<"model" | "command_type">;
6
+ aiRequestCount: promClient.Counter<"model" | "success" | "command_type">;
7
+ aiTokensUsed: promClient.Counter<"model" | "command_type">;
8
+ cacheHitCount: promClient.Counter<"cache_type">;
9
+ cacheMissCount: promClient.Counter<"cache_type">;
10
+ cacheSize: promClient.Gauge<"cache_type">;
11
+ errorCount: promClient.Counter<"command" | "error_type">;
12
+ memoryUsage: promClient.Gauge<"type">;
13
+ sessionCount: promClient.Counter<string>;
14
+ pluginLoadedCount: promClient.Counter<string>;
15
+ };
16
+ export declare class MetricsCollector {
17
+ private static instance;
18
+ private metricsEnabled;
19
+ private constructor();
20
+ static getInstance(): MetricsCollector;
21
+ recordCommand(command: string, duration: number, success: boolean): void;
22
+ recordAIRequest(model: string, commandType: string, duration: number, success: boolean, tokensUsed?: number): void;
23
+ recordCacheHit(cacheType: string): void;
24
+ recordCacheMiss(cacheType: string): void;
25
+ updateCacheSize(cacheType: string, size: number): void;
26
+ recordError(errorType: string, command?: string): void;
27
+ updateMemoryUsage(): void;
28
+ recordSession(): void;
29
+ recordPluginLoaded(): void;
30
+ getMetrics(): Promise<string>;
31
+ getMetricsJSON(): Promise<promClient.MetricObjectWithValues<promClient.MetricValue<string>>[]>;
32
+ getRegistry(): promClient.Registry;
33
+ }
34
+ export declare const metricsCollector: MetricsCollector;