@rahul05ranjan/dhruv-cli 1.3.0 → 1.5.0

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 (121) 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/ci.yml +51 -38
  9. package/.github/workflows/contribution.yml +41 -34
  10. package/.github/workflows/dependabot-auto-merge.yml +62 -2
  11. package/.github/workflows/labeler.yml +1 -0
  12. package/.github/workflows/release.yml +229 -0
  13. package/.github/workflows/security.yml +201 -0
  14. package/.releaserc.json +50 -0
  15. package/AGENTS.md +13 -0
  16. package/CHANGELOG.md +13 -0
  17. package/README.md +145 -40
  18. package/__tests__/cli-contract.test.ts +104 -0
  19. package/__tests__/core.test.ts +439 -0
  20. package/__tests__/diagnostics.test.ts +155 -0
  21. package/__tests__/file-workflows.test.ts +195 -0
  22. package/__tests__/interactive.test.ts +119 -0
  23. package/__tests__/setup.ts +62 -0
  24. package/__tests__/workflows.test.ts +118 -0
  25. package/dist/commands/explain.js +13 -44
  26. package/dist/commands/fix.js +13 -38
  27. package/dist/commands/generate.d.ts +6 -1
  28. package/dist/commands/generate.js +60 -55
  29. package/dist/commands/health.d.ts +4 -0
  30. package/dist/commands/health.js +419 -0
  31. package/dist/commands/init.js +56 -42
  32. package/dist/commands/menu.js +137 -24
  33. package/dist/commands/metrics.d.ts +5 -0
  34. package/dist/commands/metrics.js +80 -0
  35. package/dist/commands/optimize.js +42 -34
  36. package/dist/commands/review.d.ts +4 -1
  37. package/dist/commands/review.js +87 -43
  38. package/dist/commands/security-check.d.ts +4 -1
  39. package/dist/commands/security-check.js +109 -38
  40. package/dist/commands/status.d.ts +1 -0
  41. package/dist/commands/status.js +83 -0
  42. package/dist/commands/suggest.js +13 -39
  43. package/dist/config/config.d.ts +5 -1
  44. package/dist/config/config.js +53 -8
  45. package/dist/core/ai.d.ts +88 -2
  46. package/dist/core/ai.js +231 -30
  47. package/dist/core/command-catalog.d.ts +10 -0
  48. package/dist/core/command-catalog.js +27 -0
  49. package/dist/core/command-runner.d.ts +17 -0
  50. package/dist/core/command-runner.js +157 -0
  51. package/dist/core/logger.d.ts +40 -0
  52. package/dist/core/logger.js +139 -0
  53. package/dist/core/metrics.d.ts +62 -0
  54. package/dist/core/metrics.js +284 -0
  55. package/dist/core/prompts.d.ts +1 -0
  56. package/dist/core/prompts.js +121 -0
  57. package/dist/core/security.d.ts +34 -0
  58. package/dist/core/security.js +197 -0
  59. package/dist/index.js +84 -22
  60. package/dist/utils/projectType.d.ts +1 -1
  61. package/dist/utils/projectType.js +26 -7
  62. package/dist/utils/ux.d.ts +3 -0
  63. package/dist/utils/ux.js +15 -0
  64. package/docs/agents/domain.md +51 -0
  65. package/docs/agents/issue-tracker.md +45 -0
  66. package/docs/agents/triage-labels.md +15 -0
  67. package/docs/api/.nojekyll +1 -0
  68. package/docs/api/assets/hierarchy.js +1 -0
  69. package/docs/api/assets/highlight.css +71 -0
  70. package/docs/api/assets/icons.js +18 -0
  71. package/docs/api/assets/icons.svg +1 -0
  72. package/docs/api/assets/main.js +60 -0
  73. package/docs/api/assets/navigation.js +1 -0
  74. package/docs/api/assets/search.js +1 -0
  75. package/docs/api/assets/style.css +1633 -0
  76. package/docs/api/hierarchy.html +1 -0
  77. package/docs/api/index.html +161 -0
  78. package/docs/api/media/CONTRIBUTING.md +60 -0
  79. package/docs/api/media/SECURITY.md +8 -0
  80. package/docs/api/media/dhruv-cli-preview.svg +42 -0
  81. package/docs/api/media/publishing-fix.md +34 -0
  82. package/docs/api/modules.html +1 -0
  83. package/docs/dhruv-cli-preview.svg +42 -0
  84. package/docs/index.html +631 -533
  85. package/docs/publishing-fix.md +34 -0
  86. package/eslint.config.js +170 -0
  87. package/jest.config.json +37 -0
  88. package/lighthouserc.json +22 -0
  89. package/package.json +62 -8
  90. package/src/commands/explain.ts +13 -42
  91. package/src/commands/fix.ts +13 -31
  92. package/src/commands/generate.ts +68 -48
  93. package/src/commands/health.ts +485 -0
  94. package/src/commands/init.ts +57 -42
  95. package/src/commands/menu.ts +132 -24
  96. package/src/commands/metrics.ts +100 -0
  97. package/src/commands/optimize.ts +40 -28
  98. package/src/commands/review.ts +96 -37
  99. package/src/commands/security-check.ts +127 -33
  100. package/src/commands/status.ts +83 -0
  101. package/src/commands/suggest.ts +13 -32
  102. package/src/config/config.ts +60 -8
  103. package/src/core/ai.ts +265 -26
  104. package/src/core/command-catalog.ts +37 -0
  105. package/src/core/command-runner.ts +185 -0
  106. package/src/core/logger.ts +195 -0
  107. package/src/core/metrics.ts +335 -0
  108. package/src/core/prompts.ts +128 -0
  109. package/src/core/security.ts +243 -0
  110. package/src/index.ts +90 -22
  111. package/src/utils/projectType.ts +22 -7
  112. package/src/utils/ux.ts +18 -0
  113. package/test-suite.sh +147 -0
  114. package/tsconfig.json +4 -3
  115. package/typedoc.json +44 -0
  116. package/types/global.d.ts +13 -0
  117. package/validate-workflows.sh +270 -0
  118. package/.eslintignore +0 -1
  119. package/.eslintrc.cjs +0 -43
  120. package/.github/workflows/auto-assign.yml +0 -14
  121. package/src/core/ai.test.js +0 -40
@@ -0,0 +1,157 @@
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
+ if (!error || typeof error !== 'object' || !('kind' in error)) {
19
+ return error instanceof Error ? error.message : String(error);
20
+ }
21
+ const typedError = error;
22
+ switch (typedError.kind) {
23
+ case 'connection':
24
+ return `💡 Make sure Ollama is running: ollama serve`;
25
+ case 'model-not-found':
26
+ return `💡 Install the model: ollama pull ${typedError.model || model}`;
27
+ case 'empty-response':
28
+ return `💡 Model returned nothing. Install it: ollama pull ${typedError.model || model}`;
29
+ case 'timeout':
30
+ return `💡 The request timed out after ${typedError.timeoutMs}ms. Try again, use a smaller prompt, or increase --timeout.`;
31
+ case 'cancelled':
32
+ return '💡 Request cancelled. Run the command again when ready.';
33
+ default:
34
+ return typedError.cause;
35
+ }
36
+ }
37
+ export async function runCommand(spec) {
38
+ const startTime = Date.now();
39
+ const { name, input } = spec;
40
+ const config = loadConfig();
41
+ const jsonOutput = config.responseFormat === 'json';
42
+ const writeJson = (result) => {
43
+ process.stdout.write(`${JSON.stringify(result)}\n`);
44
+ };
45
+ const fail = (error) => {
46
+ process.exitCode = 2;
47
+ if (jsonOutput)
48
+ writeJson({ ok: false, command: name, error, model: config.model, durationMs: Date.now() - startTime });
49
+ else
50
+ printError(error);
51
+ logCommand(name, startTime, false, { error });
52
+ metricsCollector.recordCommand(name, Date.now() - startTime, false);
53
+ };
54
+ // Validation and rate limiting — every command, uniformly.
55
+ const securityCheck = securityManager.validateInput(name, input);
56
+ if (!securityCheck.valid) {
57
+ fail(securityCheck.error);
58
+ return;
59
+ }
60
+ const rateLimitCheck = securityManager.checkRateLimit('user');
61
+ if (!rateLimitCheck.allowed) {
62
+ fail('Rate limit exceeded. Please try again later.');
63
+ return;
64
+ }
65
+ const spinner = jsonOutput ? undefined : ora('Thinking...').start();
66
+ let requestTimeout;
67
+ let sigintHandler;
68
+ try {
69
+ spinner?.stop();
70
+ if (!jsonOutput) {
71
+ console.log(chalk.yellowBright('🤖 Dhruv CLI: AI-powered developer assistant'));
72
+ console.log(chalk.green.bold(spec.header));
73
+ console.log();
74
+ }
75
+ let streamed = false;
76
+ const controller = new AbortController();
77
+ const request = {
78
+ ...spec.buildRequest(input, config.model),
79
+ signal: controller.signal,
80
+ onToken: (token) => {
81
+ streamed = true;
82
+ if (!jsonOutput)
83
+ process.stdout.write(token);
84
+ },
85
+ };
86
+ const aiStartTime = Date.now();
87
+ const responsePromise = ask(request);
88
+ const cancellationPromise = new Promise((_, reject) => {
89
+ sigintHandler = () => {
90
+ controller.abort();
91
+ reject({ kind: 'cancelled' });
92
+ };
93
+ process.once('SIGINT', sigintHandler);
94
+ });
95
+ const response = config.timeoutMs > 0
96
+ ? await Promise.race([
97
+ responsePromise,
98
+ cancellationPromise,
99
+ new Promise((_, reject) => {
100
+ requestTimeout = setTimeout(() => {
101
+ controller.abort();
102
+ reject({ kind: 'timeout', timeoutMs: config.timeoutMs });
103
+ }, config.timeoutMs);
104
+ }),
105
+ ])
106
+ : await Promise.race([responsePromise, cancellationPromise]);
107
+ if (requestTimeout)
108
+ clearTimeout(requestTimeout);
109
+ if (sigintHandler)
110
+ process.removeListener('SIGINT', sigintHandler);
111
+ if (!response.trim()) {
112
+ throw { kind: 'empty-response', model: config.model };
113
+ }
114
+ const durationMs = Date.now() - startTime;
115
+ if (jsonOutput) {
116
+ writeJson({ ok: true, command: name, model: config.model, response, durationMs });
117
+ }
118
+ else {
119
+ if (!streamed)
120
+ process.stdout.write(response);
121
+ process.stdout.write('\n');
122
+ console.log('\n');
123
+ if (spec.footer)
124
+ console.log(chalk.dim(spec.footer));
125
+ }
126
+ if (spec.onComplete)
127
+ spec.onComplete(response, input);
128
+ metricsCollector.recordAIRequest(config.model, name, Date.now() - aiStartTime, true);
129
+ logCommand(name, startTime, true, { model: config.model });
130
+ logPerformance(name, durationMs);
131
+ metricsCollector.recordCommand(name, durationMs, true);
132
+ logger.info(`${name} command completed successfully`, { duration: durationMs });
133
+ }
134
+ catch (err) {
135
+ spinner?.stop();
136
+ if (requestTimeout)
137
+ clearTimeout(requestTimeout);
138
+ if (sigintHandler)
139
+ process.removeListener('SIGINT', sigintHandler);
140
+ const cancelled = typeof err === 'object' && err !== null && 'kind' in err && err.kind === 'cancelled';
141
+ process.exitCode = cancelled ? 130 : 1;
142
+ const duration = Date.now() - startTime;
143
+ const hint = describeAIError(err, config.model);
144
+ metricsCollector.recordAIRequest(config.model, name, duration, false);
145
+ if (jsonOutput) {
146
+ writeJson({ ok: false, command: name, model: config.model, error: 'Command failed.', hint, durationMs: duration });
147
+ }
148
+ else {
149
+ printError(`Command failed.`);
150
+ console.log(chalk.yellow(hint));
151
+ }
152
+ logError(`${name} command failed`, err, { command: name });
153
+ logCommand(name, startTime, false, { error: err.message });
154
+ metricsCollector.recordCommand(name, duration, false);
155
+ metricsCollector.recordError('ai_request_failed', name);
156
+ }
157
+ }
@@ -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,139 @@
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
+ stderrLevels: ['error', 'warn', 'info', 'debug'],
35
+ format: winston.format.combine(winston.format.colorize(), winston.format.simple(), winston.format.printf(({ timestamp, level, message }) => {
36
+ return `${timestamp} ${level}: ${message}`;
37
+ }))
38
+ }),
39
+ // Daily rotating file for all logs
40
+ new DailyRotateFile({
41
+ filename: path.join(logDir, 'dhruv-%DATE%.log'),
42
+ datePattern: 'YYYY-MM-DD',
43
+ maxSize: '20m',
44
+ maxFiles: '14d',
45
+ level: 'debug'
46
+ }),
47
+ // Separate error log
48
+ new DailyRotateFile({
49
+ filename: path.join(logDir, 'dhruv-error-%DATE%.log'),
50
+ datePattern: 'YYYY-MM-DD',
51
+ maxSize: '20m',
52
+ maxFiles: '30d',
53
+ level: 'error'
54
+ })
55
+ ];
56
+ return winston.createLogger({
57
+ level: 'debug',
58
+ format: logFormat,
59
+ defaultMeta: { sessionId: this.sessionId },
60
+ transports
61
+ });
62
+ }
63
+ info(message, meta) {
64
+ this.logger.info(message, meta);
65
+ }
66
+ warn(message, meta) {
67
+ this.logger.warn(message, meta);
68
+ }
69
+ error(message, error, meta) {
70
+ const errorInfo = error ? {
71
+ name: error.name,
72
+ message: error.message,
73
+ stack: error.stack
74
+ } : undefined;
75
+ this.logger.error(message, { error: errorInfo, ...meta });
76
+ }
77
+ debug(message, meta) {
78
+ this.logger.debug(message, meta);
79
+ }
80
+ command(command, startTime, success, meta) {
81
+ const duration = Date.now() - startTime;
82
+ const level = success ? 'info' : 'error';
83
+ const message = `Command ${command} ${success ? 'completed' : 'failed'} in ${duration}ms`;
84
+ this.logger.log(level, message, {
85
+ command,
86
+ duration,
87
+ success,
88
+ ...meta
89
+ });
90
+ }
91
+ performance(metric, value, unit = 'ms', meta) {
92
+ this.logger.info(`Performance: ${metric} = ${value}${unit}`, {
93
+ metric,
94
+ value,
95
+ unit,
96
+ ...meta
97
+ });
98
+ }
99
+ security(event, details) {
100
+ this.logger.warn(`Security Event: ${event}`, {
101
+ security: true,
102
+ event,
103
+ ...details
104
+ });
105
+ }
106
+ getSessionId() {
107
+ return this.sessionId;
108
+ }
109
+ async flush() {
110
+ return new Promise((resolve) => {
111
+ this.logger.on('finish', resolve);
112
+ this.logger.end();
113
+ });
114
+ }
115
+ }
116
+ // Export singleton instance
117
+ export const logger = new Logger();
118
+ // Helper functions for common logging patterns
119
+ export const logCommand = (command, startTime, success, meta) => {
120
+ logger.command(command, startTime, success, meta);
121
+ };
122
+ export const logPerformance = (metric, value, unit = 'ms', meta) => {
123
+ logger.performance(metric, value, unit, meta);
124
+ };
125
+ export const logSecurity = (event, details) => {
126
+ logger.security(event, details);
127
+ };
128
+ export const logError = (message, error, meta) => {
129
+ logger.error(message, error, meta);
130
+ };
131
+ export const logInfo = (message, meta) => {
132
+ logger.info(message, meta);
133
+ };
134
+ export const logWarn = (message, meta) => {
135
+ logger.warn(message, meta);
136
+ };
137
+ export const logDebug = (message, meta) => {
138
+ logger.debug(message, meta);
139
+ };
@@ -0,0 +1,62 @@
1
+ import promClient from 'prom-client';
2
+ export interface CommandSummary {
3
+ runs: number;
4
+ successes: number;
5
+ failures: number;
6
+ durationMs: number;
7
+ }
8
+ export interface ModelSummary {
9
+ requests: number;
10
+ successes: number;
11
+ failures: number;
12
+ durationMs: number;
13
+ }
14
+ export interface CacheSummary {
15
+ hits: number;
16
+ misses: number;
17
+ }
18
+ export interface MetricsSummary {
19
+ sessions: number;
20
+ commands: Record<string, CommandSummary>;
21
+ errors: Record<string, number>;
22
+ models: Record<string, ModelSummary>;
23
+ cache: CacheSummary;
24
+ }
25
+ export declare const metrics: {
26
+ commandDuration: promClient.Histogram<"success" | "command">;
27
+ commandCount: promClient.Counter<"success" | "command">;
28
+ aiRequestDuration: promClient.Histogram<"model" | "command_type">;
29
+ aiRequestCount: promClient.Counter<"model" | "success" | "command_type">;
30
+ aiTokensUsed: promClient.Counter<"model" | "command_type">;
31
+ cacheHitCount: promClient.Counter<"cache_type">;
32
+ cacheMissCount: promClient.Counter<"cache_type">;
33
+ cacheSize: promClient.Gauge<"cache_type">;
34
+ errorCount: promClient.Counter<"command" | "error_type">;
35
+ memoryUsage: promClient.Gauge<"type">;
36
+ sessionCount: promClient.Counter<string>;
37
+ pluginLoadedCount: promClient.Counter<string>;
38
+ };
39
+ export declare class MetricsCollector {
40
+ private static instance;
41
+ private metricsEnabled;
42
+ private constructor();
43
+ static getInstance(): MetricsCollector;
44
+ recordCommand(command: string, duration: number, success: boolean): void;
45
+ recordAIRequest(model: string, commandType: string, duration: number, success: boolean, tokensUsed?: number): void;
46
+ recordCacheHit(cacheType: string): void;
47
+ recordCacheMiss(cacheType: string): void;
48
+ updateCacheSize(cacheType: string, size: number): void;
49
+ recordError(errorType: string, command?: string): void;
50
+ updateMemoryUsage(): void;
51
+ recordSession(): void;
52
+ recordPluginLoaded(): void;
53
+ getSummary(): MetricsSummary;
54
+ resetPersistent(): void;
55
+ private persistentPath;
56
+ private readPersistent;
57
+ private updatePersistent;
58
+ getMetrics(): Promise<string>;
59
+ getMetricsJSON(): Promise<promClient.MetricObjectWithValues<promClient.MetricValue<string>>[]>;
60
+ getRegistry(): promClient.Registry;
61
+ }
62
+ export declare const metricsCollector: MetricsCollector;
@@ -0,0 +1,284 @@
1
+ import promClient from 'prom-client';
2
+ import { logger } from './logger.js';
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ function emptySummary() {
6
+ return { sessions: 0, commands: {}, errors: {}, models: {}, cache: { hits: 0, misses: 0 } };
7
+ }
8
+ // Create a Registry which registers the metrics
9
+ const register = new promClient.Registry();
10
+ // Add a default label which is added to all metrics
11
+ register.setDefaultLabels({
12
+ app: 'dhruv-cli'
13
+ });
14
+ // Enable the collection of default metrics
15
+ promClient.collectDefaultMetrics({ register });
16
+ // Custom metrics
17
+ export const metrics = {
18
+ // Command execution metrics
19
+ commandDuration: new promClient.Histogram({
20
+ name: 'dhruv_command_duration_seconds',
21
+ help: 'Duration of command execution in seconds',
22
+ labelNames: ['command', 'success'],
23
+ buckets: [0.1, 0.5, 1, 2, 5, 10, 30]
24
+ }),
25
+ commandCount: new promClient.Counter({
26
+ name: 'dhruv_command_total',
27
+ help: 'Total number of commands executed',
28
+ labelNames: ['command', 'success']
29
+ }),
30
+ // AI service metrics
31
+ aiRequestDuration: new promClient.Histogram({
32
+ name: 'dhruv_ai_request_duration_seconds',
33
+ help: 'Duration of AI requests in seconds',
34
+ labelNames: ['model', 'command_type'],
35
+ buckets: [1, 5, 10, 30, 60, 120]
36
+ }),
37
+ aiRequestCount: new promClient.Counter({
38
+ name: 'dhruv_ai_request_total',
39
+ help: 'Total number of AI requests',
40
+ labelNames: ['model', 'command_type', 'success']
41
+ }),
42
+ aiTokensUsed: new promClient.Counter({
43
+ name: 'dhruv_ai_tokens_total',
44
+ help: 'Total number of tokens used in AI requests',
45
+ labelNames: ['model', 'command_type']
46
+ }),
47
+ // Cache metrics
48
+ cacheHitCount: new promClient.Counter({
49
+ name: 'dhruv_cache_hit_total',
50
+ help: 'Total number of cache hits',
51
+ labelNames: ['cache_type']
52
+ }),
53
+ cacheMissCount: new promClient.Counter({
54
+ name: 'dhruv_cache_miss_total',
55
+ help: 'Total number of cache misses',
56
+ labelNames: ['cache_type']
57
+ }),
58
+ cacheSize: new promClient.Gauge({
59
+ name: 'dhruv_cache_size_bytes',
60
+ help: 'Current size of cache in bytes',
61
+ labelNames: ['cache_type']
62
+ }),
63
+ // Error metrics
64
+ errorCount: new promClient.Counter({
65
+ name: 'dhruv_error_total',
66
+ help: 'Total number of errors',
67
+ labelNames: ['error_type', 'command']
68
+ }),
69
+ // Performance metrics
70
+ memoryUsage: new promClient.Gauge({
71
+ name: 'dhruv_memory_usage_bytes',
72
+ help: 'Current memory usage in bytes',
73
+ labelNames: ['type']
74
+ }),
75
+ // User engagement metrics
76
+ sessionCount: new promClient.Counter({
77
+ name: 'dhruv_session_total',
78
+ help: 'Total number of user sessions'
79
+ }),
80
+ pluginLoadedCount: new promClient.Counter({
81
+ name: 'dhruv_plugin_loaded_total',
82
+ help: 'Total number of plugins loaded'
83
+ })
84
+ };
85
+ // Register all metrics
86
+ Object.values(metrics).forEach(metric => {
87
+ register.registerMetric(metric);
88
+ });
89
+ export class MetricsCollector {
90
+ constructor() {
91
+ this.metricsEnabled = process.env.DHRUV_METRICS_ENABLED !== 'false';
92
+ if (this.metricsEnabled) {
93
+ logger.info('Metrics collection enabled');
94
+ }
95
+ }
96
+ static getInstance() {
97
+ if (!MetricsCollector.instance) {
98
+ MetricsCollector.instance = new MetricsCollector();
99
+ }
100
+ return MetricsCollector.instance;
101
+ }
102
+ recordCommand(command, duration, success) {
103
+ if (!this.metricsEnabled)
104
+ return;
105
+ try {
106
+ metrics.commandDuration.observe({ command, success: success.toString() }, duration / 1000);
107
+ metrics.commandCount.inc({ command, success: success.toString() });
108
+ this.updatePersistent((summary) => {
109
+ const current = summary.commands[command] ?? { runs: 0, successes: 0, failures: 0, durationMs: 0 };
110
+ current.runs += 1;
111
+ if (success)
112
+ current.successes += 1;
113
+ else
114
+ current.failures += 1;
115
+ current.durationMs += duration;
116
+ summary.commands[command] = current;
117
+ });
118
+ }
119
+ catch (error) {
120
+ logger.error('Failed to record command metrics', error);
121
+ }
122
+ }
123
+ recordAIRequest(model, commandType, duration, success, tokensUsed) {
124
+ if (!this.metricsEnabled)
125
+ return;
126
+ try {
127
+ metrics.aiRequestDuration.observe({ model, command_type: commandType }, duration / 1000);
128
+ metrics.aiRequestCount.inc({ model, command_type: commandType, success: success.toString() });
129
+ if (tokensUsed) {
130
+ metrics.aiTokensUsed.inc({ model, command_type: commandType }, tokensUsed);
131
+ }
132
+ this.updatePersistent((summary) => {
133
+ const current = summary.models[model] ?? { requests: 0, successes: 0, failures: 0, durationMs: 0 };
134
+ current.requests += 1;
135
+ if (success)
136
+ current.successes += 1;
137
+ else
138
+ current.failures += 1;
139
+ current.durationMs += duration;
140
+ summary.models[model] = current;
141
+ });
142
+ }
143
+ catch (error) {
144
+ logger.error('Failed to record AI request metrics', error);
145
+ }
146
+ }
147
+ recordCacheHit(cacheType) {
148
+ if (!this.metricsEnabled)
149
+ return;
150
+ try {
151
+ metrics.cacheHitCount.inc({ cache_type: cacheType });
152
+ this.updatePersistent((summary) => {
153
+ summary.cache.hits += 1;
154
+ });
155
+ }
156
+ catch (error) {
157
+ logger.error('Failed to record cache hit metrics', error);
158
+ }
159
+ }
160
+ recordCacheMiss(cacheType) {
161
+ if (!this.metricsEnabled)
162
+ return;
163
+ try {
164
+ metrics.cacheMissCount.inc({ cache_type: cacheType });
165
+ this.updatePersistent((summary) => {
166
+ summary.cache.misses += 1;
167
+ });
168
+ }
169
+ catch (error) {
170
+ logger.error('Failed to record cache miss metrics', error);
171
+ }
172
+ }
173
+ updateCacheSize(cacheType, size) {
174
+ if (!this.metricsEnabled)
175
+ return;
176
+ try {
177
+ metrics.cacheSize.set({ cache_type: cacheType }, size);
178
+ }
179
+ catch (error) {
180
+ logger.error('Failed to update cache size metrics', error);
181
+ }
182
+ }
183
+ recordError(errorType, command) {
184
+ if (!this.metricsEnabled)
185
+ return;
186
+ try {
187
+ metrics.errorCount.inc({ error_type: errorType, command: command || 'unknown' });
188
+ this.updatePersistent((summary) => {
189
+ const key = command ? `${errorType}:${command}` : errorType;
190
+ summary.errors[key] = (summary.errors[key] ?? 0) + 1;
191
+ });
192
+ }
193
+ catch (error) {
194
+ logger.error('Failed to record error metrics', error);
195
+ }
196
+ }
197
+ updateMemoryUsage() {
198
+ if (!this.metricsEnabled)
199
+ return;
200
+ try {
201
+ const memUsage = process.memoryUsage();
202
+ metrics.memoryUsage.set({ type: 'rss' }, memUsage.rss);
203
+ metrics.memoryUsage.set({ type: 'heap_used' }, memUsage.heapUsed);
204
+ metrics.memoryUsage.set({ type: 'heap_total' }, memUsage.heapTotal);
205
+ metrics.memoryUsage.set({ type: 'external' }, memUsage.external);
206
+ }
207
+ catch (error) {
208
+ logger.error('Failed to update memory usage metrics', error);
209
+ }
210
+ }
211
+ recordSession() {
212
+ if (!this.metricsEnabled)
213
+ return;
214
+ try {
215
+ metrics.sessionCount.inc();
216
+ this.updatePersistent((summary) => {
217
+ summary.sessions += 1;
218
+ });
219
+ }
220
+ catch (error) {
221
+ logger.error('Failed to record session metrics', error);
222
+ }
223
+ }
224
+ recordPluginLoaded() {
225
+ if (!this.metricsEnabled)
226
+ return;
227
+ try {
228
+ metrics.pluginLoadedCount.inc();
229
+ }
230
+ catch (error) {
231
+ logger.error('Failed to record plugin loaded metrics', error);
232
+ }
233
+ }
234
+ getSummary() {
235
+ return this.readPersistent();
236
+ }
237
+ resetPersistent() {
238
+ try {
239
+ fs.unlinkSync(this.persistentPath());
240
+ }
241
+ catch (error) {
242
+ if (error.code !== 'ENOENT')
243
+ throw error;
244
+ }
245
+ }
246
+ persistentPath() {
247
+ return path.join(process.cwd(), '.dhruv-metrics.json');
248
+ }
249
+ readPersistent() {
250
+ try {
251
+ const parsed = JSON.parse(fs.readFileSync(this.persistentPath(), 'utf8'));
252
+ return {
253
+ sessions: typeof parsed.sessions === 'number' ? parsed.sessions : 0,
254
+ commands: parsed.commands ?? {},
255
+ errors: parsed.errors ?? {},
256
+ models: parsed.models ?? {},
257
+ cache: parsed.cache ?? { hits: 0, misses: 0 },
258
+ };
259
+ }
260
+ catch {
261
+ return emptySummary();
262
+ }
263
+ }
264
+ updatePersistent(update) {
265
+ try {
266
+ const summary = this.readPersistent();
267
+ update(summary);
268
+ fs.writeFileSync(this.persistentPath(), JSON.stringify(summary, null, 2));
269
+ }
270
+ catch (error) {
271
+ logger.debug('Failed to persist metrics', { error: error.message });
272
+ }
273
+ }
274
+ async getMetrics() {
275
+ return register.metrics();
276
+ }
277
+ async getMetricsJSON() {
278
+ return register.getMetricsAsJSON();
279
+ }
280
+ getRegistry() {
281
+ return register;
282
+ }
283
+ }
284
+ export const metricsCollector = MetricsCollector.getInstance();
@@ -0,0 +1 @@
1
+ export declare function getSystemMessage(type: string): string;