@elliotding/ai-agent-mcp 0.1.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/dist/api/cached-client.d.ts +48 -0
- package/dist/api/cached-client.d.ts.map +1 -0
- package/dist/api/cached-client.js +126 -0
- package/dist/api/cached-client.js.map +1 -0
- package/dist/api/client.d.ts +213 -0
- package/dist/api/client.d.ts.map +1 -0
- package/dist/api/client.js +326 -0
- package/dist/api/client.js.map +1 -0
- package/dist/auth/index.d.ts +8 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +26 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth/middleware.d.ts +36 -0
- package/dist/auth/middleware.d.ts.map +1 -0
- package/dist/auth/middleware.js +194 -0
- package/dist/auth/middleware.js.map +1 -0
- package/dist/auth/permissions.d.ts +60 -0
- package/dist/auth/permissions.d.ts.map +1 -0
- package/dist/auth/permissions.js +256 -0
- package/dist/auth/permissions.js.map +1 -0
- package/dist/auth/token-validator.d.ts +52 -0
- package/dist/auth/token-validator.d.ts.map +1 -0
- package/dist/auth/token-validator.js +217 -0
- package/dist/auth/token-validator.js.map +1 -0
- package/dist/cache/cache-manager.d.ts +49 -0
- package/dist/cache/cache-manager.d.ts.map +1 -0
- package/dist/cache/cache-manager.js +191 -0
- package/dist/cache/cache-manager.js.map +1 -0
- package/dist/cache/index.d.ts +6 -0
- package/dist/cache/index.d.ts.map +1 -0
- package/dist/cache/index.js +12 -0
- package/dist/cache/index.js.map +1 -0
- package/dist/cache/redis-client.d.ts +45 -0
- package/dist/cache/redis-client.d.ts.map +1 -0
- package/dist/cache/redis-client.js +210 -0
- package/dist/cache/redis-client.js.map +1 -0
- package/dist/config/constants.d.ts +28 -0
- package/dist/config/constants.d.ts.map +1 -0
- package/dist/config/constants.js +31 -0
- package/dist/config/constants.js.map +1 -0
- package/dist/config/index.d.ts +54 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +168 -0
- package/dist/config/index.js.map +1 -0
- package/dist/filesystem/manager.d.ts +45 -0
- package/dist/filesystem/manager.d.ts.map +1 -0
- package/dist/filesystem/manager.js +246 -0
- package/dist/filesystem/manager.js.map +1 -0
- package/dist/git/multi-source-manager.d.ts +62 -0
- package/dist/git/multi-source-manager.d.ts.map +1 -0
- package/dist/git/multi-source-manager.js +293 -0
- package/dist/git/multi-source-manager.js.map +1 -0
- package/dist/git/operations.d.ts +27 -0
- package/dist/git/operations.d.ts.map +1 -0
- package/dist/git/operations.js +83 -0
- package/dist/git/operations.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +109 -0
- package/dist/index.js.map +1 -0
- package/dist/monitoring/health.d.ts +35 -0
- package/dist/monitoring/health.d.ts.map +1 -0
- package/dist/monitoring/health.js +105 -0
- package/dist/monitoring/health.js.map +1 -0
- package/dist/resources/index.d.ts +6 -0
- package/dist/resources/index.d.ts.map +1 -0
- package/dist/resources/index.js +10 -0
- package/dist/resources/index.js.map +1 -0
- package/dist/resources/loader.d.ts +87 -0
- package/dist/resources/loader.d.ts.map +1 -0
- package/dist/resources/loader.js +452 -0
- package/dist/resources/loader.js.map +1 -0
- package/dist/server/http.d.ts +57 -0
- package/dist/server/http.d.ts.map +1 -0
- package/dist/server/http.js +336 -0
- package/dist/server/http.js.map +1 -0
- package/dist/server.d.ts +13 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +157 -0
- package/dist/server.js.map +1 -0
- package/dist/session/manager.d.ts +91 -0
- package/dist/session/manager.d.ts.map +1 -0
- package/dist/session/manager.js +251 -0
- package/dist/session/manager.js.map +1 -0
- package/dist/tools/index.d.ts +11 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +27 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/manage-subscription.d.ts +43 -0
- package/dist/tools/manage-subscription.d.ts.map +1 -0
- package/dist/tools/manage-subscription.js +268 -0
- package/dist/tools/manage-subscription.js.map +1 -0
- package/dist/tools/registry.d.ts +40 -0
- package/dist/tools/registry.d.ts.map +1 -0
- package/dist/tools/registry.js +85 -0
- package/dist/tools/registry.js.map +1 -0
- package/dist/tools/search-resources.d.ts +31 -0
- package/dist/tools/search-resources.d.ts.map +1 -0
- package/dist/tools/search-resources.js +154 -0
- package/dist/tools/search-resources.js.map +1 -0
- package/dist/tools/sync-resources.d.ts +41 -0
- package/dist/tools/sync-resources.d.ts.map +1 -0
- package/dist/tools/sync-resources.js +606 -0
- package/dist/tools/sync-resources.js.map +1 -0
- package/dist/tools/uninstall-resource.d.ts +30 -0
- package/dist/tools/uninstall-resource.d.ts.map +1 -0
- package/dist/tools/uninstall-resource.js +259 -0
- package/dist/tools/uninstall-resource.js.map +1 -0
- package/dist/tools/upload-resource.d.ts +77 -0
- package/dist/tools/upload-resource.d.ts.map +1 -0
- package/dist/tools/upload-resource.js +252 -0
- package/dist/tools/upload-resource.js.map +1 -0
- package/dist/transport/sse.d.ts +29 -0
- package/dist/transport/sse.d.ts.map +1 -0
- package/dist/transport/sse.js +271 -0
- package/dist/transport/sse.js.map +1 -0
- package/dist/types/errors.d.ts +60 -0
- package/dist/types/errors.d.ts.map +1 -0
- package/dist/types/errors.js +112 -0
- package/dist/types/errors.js.map +1 -0
- package/dist/types/index.d.ts +7 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +23 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/mcp.d.ts +50 -0
- package/dist/types/mcp.d.ts.map +1 -0
- package/dist/types/mcp.js +6 -0
- package/dist/types/mcp.js.map +1 -0
- package/dist/types/resources.d.ts +109 -0
- package/dist/types/resources.d.ts.map +1 -0
- package/dist/types/resources.js +7 -0
- package/dist/types/resources.js.map +1 -0
- package/dist/types/tools.d.ts +147 -0
- package/dist/types/tools.d.ts.map +1 -0
- package/dist/types/tools.js +6 -0
- package/dist/types/tools.js.map +1 -0
- package/dist/utils/cursor-paths.d.ts +49 -0
- package/dist/utils/cursor-paths.d.ts.map +1 -0
- package/dist/utils/cursor-paths.js +116 -0
- package/dist/utils/cursor-paths.js.map +1 -0
- package/dist/utils/log-cleaner.d.ts +18 -0
- package/dist/utils/log-cleaner.d.ts.map +1 -0
- package/dist/utils/log-cleaner.js +112 -0
- package/dist/utils/log-cleaner.js.map +1 -0
- package/dist/utils/logger.d.ts +59 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +292 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/validation.d.ts +58 -0
- package/dist/utils/validation.d.ts.map +1 -0
- package/dist/utils/validation.js +214 -0
- package/dist/utils/validation.js.map +1 -0
- package/package.json +58 -0
- package/src/api/cached-client.ts +144 -0
- package/src/api/client.ts +578 -0
- package/src/auth/index.ts +11 -0
- package/src/auth/middleware.ts +244 -0
- package/src/auth/permissions.ts +317 -0
- package/src/auth/token-validator.ts +294 -0
- package/src/cache/cache-manager.ts +243 -0
- package/src/cache/index.ts +6 -0
- package/src/cache/redis-client.ts +249 -0
- package/src/config/constants.ts +33 -0
- package/src/config/index.ts +228 -0
- package/src/filesystem/manager.ts +235 -0
- package/src/git/multi-source-manager.ts +333 -0
- package/src/git/operations.ts +93 -0
- package/src/index.ts +139 -0
- package/src/monitoring/health.ts +132 -0
- package/src/resources/index.ts +13 -0
- package/src/resources/loader.ts +530 -0
- package/src/server/http.ts +427 -0
- package/src/server.ts +191 -0
- package/src/session/manager.ts +296 -0
- package/src/tools/index.ts +11 -0
- package/src/tools/manage-subscription.ts +332 -0
- package/src/tools/registry.ts +97 -0
- package/src/tools/search-resources.ts +177 -0
- package/src/tools/sync-resources.ts +662 -0
- package/src/tools/uninstall-resource.ts +248 -0
- package/src/tools/upload-resource.ts +258 -0
- package/src/transport/sse.ts +308 -0
- package/src/types/errors.ts +146 -0
- package/src/types/index.ts +7 -0
- package/src/types/mcp.ts +61 -0
- package/src/types/resources.ts +141 -0
- package/src/types/tools.ts +175 -0
- package/src/utils/cursor-paths.ts +83 -0
- package/src/utils/log-cleaner.ts +92 -0
- package/src/utils/logger.ts +333 -0
- package/src/utils/validation.ts +262 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Tool Types
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { MCPToolSchema } from './mcp';
|
|
6
|
+
|
|
7
|
+
// Tool Handler Function Type (generic, accepts any params and returns any result)
|
|
8
|
+
export type ToolHandler = (params: unknown) => Promise<ToolResult>;
|
|
9
|
+
|
|
10
|
+
// Tool Definition
|
|
11
|
+
export interface ToolDefinition {
|
|
12
|
+
name: string;
|
|
13
|
+
description: string;
|
|
14
|
+
inputSchema: MCPToolSchema;
|
|
15
|
+
handler: ToolHandler;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Tool Result (generic)
|
|
19
|
+
export interface ToolResult<T = unknown> {
|
|
20
|
+
success: boolean;
|
|
21
|
+
data?: T;
|
|
22
|
+
error?: {
|
|
23
|
+
code: string;
|
|
24
|
+
message: string;
|
|
25
|
+
details?: unknown;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
//===============================================
|
|
30
|
+
// Tool-specific Parameter and Result Types
|
|
31
|
+
//===============================================
|
|
32
|
+
|
|
33
|
+
// sync_resources
|
|
34
|
+
export interface SyncResourcesParams {
|
|
35
|
+
mode?: 'check' | 'incremental' | 'full';
|
|
36
|
+
scope?: 'global' | 'workspace' | 'all';
|
|
37
|
+
types?: string[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface McpSetupItem {
|
|
41
|
+
/** MCP server name as it appears in mcp.json */
|
|
42
|
+
server_name: string;
|
|
43
|
+
/** Human-readable path to mcp.json on this platform */
|
|
44
|
+
mcp_json_path: string;
|
|
45
|
+
/** env keys that are currently empty and need user input */
|
|
46
|
+
missing_env: string[];
|
|
47
|
+
/** true when the registered command might not be correct for this machine */
|
|
48
|
+
command_needs_verification: boolean;
|
|
49
|
+
/** the command string that was registered */
|
|
50
|
+
command: string;
|
|
51
|
+
/** freeform guidance shown to the user */
|
|
52
|
+
setup_hint: string;
|
|
53
|
+
/** absolute path to a local setup/readme doc found in the install directory, if any */
|
|
54
|
+
setup_doc?: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface SyncResourcesResult {
|
|
58
|
+
mode: string;
|
|
59
|
+
health_score: number;
|
|
60
|
+
summary: {
|
|
61
|
+
total: number;
|
|
62
|
+
synced: number;
|
|
63
|
+
cached: number;
|
|
64
|
+
failed: number;
|
|
65
|
+
};
|
|
66
|
+
details: Array<{
|
|
67
|
+
id: string;
|
|
68
|
+
name: string;
|
|
69
|
+
action: string;
|
|
70
|
+
version: string;
|
|
71
|
+
}>;
|
|
72
|
+
/**
|
|
73
|
+
* MCP servers that were installed/updated but require manual configuration
|
|
74
|
+
* before they can be used. Present only when at least one server needs setup.
|
|
75
|
+
*/
|
|
76
|
+
pending_setup?: McpSetupItem[];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// manage_subscription
|
|
80
|
+
export interface ManageSubscriptionParams {
|
|
81
|
+
action: 'subscribe' | 'unsubscribe' | 'list' | 'batch_subscribe' | 'batch_unsubscribe';
|
|
82
|
+
resource_ids?: string[];
|
|
83
|
+
auto_sync?: boolean;
|
|
84
|
+
scope?: 'global' | 'workspace';
|
|
85
|
+
notify?: boolean;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface ManageSubscriptionResult {
|
|
89
|
+
action: string;
|
|
90
|
+
success: boolean;
|
|
91
|
+
subscriptions?: Array<{
|
|
92
|
+
id: string;
|
|
93
|
+
name: string;
|
|
94
|
+
type: string;
|
|
95
|
+
subscribed_at: string;
|
|
96
|
+
}>;
|
|
97
|
+
message?: string;
|
|
98
|
+
/** Sync results for each resource after auto-sync on subscribe */
|
|
99
|
+
sync_details?: Array<{ id: string; name: string; action: string }>;
|
|
100
|
+
/** MCP servers that need manual configuration after auto-sync */
|
|
101
|
+
pending_setup?: unknown[];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// search_resources
|
|
105
|
+
export interface SearchResourcesParams {
|
|
106
|
+
team?: string;
|
|
107
|
+
type?: string;
|
|
108
|
+
keyword: string;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export interface SearchResourcesResult {
|
|
112
|
+
total: number;
|
|
113
|
+
results: Array<{
|
|
114
|
+
id: string;
|
|
115
|
+
name: string;
|
|
116
|
+
type: string;
|
|
117
|
+
team: string;
|
|
118
|
+
version: string;
|
|
119
|
+
description: string;
|
|
120
|
+
score: number;
|
|
121
|
+
is_subscribed: boolean;
|
|
122
|
+
is_installed: boolean;
|
|
123
|
+
}>;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// upload_resource
|
|
127
|
+
export interface FileEntry {
|
|
128
|
+
path: string; // Relative path under the type subdir (e.g. "my-cmd.md" or "code-review/SKILL.md")
|
|
129
|
+
content: string; // File content string
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export interface UploadResourceParams {
|
|
133
|
+
resource_id: string;
|
|
134
|
+
type: 'command' | 'skill' | 'rule' | 'mcp';
|
|
135
|
+
message: string;
|
|
136
|
+
/** Human-readable resource name sent to the CSP API. Defaults to resource_id. */
|
|
137
|
+
name?: string;
|
|
138
|
+
/** Target source repo from ai-resources-config.json (e.g. "csp", "client-sdk-ai-hub"). Defaults to default_source. */
|
|
139
|
+
target_source?: string;
|
|
140
|
+
team?: string;
|
|
141
|
+
|
|
142
|
+
// ---- Three input modes (priority: files > directory > content) ----
|
|
143
|
+
|
|
144
|
+
/** File list to upload. Paths are relative to the type subdir of the chosen source. Any file extension allowed. */
|
|
145
|
+
files: FileEntry[];
|
|
146
|
+
|
|
147
|
+
// ---- Optional fields ----
|
|
148
|
+
title?: string;
|
|
149
|
+
metadata?: Record<string, unknown>;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export interface UploadResourceResult {
|
|
153
|
+
resource_id: string;
|
|
154
|
+
version: string;
|
|
155
|
+
url: string;
|
|
156
|
+
commit_hash: string;
|
|
157
|
+
message: string;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// uninstall_resource
|
|
161
|
+
export interface UninstallResourceParams {
|
|
162
|
+
resource_id_or_name: string;
|
|
163
|
+
remove_from_account?: boolean;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export interface UninstallResourceResult {
|
|
167
|
+
success: boolean;
|
|
168
|
+
removed_resources: Array<{
|
|
169
|
+
id: string;
|
|
170
|
+
name: string;
|
|
171
|
+
path: string;
|
|
172
|
+
}>;
|
|
173
|
+
subscription_removed: boolean;
|
|
174
|
+
message: string;
|
|
175
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cursor IDE standard directory path resolver.
|
|
3
|
+
*
|
|
4
|
+
* Cursor stores user-level assets in platform-specific locations:
|
|
5
|
+
* macOS / Linux : ~/.cursor/<type>/
|
|
6
|
+
* Windows : %APPDATA%\Cursor\User\<type>\
|
|
7
|
+
* (typically C:\Users\<user>\AppData\Roaming\Cursor\User\<type>\)
|
|
8
|
+
*
|
|
9
|
+
* Resource type → subdirectory mapping mirrors the actual Cursor directory layout.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import * as os from 'os';
|
|
13
|
+
import * as path from 'path';
|
|
14
|
+
|
|
15
|
+
/** Supported Cursor resource types and their directory names. */
|
|
16
|
+
export const CURSOR_TYPE_DIRS: Record<string, string> = {
|
|
17
|
+
skill: 'skills',
|
|
18
|
+
skills: 'skills',
|
|
19
|
+
command: 'commands',
|
|
20
|
+
commands:'commands',
|
|
21
|
+
rule: 'rules',
|
|
22
|
+
rules: 'rules',
|
|
23
|
+
mcp: 'mcp-servers',
|
|
24
|
+
'mcp-servers': 'mcp-servers',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Returns the root of the Cursor user directory on the current platform.
|
|
29
|
+
*
|
|
30
|
+
* macOS / Linux : ~/.cursor
|
|
31
|
+
* Windows : %APPDATA%\Cursor\User
|
|
32
|
+
*/
|
|
33
|
+
export function getCursorRootDir(): string {
|
|
34
|
+
if (process.platform === 'win32') {
|
|
35
|
+
// APPDATA is always set on Windows; fall back to USERPROFILE as a safety net
|
|
36
|
+
const appData = process.env.APPDATA ?? path.join(os.homedir(), 'AppData', 'Roaming');
|
|
37
|
+
return path.join(appData, 'Cursor', 'User');
|
|
38
|
+
}
|
|
39
|
+
// macOS and Linux both use ~/.cursor
|
|
40
|
+
return path.join(os.homedir(), '.cursor');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Returns the Cursor subdirectory for a given resource type.
|
|
45
|
+
*
|
|
46
|
+
* @param resourceType - API resource type string (e.g. 'skill', 'command', 'rule', 'mcp')
|
|
47
|
+
* @returns Absolute path to the matching Cursor directory
|
|
48
|
+
* @throws Error if the resource type is not recognised
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* getCursorTypeDir('skill') // ~/.cursor/skills
|
|
52
|
+
* getCursorTypeDir('command') // ~/.cursor/commands
|
|
53
|
+
* getCursorTypeDir('rule') // ~/.cursor/rules
|
|
54
|
+
* getCursorTypeDir('mcp') // ~/.cursor/mcp-servers
|
|
55
|
+
*/
|
|
56
|
+
export function getCursorTypeDir(resourceType: string): string {
|
|
57
|
+
const subdir = CURSOR_TYPE_DIRS[resourceType.toLowerCase()];
|
|
58
|
+
if (!subdir) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
`Unknown resource type "${resourceType}". ` +
|
|
61
|
+
`Supported types: ${Object.keys(CURSOR_TYPE_DIRS).join(', ')}`
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
return path.join(getCursorRootDir(), subdir);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Returns the install path for a specific named resource.
|
|
69
|
+
*
|
|
70
|
+
* For directory-based resources (skill, mcp) the result is a directory:
|
|
71
|
+
* ~/.cursor/skills/<name>/
|
|
72
|
+
*
|
|
73
|
+
* For file-based resources (command, rule) the result is the file path
|
|
74
|
+
* preserving the original filename (caller should pass name with extension):
|
|
75
|
+
* ~/.cursor/commands/<name> (e.g. generate-testcase.md)
|
|
76
|
+
* ~/.cursor/rules/<name> (e.g. elliotTest.mdc)
|
|
77
|
+
*
|
|
78
|
+
* @param resourceType - Resource type string
|
|
79
|
+
* @param resourceName - Resource name (with or without extension)
|
|
80
|
+
*/
|
|
81
|
+
export function getCursorResourcePath(resourceType: string, resourceName: string): string {
|
|
82
|
+
return path.join(getCursorTypeDir(resourceType), resourceName);
|
|
83
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Log Cleanup Module
|
|
3
|
+
* Automatically deletes log files older than retention period
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as fs from 'fs';
|
|
7
|
+
import * as path from 'path';
|
|
8
|
+
import { logger } from './logger';
|
|
9
|
+
import { config } from '../config';
|
|
10
|
+
|
|
11
|
+
// Matches both the canonical name (app-YYYY-MM-DD.log) produced after the
|
|
12
|
+
// midnight rename, and the active pino-roll name (app.YYYY-MM-DD.1.log).
|
|
13
|
+
const LOG_FILE_PATTERN = /^app[.-]\d{4}-\d{2}-\d{2}[\d.]*\.log$/;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Delete log files older than retention days
|
|
17
|
+
*/
|
|
18
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
19
|
+
export async function cleanupOldLogs(): Promise<void> {
|
|
20
|
+
const logsDir = path.resolve(process.cwd(), config.logging.dir);
|
|
21
|
+
|
|
22
|
+
if (!fs.existsSync(logsDir)) {
|
|
23
|
+
logger.debug('Logs directory does not exist, skipping cleanup');
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const retentionMs = config.logging.retentionDays * 24 * 60 * 60 * 1000;
|
|
28
|
+
const now = Date.now();
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
const files = fs.readdirSync(logsDir);
|
|
32
|
+
let deletedCount = 0;
|
|
33
|
+
|
|
34
|
+
for (const file of files) {
|
|
35
|
+
const match = file.match(LOG_FILE_PATTERN);
|
|
36
|
+
if (!match) {
|
|
37
|
+
continue; // Skip non-log files
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const filePath = path.join(logsDir, file);
|
|
41
|
+
const stats = fs.statSync(filePath);
|
|
42
|
+
const fileAge = now - stats.mtimeMs;
|
|
43
|
+
|
|
44
|
+
if (fileAge > retentionMs) {
|
|
45
|
+
fs.unlinkSync(filePath);
|
|
46
|
+
deletedCount++;
|
|
47
|
+
logger.info(
|
|
48
|
+
{ file, agedays: Math.floor(fileAge / (24 * 60 * 60 * 1000)) },
|
|
49
|
+
`Deleted old log file: ${file}`
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (deletedCount > 0) {
|
|
55
|
+
logger.info(
|
|
56
|
+
{ deletedCount },
|
|
57
|
+
`Log cleanup completed: ${deletedCount} old log files deleted`
|
|
58
|
+
);
|
|
59
|
+
} else {
|
|
60
|
+
logger.debug('Log cleanup completed: no old log files to delete');
|
|
61
|
+
}
|
|
62
|
+
} catch (error) {
|
|
63
|
+
logger.error({ error }, 'Failed to cleanup old log files');
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Start log cleanup scheduler
|
|
69
|
+
* Runs cleanup once per day at 2 AM
|
|
70
|
+
*/
|
|
71
|
+
export function startLogCleanupSchedule(): NodeJS.Timeout {
|
|
72
|
+
// Run cleanup immediately on startup
|
|
73
|
+
void cleanupOldLogs();
|
|
74
|
+
|
|
75
|
+
// Schedule cleanup every 7 days
|
|
76
|
+
const interval = 7 * 24 * 60 * 60 * 1000; // 7 days
|
|
77
|
+
const timer = setInterval(() => {
|
|
78
|
+
void cleanupOldLogs();
|
|
79
|
+
}, interval);
|
|
80
|
+
|
|
81
|
+
logger.info({ retentionDays: config.logging.retentionDays }, 'Log cleanup scheduler started');
|
|
82
|
+
|
|
83
|
+
return timer;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Stop log cleanup scheduler
|
|
88
|
+
*/
|
|
89
|
+
export function stopLogCleanupSchedule(timer: NodeJS.Timeout): void {
|
|
90
|
+
clearInterval(timer);
|
|
91
|
+
logger.info('Log cleanup scheduler stopped');
|
|
92
|
+
}
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logging Module
|
|
3
|
+
* Structured logging using pino with daily file rotation.
|
|
4
|
+
*
|
|
5
|
+
* Files are named app-YYYY-MM-DD.log. Rotation is implemented by:
|
|
6
|
+
* 1. Starting pino/file pointing at today's file (fixed fd, opened at startup).
|
|
7
|
+
* 2. A midnight timer in the main thread spawns a fresh child process for the
|
|
8
|
+
* next day's file via a second pino instance — but that would mean two loggers.
|
|
9
|
+
*
|
|
10
|
+
* Practical solution used here:
|
|
11
|
+
* - Use pino-roll (daily, dateFormat: 'yyyy-MM-dd').
|
|
12
|
+
* - pino-roll produces Logs/app.YYYY-MM-DD.1.log (date + sequential counter).
|
|
13
|
+
* - At midnight + 2 s we rename the *previous* day's app.YYYY-MM-DD.1.log
|
|
14
|
+
* → app-YYYY-MM-DD.log so the canonical name is clean.
|
|
15
|
+
* - The active (today's) file keeps the pino-roll name until it rotates.
|
|
16
|
+
* - log-cleaner scans by mtime so it handles both naming conventions.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import pino from 'pino';
|
|
20
|
+
import * as path from 'path';
|
|
21
|
+
import * as fs from 'fs';
|
|
22
|
+
import { config } from '../config';
|
|
23
|
+
|
|
24
|
+
// Ensure logs directory exists (relative to project root)
|
|
25
|
+
const logsDir = path.resolve(process.cwd(), config.logging.dir);
|
|
26
|
+
if (!fs.existsSync(logsDir)) {
|
|
27
|
+
fs.mkdirSync(logsDir, { recursive: true });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** ms until the next local midnight + 1 s buffer. */
|
|
31
|
+
function msUntilMidnight(): number {
|
|
32
|
+
const now = new Date();
|
|
33
|
+
const next = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1, 0, 0, 1);
|
|
34
|
+
return next.getTime() - now.getTime();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Rename yesterday's pino-roll file (app.YYYY-MM-DD.1.log)
|
|
39
|
+
* to the canonical name (app-YYYY-MM-DD.log) once it has been rotated away.
|
|
40
|
+
*/
|
|
41
|
+
function renameYesterdayLog(): void {
|
|
42
|
+
const d = new Date();
|
|
43
|
+
d.setDate(d.getDate() - 1);
|
|
44
|
+
const dateStr = [
|
|
45
|
+
d.getFullYear(),
|
|
46
|
+
String(d.getMonth() + 1).padStart(2, '0'),
|
|
47
|
+
String(d.getDate()).padStart(2, '0'),
|
|
48
|
+
].join('-');
|
|
49
|
+
|
|
50
|
+
const src = path.join(logsDir, `app.${dateStr}.1.log`);
|
|
51
|
+
const dst = path.join(logsDir, `app-${dateStr}.log`);
|
|
52
|
+
if (fs.existsSync(src) && !fs.existsSync(dst)) {
|
|
53
|
+
try { fs.renameSync(src, dst); } catch { /* non-fatal */ }
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Fire rename at midnight + 2 s, then every 24 h.
|
|
58
|
+
setTimeout(() => {
|
|
59
|
+
renameYesterdayLog();
|
|
60
|
+
setInterval(renameYesterdayLog, 24 * 60 * 60 * 1000).unref();
|
|
61
|
+
}, msUntilMidnight() + 2000).unref();
|
|
62
|
+
|
|
63
|
+
// Create pino logger with multi-target transport
|
|
64
|
+
export const logger = pino({
|
|
65
|
+
level: config.logLevel,
|
|
66
|
+
timestamp: pino.stdTimeFunctions.isoTime,
|
|
67
|
+
base: {
|
|
68
|
+
service: 'csp-ai-agent-mcp',
|
|
69
|
+
},
|
|
70
|
+
transport: {
|
|
71
|
+
targets: [
|
|
72
|
+
// Console output (pretty format in development)
|
|
73
|
+
{
|
|
74
|
+
target: 'pino-pretty',
|
|
75
|
+
level: config.logLevel,
|
|
76
|
+
options: {
|
|
77
|
+
colorize: true,
|
|
78
|
+
translateTime: 'SYS:standard',
|
|
79
|
+
ignore: 'pid,hostname',
|
|
80
|
+
singleLine: false,
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
// Daily-rotating file output.
|
|
84
|
+
// Active file: Logs/app.YYYY-MM-DD.1.log
|
|
85
|
+
// After midnight rename: Logs/app-YYYY-MM-DD.log
|
|
86
|
+
{
|
|
87
|
+
target: 'pino-roll',
|
|
88
|
+
level: config.logLevel,
|
|
89
|
+
options: {
|
|
90
|
+
file: path.join(logsDir, 'app'),
|
|
91
|
+
frequency: 'daily',
|
|
92
|
+
dateFormat: 'yyyy-MM-dd',
|
|
93
|
+
mkdir: true,
|
|
94
|
+
sync: false,
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
],
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Log MCP Tool call
|
|
103
|
+
*/
|
|
104
|
+
export function logToolCall(
|
|
105
|
+
toolName: string,
|
|
106
|
+
userId: string,
|
|
107
|
+
params: Record<string, unknown>,
|
|
108
|
+
durationMs: number
|
|
109
|
+
): void {
|
|
110
|
+
logger.info(
|
|
111
|
+
{
|
|
112
|
+
type: 'tool_call',
|
|
113
|
+
toolName,
|
|
114
|
+
userId,
|
|
115
|
+
params,
|
|
116
|
+
durationMs,
|
|
117
|
+
},
|
|
118
|
+
`Tool ${toolName} called by ${userId} (${durationMs}ms)`
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Log error with context
|
|
124
|
+
*/
|
|
125
|
+
export function logError(error: Error, context?: Record<string, unknown>): void {
|
|
126
|
+
logger.error(
|
|
127
|
+
{
|
|
128
|
+
type: 'error',
|
|
129
|
+
error: {
|
|
130
|
+
message: error.message,
|
|
131
|
+
stack: error.stack,
|
|
132
|
+
name: error.name,
|
|
133
|
+
},
|
|
134
|
+
...context,
|
|
135
|
+
},
|
|
136
|
+
error.message
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Log performance metrics
|
|
142
|
+
*/
|
|
143
|
+
export function logPerformance(
|
|
144
|
+
operation: string,
|
|
145
|
+
durationMs: number,
|
|
146
|
+
metadata?: Record<string, unknown>
|
|
147
|
+
): void {
|
|
148
|
+
logger.info(
|
|
149
|
+
{
|
|
150
|
+
type: 'performance',
|
|
151
|
+
operation,
|
|
152
|
+
durationMs,
|
|
153
|
+
...metadata,
|
|
154
|
+
},
|
|
155
|
+
`${operation} completed in ${durationMs}ms`
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Log API request with detailed information
|
|
161
|
+
*/
|
|
162
|
+
export function logApiRequest(
|
|
163
|
+
method: string,
|
|
164
|
+
url: string,
|
|
165
|
+
statusCode: number,
|
|
166
|
+
durationMs: number,
|
|
167
|
+
requestData?: unknown,
|
|
168
|
+
responseData?: unknown,
|
|
169
|
+
headers?: Record<string, string>
|
|
170
|
+
): void {
|
|
171
|
+
logger.info(
|
|
172
|
+
{
|
|
173
|
+
type: 'api_request',
|
|
174
|
+
method,
|
|
175
|
+
url,
|
|
176
|
+
statusCode,
|
|
177
|
+
durationMs,
|
|
178
|
+
requestData: requestData ? JSON.stringify(requestData).substring(0, 500) : undefined,
|
|
179
|
+
responseData: responseData ? JSON.stringify(responseData).substring(0, 1000) : undefined,
|
|
180
|
+
headers: headers ? sanitizeHeaders(headers) : undefined,
|
|
181
|
+
},
|
|
182
|
+
`${method} ${url} - ${statusCode} (${durationMs}ms)`
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Log API error with full details
|
|
188
|
+
*/
|
|
189
|
+
export function logApiError(
|
|
190
|
+
method: string,
|
|
191
|
+
url: string,
|
|
192
|
+
error: Error,
|
|
193
|
+
requestData?: unknown,
|
|
194
|
+
statusCode?: number
|
|
195
|
+
): void {
|
|
196
|
+
logger.error(
|
|
197
|
+
{
|
|
198
|
+
type: 'api_error',
|
|
199
|
+
method,
|
|
200
|
+
url,
|
|
201
|
+
statusCode,
|
|
202
|
+
requestData: requestData ? JSON.stringify(requestData).substring(0, 500) : undefined,
|
|
203
|
+
error: {
|
|
204
|
+
message: error.message,
|
|
205
|
+
stack: error.stack,
|
|
206
|
+
name: error.name,
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
`API Error: ${method} ${url} - ${error.message}`
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Log tool execution step
|
|
215
|
+
*/
|
|
216
|
+
export function logToolStep(
|
|
217
|
+
toolName: string,
|
|
218
|
+
step: string,
|
|
219
|
+
details?: Record<string, unknown>
|
|
220
|
+
): void {
|
|
221
|
+
logger.debug(
|
|
222
|
+
{
|
|
223
|
+
type: 'tool_step',
|
|
224
|
+
toolName,
|
|
225
|
+
step,
|
|
226
|
+
...details,
|
|
227
|
+
},
|
|
228
|
+
`[${toolName}] ${step}`
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Log tool execution result
|
|
234
|
+
*/
|
|
235
|
+
export function logToolResult(
|
|
236
|
+
toolName: string,
|
|
237
|
+
success: boolean,
|
|
238
|
+
result?: unknown,
|
|
239
|
+
error?: Error
|
|
240
|
+
): void {
|
|
241
|
+
const level = success ? 'info' : 'error';
|
|
242
|
+
logger[level](
|
|
243
|
+
{
|
|
244
|
+
type: 'tool_result',
|
|
245
|
+
toolName,
|
|
246
|
+
success,
|
|
247
|
+
result: result ? JSON.stringify(result).substring(0, 1000) : undefined,
|
|
248
|
+
error: error ? {
|
|
249
|
+
message: error.message,
|
|
250
|
+
stack: error.stack,
|
|
251
|
+
name: error.name,
|
|
252
|
+
} : undefined,
|
|
253
|
+
},
|
|
254
|
+
`[${toolName}] ${success ? 'Success' : 'Failed'}`
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Log authentication attempt
|
|
260
|
+
*/
|
|
261
|
+
export function logAuthAttempt(
|
|
262
|
+
type: 'token_validation' | 'permission_check',
|
|
263
|
+
success: boolean,
|
|
264
|
+
details?: Record<string, unknown>
|
|
265
|
+
): void {
|
|
266
|
+
const level = success ? 'info' : 'warn';
|
|
267
|
+
logger[level](
|
|
268
|
+
{
|
|
269
|
+
type: 'auth',
|
|
270
|
+
operation: type,
|
|
271
|
+
success,
|
|
272
|
+
...details,
|
|
273
|
+
},
|
|
274
|
+
`Auth ${type}: ${success ? 'Success' : 'Failed'}`
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Log cache operation
|
|
280
|
+
*/
|
|
281
|
+
export function logCacheOperation(
|
|
282
|
+
operation: 'get' | 'set' | 'delete' | 'hit' | 'miss',
|
|
283
|
+
key: string,
|
|
284
|
+
details?: Record<string, unknown>
|
|
285
|
+
): void {
|
|
286
|
+
logger.debug(
|
|
287
|
+
{
|
|
288
|
+
type: 'cache',
|
|
289
|
+
operation,
|
|
290
|
+
key,
|
|
291
|
+
...details,
|
|
292
|
+
},
|
|
293
|
+
`Cache ${operation}: ${key}`
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Sanitize headers to remove sensitive information
|
|
299
|
+
*/
|
|
300
|
+
function sanitizeHeaders(headers: Record<string, string>): Record<string, string> {
|
|
301
|
+
const sanitized = { ...headers };
|
|
302
|
+
|
|
303
|
+
// Mask Authorization header
|
|
304
|
+
if (sanitized['Authorization'] || sanitized['authorization']) {
|
|
305
|
+
const authKey = sanitized['Authorization'] ? 'Authorization' : 'authorization';
|
|
306
|
+
const authValue = sanitized[authKey];
|
|
307
|
+
if (authValue && authValue.startsWith('Bearer ')) {
|
|
308
|
+
const token = authValue.substring(7);
|
|
309
|
+
sanitized[authKey] = `Bearer ${token.substring(0, 10)}...${token.substring(token.length - 10)}`;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return sanitized;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Log Git operation
|
|
318
|
+
*/
|
|
319
|
+
export function logGitOperation(
|
|
320
|
+
operation: string,
|
|
321
|
+
details: Record<string, unknown>,
|
|
322
|
+
durationMs: number
|
|
323
|
+
): void {
|
|
324
|
+
logger.info(
|
|
325
|
+
{
|
|
326
|
+
type: 'git_operation',
|
|
327
|
+
operation,
|
|
328
|
+
...details,
|
|
329
|
+
durationMs,
|
|
330
|
+
},
|
|
331
|
+
`Git ${operation} completed (${durationMs}ms)`
|
|
332
|
+
);
|
|
333
|
+
}
|