@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.
- package/.github/ENTERPRISE.md +275 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +45 -17
- package/.github/ISSUE_TEMPLATE/documentation_issue.md +61 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +61 -9
- package/.github/ISSUE_TEMPLATE/security_vulnerability.md +74 -0
- package/.github/dependabot.yml +42 -2
- package/.github/pull_request_template.md +13 -0
- package/.github/workflows/ci.yml +51 -38
- package/.github/workflows/contribution.yml +41 -34
- package/.github/workflows/dependabot-auto-merge.yml +62 -2
- package/.github/workflows/labeler.yml +1 -0
- package/.github/workflows/release.yml +229 -0
- package/.github/workflows/security.yml +201 -0
- package/.releaserc.json +50 -0
- package/AGENTS.md +13 -0
- package/CHANGELOG.md +13 -0
- package/README.md +145 -40
- package/__tests__/cli-contract.test.ts +104 -0
- package/__tests__/core.test.ts +439 -0
- package/__tests__/diagnostics.test.ts +155 -0
- package/__tests__/file-workflows.test.ts +195 -0
- package/__tests__/interactive.test.ts +119 -0
- package/__tests__/setup.ts +62 -0
- package/__tests__/workflows.test.ts +118 -0
- package/dist/commands/explain.js +13 -44
- package/dist/commands/fix.js +13 -38
- package/dist/commands/generate.d.ts +6 -1
- package/dist/commands/generate.js +60 -55
- package/dist/commands/health.d.ts +4 -0
- package/dist/commands/health.js +419 -0
- package/dist/commands/init.js +56 -42
- package/dist/commands/menu.js +137 -24
- package/dist/commands/metrics.d.ts +5 -0
- package/dist/commands/metrics.js +80 -0
- package/dist/commands/optimize.js +42 -34
- package/dist/commands/review.d.ts +4 -1
- package/dist/commands/review.js +87 -43
- package/dist/commands/security-check.d.ts +4 -1
- package/dist/commands/security-check.js +109 -38
- package/dist/commands/status.d.ts +1 -0
- package/dist/commands/status.js +83 -0
- package/dist/commands/suggest.js +13 -39
- package/dist/config/config.d.ts +5 -1
- package/dist/config/config.js +53 -8
- package/dist/core/ai.d.ts +88 -2
- package/dist/core/ai.js +231 -30
- package/dist/core/command-catalog.d.ts +10 -0
- package/dist/core/command-catalog.js +27 -0
- package/dist/core/command-runner.d.ts +17 -0
- package/dist/core/command-runner.js +157 -0
- package/dist/core/logger.d.ts +40 -0
- package/dist/core/logger.js +139 -0
- package/dist/core/metrics.d.ts +62 -0
- package/dist/core/metrics.js +284 -0
- package/dist/core/prompts.d.ts +1 -0
- package/dist/core/prompts.js +121 -0
- package/dist/core/security.d.ts +34 -0
- package/dist/core/security.js +197 -0
- package/dist/index.js +84 -22
- package/dist/utils/projectType.d.ts +1 -1
- package/dist/utils/projectType.js +26 -7
- package/dist/utils/ux.d.ts +3 -0
- package/dist/utils/ux.js +15 -0
- package/docs/agents/domain.md +51 -0
- package/docs/agents/issue-tracker.md +45 -0
- package/docs/agents/triage-labels.md +15 -0
- package/docs/api/.nojekyll +1 -0
- package/docs/api/assets/hierarchy.js +1 -0
- package/docs/api/assets/highlight.css +71 -0
- package/docs/api/assets/icons.js +18 -0
- package/docs/api/assets/icons.svg +1 -0
- package/docs/api/assets/main.js +60 -0
- package/docs/api/assets/navigation.js +1 -0
- package/docs/api/assets/search.js +1 -0
- package/docs/api/assets/style.css +1633 -0
- package/docs/api/hierarchy.html +1 -0
- package/docs/api/index.html +161 -0
- package/docs/api/media/CONTRIBUTING.md +60 -0
- package/docs/api/media/SECURITY.md +8 -0
- package/docs/api/media/dhruv-cli-preview.svg +42 -0
- package/docs/api/media/publishing-fix.md +34 -0
- package/docs/api/modules.html +1 -0
- package/docs/dhruv-cli-preview.svg +42 -0
- package/docs/index.html +631 -533
- package/docs/publishing-fix.md +34 -0
- package/eslint.config.js +170 -0
- package/jest.config.json +37 -0
- package/lighthouserc.json +22 -0
- package/package.json +62 -8
- package/src/commands/explain.ts +13 -42
- package/src/commands/fix.ts +13 -31
- package/src/commands/generate.ts +68 -48
- package/src/commands/health.ts +485 -0
- package/src/commands/init.ts +57 -42
- package/src/commands/menu.ts +132 -24
- package/src/commands/metrics.ts +100 -0
- package/src/commands/optimize.ts +40 -28
- package/src/commands/review.ts +96 -37
- package/src/commands/security-check.ts +127 -33
- package/src/commands/status.ts +83 -0
- package/src/commands/suggest.ts +13 -32
- package/src/config/config.ts +60 -8
- package/src/core/ai.ts +265 -26
- package/src/core/command-catalog.ts +37 -0
- package/src/core/command-runner.ts +185 -0
- package/src/core/logger.ts +195 -0
- package/src/core/metrics.ts +335 -0
- package/src/core/prompts.ts +128 -0
- package/src/core/security.ts +243 -0
- package/src/index.ts +90 -22
- package/src/utils/projectType.ts +22 -7
- package/src/utils/ux.ts +18 -0
- package/test-suite.sh +147 -0
- package/tsconfig.json +4 -3
- package/typedoc.json +44 -0
- package/types/global.d.ts +13 -0
- package/validate-workflows.sh +270 -0
- package/.eslintignore +0 -1
- package/.eslintrc.cjs +0 -43
- package/.github/workflows/auto-assign.yml +0 -14
- 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;
|