@agi-cli/server 0.1.66 → 0.1.68
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/package.json +3 -3
- package/src/index.ts +9 -0
- package/src/routes/ask.ts +6 -6
- package/src/routes/config.ts +196 -159
- package/src/routes/git.ts +160 -47
- package/src/routes/session-messages.ts +114 -95
- package/src/routes/sessions.ts +5 -2
- package/src/runtime/api-error.ts +191 -0
- package/src/runtime/debug-state.ts +124 -0
- package/src/runtime/debug.ts +43 -30
- package/src/runtime/logger.ts +204 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified API error handling
|
|
3
|
+
*
|
|
4
|
+
* Provides consistent error serialization and response formatting
|
|
5
|
+
* across all API endpoints.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { isDebugEnabled } from './debug-state';
|
|
9
|
+
import { toErrorPayload } from './error-handling';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Standard API error response format
|
|
13
|
+
*/
|
|
14
|
+
export type APIErrorResponse = {
|
|
15
|
+
error: {
|
|
16
|
+
message: string;
|
|
17
|
+
type: string;
|
|
18
|
+
code?: string;
|
|
19
|
+
status?: number;
|
|
20
|
+
details?: Record<string, unknown>;
|
|
21
|
+
stack?: string;
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Custom API Error class
|
|
27
|
+
*/
|
|
28
|
+
export class APIError extends Error {
|
|
29
|
+
public readonly code?: string;
|
|
30
|
+
public readonly status: number;
|
|
31
|
+
public readonly type: string;
|
|
32
|
+
public readonly details?: Record<string, unknown>;
|
|
33
|
+
|
|
34
|
+
constructor(
|
|
35
|
+
message: string,
|
|
36
|
+
options?: {
|
|
37
|
+
code?: string;
|
|
38
|
+
status?: number;
|
|
39
|
+
type?: string;
|
|
40
|
+
details?: Record<string, unknown>;
|
|
41
|
+
cause?: unknown;
|
|
42
|
+
},
|
|
43
|
+
) {
|
|
44
|
+
super(message);
|
|
45
|
+
this.name = 'APIError';
|
|
46
|
+
this.code = options?.code;
|
|
47
|
+
this.status = options?.status ?? 500;
|
|
48
|
+
this.type = options?.type ?? 'api_error';
|
|
49
|
+
this.details = options?.details;
|
|
50
|
+
|
|
51
|
+
if (options?.cause) {
|
|
52
|
+
this.cause = options.cause;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Maintain proper stack trace
|
|
56
|
+
if (Error.captureStackTrace) {
|
|
57
|
+
Error.captureStackTrace(this, APIError);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Serialize any error into a consistent API error response
|
|
64
|
+
*
|
|
65
|
+
* @param err - The error to serialize
|
|
66
|
+
* @returns A properly formatted API error response
|
|
67
|
+
*/
|
|
68
|
+
export function serializeError(err: unknown): APIErrorResponse {
|
|
69
|
+
// Use existing error payload logic
|
|
70
|
+
const payload = toErrorPayload(err);
|
|
71
|
+
|
|
72
|
+
// Determine HTTP status code
|
|
73
|
+
let status = 500;
|
|
74
|
+
if (err && typeof err === 'object') {
|
|
75
|
+
const errObj = err as Record<string, unknown>;
|
|
76
|
+
if (typeof errObj.status === 'number') {
|
|
77
|
+
status = errObj.status;
|
|
78
|
+
} else if (typeof errObj.statusCode === 'number') {
|
|
79
|
+
status = errObj.statusCode;
|
|
80
|
+
} else if (
|
|
81
|
+
errObj.details &&
|
|
82
|
+
typeof errObj.details === 'object' &&
|
|
83
|
+
typeof (errObj.details as Record<string, unknown>).statusCode === 'number'
|
|
84
|
+
) {
|
|
85
|
+
status = (errObj.details as Record<string, unknown>).statusCode as number;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Handle APIError instances
|
|
90
|
+
if (err instanceof APIError) {
|
|
91
|
+
status = err.status;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Extract code if available
|
|
95
|
+
let code: string | undefined;
|
|
96
|
+
if (err && typeof err === 'object') {
|
|
97
|
+
const errObj = err as Record<string, unknown>;
|
|
98
|
+
if (typeof errObj.code === 'string') {
|
|
99
|
+
code = errObj.code;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (err instanceof APIError && err.code) {
|
|
104
|
+
code = err.code;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Build response
|
|
108
|
+
const response: APIErrorResponse = {
|
|
109
|
+
error: {
|
|
110
|
+
message: payload.message || 'An error occurred',
|
|
111
|
+
type: payload.type || 'unknown',
|
|
112
|
+
status,
|
|
113
|
+
...(code ? { code } : {}),
|
|
114
|
+
...(payload.details ? { details: payload.details } : {}),
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// Include stack trace in debug mode
|
|
119
|
+
if (isDebugEnabled() && err instanceof Error && err.stack) {
|
|
120
|
+
response.error.stack = err.stack;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return response;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Create an error response with proper HTTP status code
|
|
128
|
+
*
|
|
129
|
+
* @param err - The error to convert
|
|
130
|
+
* @returns Tuple of [APIErrorResponse, HTTP status code]
|
|
131
|
+
*/
|
|
132
|
+
export function createErrorResponse(err: unknown): [APIErrorResponse, number] {
|
|
133
|
+
const response = serializeError(err);
|
|
134
|
+
return [response, response.error.status ?? 500];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Normalize error to ensure it's an Error instance
|
|
139
|
+
*
|
|
140
|
+
* @param err - The error to normalize
|
|
141
|
+
* @returns An Error instance
|
|
142
|
+
*/
|
|
143
|
+
export function normalizeError(err: unknown): Error {
|
|
144
|
+
if (err instanceof Error) {
|
|
145
|
+
return err;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (typeof err === 'string') {
|
|
149
|
+
return new Error(err);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (err && typeof err === 'object') {
|
|
153
|
+
const errObj = err as Record<string, unknown>;
|
|
154
|
+
if (typeof errObj.message === 'string') {
|
|
155
|
+
return new Error(errObj.message);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return new Error('An unknown error occurred');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Extract error message from any error type
|
|
164
|
+
*
|
|
165
|
+
* @param err - The error to extract message from
|
|
166
|
+
* @returns The error message string
|
|
167
|
+
*/
|
|
168
|
+
export function getErrorMessage(err: unknown): string {
|
|
169
|
+
if (typeof err === 'string') {
|
|
170
|
+
return err;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (err instanceof Error) {
|
|
174
|
+
return err.message;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (err && typeof err === 'object') {
|
|
178
|
+
const errObj = err as Record<string, unknown>;
|
|
179
|
+
if (typeof errObj.message === 'string') {
|
|
180
|
+
return errObj.message;
|
|
181
|
+
}
|
|
182
|
+
if (typeof errObj.error === 'string') {
|
|
183
|
+
return errObj.error;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return 'An unknown error occurred';
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Legacy compatibility - AskServiceError alias
|
|
191
|
+
export { APIError as AskServiceError };
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime debug state management
|
|
3
|
+
*
|
|
4
|
+
* Centralizes debug flag state that can be set either via:
|
|
5
|
+
* - Environment variables (AGI_DEBUG, DEBUG_AGI)
|
|
6
|
+
* - Runtime configuration (CLI --debug flag)
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const TRUTHY = new Set(['1', 'true', 'yes', 'on']);
|
|
10
|
+
|
|
11
|
+
type DebugState = {
|
|
12
|
+
enabled: boolean;
|
|
13
|
+
traceEnabled: boolean;
|
|
14
|
+
runtimeOverride: boolean | null;
|
|
15
|
+
runtimeTraceOverride: boolean | null;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// Global state
|
|
19
|
+
const state: DebugState = {
|
|
20
|
+
enabled: false,
|
|
21
|
+
traceEnabled: false,
|
|
22
|
+
runtimeOverride: null,
|
|
23
|
+
runtimeTraceOverride: null,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Check if environment variables indicate debug mode
|
|
28
|
+
*/
|
|
29
|
+
function checkEnvDebug(): boolean {
|
|
30
|
+
const sources = [process.env.AGI_DEBUG, process.env.DEBUG_AGI];
|
|
31
|
+
for (const value of sources) {
|
|
32
|
+
if (!value) continue;
|
|
33
|
+
const trimmed = value.trim().toLowerCase();
|
|
34
|
+
if (TRUTHY.has(trimmed) || trimmed === 'all') {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Check if environment variables indicate trace mode
|
|
43
|
+
*/
|
|
44
|
+
function checkEnvTrace(): boolean {
|
|
45
|
+
const sources = [process.env.AGI_TRACE, process.env.TRACE_AGI];
|
|
46
|
+
for (const value of sources) {
|
|
47
|
+
if (!value) continue;
|
|
48
|
+
const trimmed = value.trim().toLowerCase();
|
|
49
|
+
if (TRUTHY.has(trimmed)) {
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Initialize debug state from environment
|
|
58
|
+
*/
|
|
59
|
+
function initialize() {
|
|
60
|
+
if (state.runtimeOverride === null) {
|
|
61
|
+
state.enabled = checkEnvDebug();
|
|
62
|
+
}
|
|
63
|
+
if (state.runtimeTraceOverride === null) {
|
|
64
|
+
state.traceEnabled = checkEnvTrace();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Check if debug mode is enabled
|
|
70
|
+
* Considers both runtime override and environment variables
|
|
71
|
+
*/
|
|
72
|
+
export function isDebugEnabled(): boolean {
|
|
73
|
+
initialize();
|
|
74
|
+
return state.enabled;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Check if trace mode is enabled (shows stack traces)
|
|
79
|
+
* Trace mode requires debug mode to be enabled
|
|
80
|
+
*/
|
|
81
|
+
export function isTraceEnabled(): boolean {
|
|
82
|
+
initialize();
|
|
83
|
+
return state.enabled && state.traceEnabled;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Enable or disable debug mode at runtime
|
|
88
|
+
* Overrides environment variable settings
|
|
89
|
+
*
|
|
90
|
+
* @param enabled - true to enable debug mode, false to disable
|
|
91
|
+
*/
|
|
92
|
+
export function setDebugEnabled(enabled: boolean): void {
|
|
93
|
+
state.enabled = enabled;
|
|
94
|
+
state.runtimeOverride = enabled;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Enable or disable trace mode at runtime
|
|
99
|
+
* Trace mode shows full stack traces in error logs
|
|
100
|
+
*
|
|
101
|
+
* @param enabled - true to enable trace mode, false to disable
|
|
102
|
+
*/
|
|
103
|
+
export function setTraceEnabled(enabled: boolean): void {
|
|
104
|
+
state.traceEnabled = enabled;
|
|
105
|
+
state.runtimeTraceOverride = enabled;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Reset debug state to environment defaults
|
|
110
|
+
*/
|
|
111
|
+
export function resetDebugState(): void {
|
|
112
|
+
state.runtimeOverride = null;
|
|
113
|
+
state.runtimeTraceOverride = null;
|
|
114
|
+
state.enabled = checkEnvDebug();
|
|
115
|
+
state.traceEnabled = checkEnvTrace();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Get current debug state (for testing/diagnostics)
|
|
120
|
+
*/
|
|
121
|
+
export function getDebugState(): Readonly<DebugState> {
|
|
122
|
+
initialize();
|
|
123
|
+
return { ...state };
|
|
124
|
+
}
|
package/src/runtime/debug.ts
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Legacy debug utilities - now integrated with new logger
|
|
3
|
+
*
|
|
4
|
+
* This file maintains backward compatibility while using the new
|
|
5
|
+
* centralized debug-state and logger modules.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { isDebugEnabled as isDebugEnabledNew } from './debug-state';
|
|
9
|
+
import { time as timeNew, debug as debugNew } from './logger';
|
|
10
|
+
|
|
1
11
|
const TRUTHY = new Set(['1', 'true', 'yes', 'on']);
|
|
2
12
|
|
|
3
13
|
const SYNONYMS: Record<string, string> = {
|
|
@@ -60,45 +70,48 @@ function getDebugConfig(): DebugConfig {
|
|
|
60
70
|
return cachedConfig;
|
|
61
71
|
}
|
|
62
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Check if debug mode is enabled for a specific flag
|
|
75
|
+
* Now uses the centralized debug state
|
|
76
|
+
*
|
|
77
|
+
* @deprecated Use isDebugEnabled from debug-state.ts instead
|
|
78
|
+
*/
|
|
63
79
|
export function isDebugEnabled(flag?: string): boolean {
|
|
80
|
+
// Use new centralized debug state for general debug
|
|
81
|
+
if (!flag || flag === 'log') {
|
|
82
|
+
return isDebugEnabledNew();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// For specific flags like 'timing', check both new state and legacy env vars
|
|
86
|
+
if (flag === 'timing') {
|
|
87
|
+
// If new debug state is enabled OR timing flag is set
|
|
88
|
+
if (isDebugEnabledNew()) return true;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Legacy flag checking
|
|
64
92
|
const config = getDebugConfig();
|
|
65
93
|
if (config.flags.has('all')) return true;
|
|
66
94
|
if (flag) return config.flags.has(flag);
|
|
67
95
|
return config.flags.has('log');
|
|
68
96
|
}
|
|
69
97
|
|
|
98
|
+
/**
|
|
99
|
+
* Log debug message
|
|
100
|
+
* Now uses the centralized logger
|
|
101
|
+
*
|
|
102
|
+
* @deprecated Use logger.debug from logger.ts instead
|
|
103
|
+
*/
|
|
70
104
|
export function debugLog(...args: unknown[]) {
|
|
71
105
|
if (!isDebugEnabled('log')) return;
|
|
72
|
-
|
|
73
|
-
console.log('[debug]', ...args);
|
|
74
|
-
} catch {}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function nowMs(): number {
|
|
78
|
-
const perf = (globalThis as { performance?: { now?: () => number } })
|
|
79
|
-
.performance;
|
|
80
|
-
if (perf && typeof perf.now === 'function') return perf.now();
|
|
81
|
-
return Date.now();
|
|
106
|
+
debugNew(args.map((arg) => String(arg)).join(' '));
|
|
82
107
|
}
|
|
83
108
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
return {
|
|
93
|
-
end(meta?: Record<string, unknown>) {
|
|
94
|
-
if (finished) return;
|
|
95
|
-
finished = true;
|
|
96
|
-
const duration = nowMs() - start;
|
|
97
|
-
try {
|
|
98
|
-
const line = `[timing] ${label} ${duration.toFixed(1)}ms`;
|
|
99
|
-
if (meta && Object.keys(meta).length) console.log(line, meta);
|
|
100
|
-
else console.log(line);
|
|
101
|
-
} catch {}
|
|
102
|
-
},
|
|
103
|
-
};
|
|
109
|
+
/**
|
|
110
|
+
* Create a timer for performance measurement
|
|
111
|
+
* Integrated with centralized logger
|
|
112
|
+
*/
|
|
113
|
+
export function time(label: string): {
|
|
114
|
+
end(meta?: Record<string, unknown>): void;
|
|
115
|
+
} {
|
|
116
|
+
return timeNew(label);
|
|
104
117
|
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centralized logging utility
|
|
3
|
+
*
|
|
4
|
+
* Provides structured logging with debug mode awareness.
|
|
5
|
+
* Replaces scattered console.log calls throughout the codebase.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { isDebugEnabled, isTraceEnabled } from './debug-state';
|
|
9
|
+
|
|
10
|
+
export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Format a log message with optional metadata
|
|
14
|
+
*/
|
|
15
|
+
function _formatMessage(
|
|
16
|
+
level: LogLevel,
|
|
17
|
+
message: string,
|
|
18
|
+
meta?: Record<string, unknown>,
|
|
19
|
+
): string {
|
|
20
|
+
const timestamp = new Date().toISOString();
|
|
21
|
+
const prefix = `[${timestamp}] [${level.toUpperCase()}]`;
|
|
22
|
+
|
|
23
|
+
if (meta && Object.keys(meta).length > 0) {
|
|
24
|
+
return `${prefix} ${message} ${JSON.stringify(meta)}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return `${prefix} ${message}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Log at debug level (only when debug mode is enabled)
|
|
32
|
+
*/
|
|
33
|
+
export function debug(message: string, meta?: Record<string, unknown>): void {
|
|
34
|
+
if (!isDebugEnabled()) return;
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
if (meta && Object.keys(meta).length > 0) {
|
|
38
|
+
console.log(`[debug] ${message}`, meta);
|
|
39
|
+
} else {
|
|
40
|
+
console.log(`[debug] ${message}`);
|
|
41
|
+
}
|
|
42
|
+
} catch {
|
|
43
|
+
// Silently fail
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Log informational messages
|
|
49
|
+
*/
|
|
50
|
+
export function info(message: string, meta?: Record<string, unknown>): void {
|
|
51
|
+
try {
|
|
52
|
+
if (meta && Object.keys(meta).length > 0) {
|
|
53
|
+
console.log(`[info] ${message}`, meta);
|
|
54
|
+
} else {
|
|
55
|
+
console.log(`[info] ${message}`);
|
|
56
|
+
}
|
|
57
|
+
} catch {
|
|
58
|
+
// Silently fail
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Log warning messages
|
|
64
|
+
*/
|
|
65
|
+
export function warn(message: string, meta?: Record<string, unknown>): void {
|
|
66
|
+
try {
|
|
67
|
+
if (meta && Object.keys(meta).length > 0) {
|
|
68
|
+
console.warn(`[warn] ${message}`, meta);
|
|
69
|
+
} else {
|
|
70
|
+
console.warn(`[warn] ${message}`);
|
|
71
|
+
}
|
|
72
|
+
} catch {
|
|
73
|
+
// Silently fail
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Log error messages (only in debug mode, stack trace only with --trace)
|
|
79
|
+
*/
|
|
80
|
+
export function error(
|
|
81
|
+
message: string,
|
|
82
|
+
err?: unknown,
|
|
83
|
+
meta?: Record<string, unknown>,
|
|
84
|
+
): void {
|
|
85
|
+
// Only log errors when debug mode is enabled
|
|
86
|
+
if (!isDebugEnabled()) return;
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
const logMeta: Record<string, unknown> = { ...meta };
|
|
90
|
+
|
|
91
|
+
if (err) {
|
|
92
|
+
if (err instanceof Error) {
|
|
93
|
+
// Always show error name and message in debug mode
|
|
94
|
+
logMeta.error = {
|
|
95
|
+
name: err.name,
|
|
96
|
+
message: err.message,
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// Show full stack trace only with --trace flag
|
|
100
|
+
if (isTraceEnabled() && err.stack) {
|
|
101
|
+
logMeta.error.stack = err.stack;
|
|
102
|
+
}
|
|
103
|
+
} else if (typeof err === 'string') {
|
|
104
|
+
logMeta.error = err;
|
|
105
|
+
} else if (err && typeof err === 'object') {
|
|
106
|
+
// For other error objects, try to extract useful info
|
|
107
|
+
const errObj = err as Record<string, unknown>;
|
|
108
|
+
logMeta.error = {
|
|
109
|
+
...(typeof errObj.name === 'string' ? { name: errObj.name } : {}),
|
|
110
|
+
...(typeof errObj.message === 'string'
|
|
111
|
+
? { message: errObj.message }
|
|
112
|
+
: {}),
|
|
113
|
+
...(typeof errObj.code === 'string' ? { code: errObj.code } : {}),
|
|
114
|
+
...(typeof errObj.status === 'number'
|
|
115
|
+
? { status: errObj.status }
|
|
116
|
+
: {}),
|
|
117
|
+
...(typeof errObj.statusCode === 'number'
|
|
118
|
+
? { statusCode: errObj.statusCode }
|
|
119
|
+
: {}),
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
// Include stack in trace mode
|
|
123
|
+
if (isTraceEnabled() && typeof errObj.stack === 'string') {
|
|
124
|
+
logMeta.error.stack = errObj.stack;
|
|
125
|
+
}
|
|
126
|
+
} else {
|
|
127
|
+
// Fallback for primitive types
|
|
128
|
+
logMeta.error = String(err);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (Object.keys(logMeta).length > 0) {
|
|
133
|
+
console.error(`[error] ${message}`, logMeta);
|
|
134
|
+
} else {
|
|
135
|
+
console.error(`[error] ${message}`);
|
|
136
|
+
}
|
|
137
|
+
} catch (logErr) {
|
|
138
|
+
// Last resort: at least try to log something
|
|
139
|
+
try {
|
|
140
|
+
console.error(`[error] ${message} (logging failed:`, logErr, ')');
|
|
141
|
+
} catch {
|
|
142
|
+
// Give up silently
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Logger object with all methods
|
|
149
|
+
*/
|
|
150
|
+
export const logger = {
|
|
151
|
+
debug,
|
|
152
|
+
info,
|
|
153
|
+
warn,
|
|
154
|
+
error,
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Timing utilities (integrates with existing debug.ts timing)
|
|
159
|
+
*/
|
|
160
|
+
function nowMs(): number {
|
|
161
|
+
const perf = (globalThis as { performance?: { now?: () => number } })
|
|
162
|
+
.performance;
|
|
163
|
+
if (perf && typeof perf.now === 'function') return perf.now();
|
|
164
|
+
return Date.now();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
type Timer = {
|
|
168
|
+
end(meta?: Record<string, unknown>): void;
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Create a timer for performance measurement
|
|
173
|
+
* Only active when debug mode is enabled
|
|
174
|
+
*/
|
|
175
|
+
export function time(label: string): Timer {
|
|
176
|
+
if (!isDebugEnabled()) {
|
|
177
|
+
return { end() {} };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const start = nowMs();
|
|
181
|
+
let finished = false;
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
end(meta?: Record<string, unknown>) {
|
|
185
|
+
if (finished) return;
|
|
186
|
+
finished = true;
|
|
187
|
+
const duration = nowMs() - start;
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
const line = `[timing] ${label} ${duration.toFixed(1)}ms`;
|
|
191
|
+
if (meta && Object.keys(meta).length) {
|
|
192
|
+
console.log(line, meta);
|
|
193
|
+
} else {
|
|
194
|
+
console.log(line);
|
|
195
|
+
}
|
|
196
|
+
} catch {
|
|
197
|
+
// Silently fail
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Export legacy compatibility
|
|
204
|
+
export { isDebugEnabled, isTraceEnabled };
|