codeninja 2.0.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/.gitattributes +11 -0
- package/README.md +293 -0
- package/agent/database-agent.md +504 -0
- package/agent/designs/README.md +10 -0
- package/agent/global-agent.md +236 -0
- package/agent/nodejs-agent.md +406 -0
- package/agent/reactjs-agent.md +260 -0
- package/cli.js +352 -0
- package/commands/audit.workflow.md +111 -0
- package/commands/create-api.workflow.md +99 -0
- package/commands/db-add-index.workflow.md +97 -0
- package/commands/db-create-table.workflow.md +132 -0
- package/commands/db-drop-table.workflow.md +103 -0
- package/commands/db-modify-table.workflow.md +159 -0
- package/commands/db-seed.workflow.md +99 -0
- package/commands/db-sync.workflow.md +100 -0
- package/commands/design.workflow.md +66 -0
- package/commands/initialize-project.workflow.md +500 -0
- package/commands/integrate-api.workflow.md +448 -0
- package/commands/modularize.workflow.md +329 -0
- package/commands/refactor.workflow.md +70 -0
- package/commands/sync.workflow.md +962 -0
- package/commands/test.workflow.md +40 -0
- package/commands/validate-page.workflow.md +543 -0
- package/mcp-server.js +842 -0
- package/package.json +24 -0
- package/tasks/README.md +283 -0
- package/tasks/add-health-route.task.md +103 -0
- package/tasks/ask-api-integration-scope.task.md +34 -0
- package/tasks/ask-api-key.task.md +23 -0
- package/tasks/ask-api-version.task.md +28 -0
- package/tasks/ask-client-type.task.md +24 -0
- package/tasks/ask-column-enum-values.task.md +51 -0
- package/tasks/ask-column-is-enum.task.md +39 -0
- package/tasks/ask-column-name.task.md +39 -0
- package/tasks/ask-column-position.task.md +39 -0
- package/tasks/ask-column-type.task.md +59 -0
- package/tasks/ask-database-config.task.md +66 -0
- package/tasks/ask-database-host.task.md +16 -0
- package/tasks/ask-database-name.task.md +18 -0
- package/tasks/ask-database-port.task.md +23 -0
- package/tasks/ask-database-type.task.md +30 -0
- package/tasks/ask-database-user.task.md +14 -0
- package/tasks/ask-design-description.task.md +16 -0
- package/tasks/ask-design-target.task.md +24 -0
- package/tasks/ask-encrypted-transport.task.md +25 -0
- package/tasks/ask-encryption-iv.task.md +23 -0
- package/tasks/ask-encryption-key.task.md +23 -0
- package/tasks/ask-feature-name.task.md +20 -0
- package/tasks/ask-http-method.task.md +21 -0
- package/tasks/ask-index-columns.task.md +46 -0
- package/tasks/ask-index-file-placement.task.md +33 -0
- package/tasks/ask-index-sort-order.task.md +37 -0
- package/tasks/ask-index-type.task.md +42 -0
- package/tasks/ask-init-mode.task.md +28 -0
- package/tasks/ask-linked-service.task.md +57 -0
- package/tasks/ask-modify-operation.task.md +36 -0
- package/tasks/ask-modularize-scope.task.md +31 -0
- package/tasks/ask-module-name.task.md +30 -0
- package/tasks/ask-new-column-name.task.md +21 -0
- package/tasks/ask-new-table-name.task.md +22 -0
- package/tasks/ask-old-column-name.task.md +22 -0
- package/tasks/ask-package-author.task.md +16 -0
- package/tasks/ask-package-name.task.md +23 -0
- package/tasks/ask-page-path.task.md +40 -0
- package/tasks/ask-primary-table.task.md +30 -0
- package/tasks/ask-project-figma.task.md +71 -0
- package/tasks/ask-project-info-doc.task.md +57 -0
- package/tasks/ask-project-scope-of-work.task.md +57 -0
- package/tasks/ask-project-type.task.md +24 -0
- package/tasks/ask-react-target-service.task.md +32 -0
- package/tasks/ask-redis-config.task.md +42 -0
- package/tasks/ask-redis-host.task.md +16 -0
- package/tasks/ask-redis-port.task.md +18 -0
- package/tasks/ask-refactor-type.task.md +26 -0
- package/tasks/ask-requires-auth.task.md +22 -0
- package/tasks/ask-response-mode.task.md +38 -0
- package/tasks/ask-route-description.task.md +20 -0
- package/tasks/ask-route-path.task.md +29 -0
- package/tasks/ask-seed-row-values.task.md +42 -0
- package/tasks/ask-seed-rows-count.task.md +22 -0
- package/tasks/ask-service-description.task.md +16 -0
- package/tasks/ask-service-name.task.md +27 -0
- package/tasks/ask-service-port.task.md +24 -0
- package/tasks/ask-supported-languages.task.md +40 -0
- package/tasks/ask-table-file-number.task.md +36 -0
- package/tasks/ask-table-indexes.task.md +47 -0
- package/tasks/ask-table-name.task.md +32 -0
- package/tasks/ask-table-needs-soft-delete.task.md +29 -0
- package/tasks/ask-table-needs-status.task.md +30 -0
- package/tasks/ask-table-purpose.task.md +28 -0
- package/tasks/ask-table-seed-data.task.md +44 -0
- package/tasks/ask-target-service.task.md +32 -0
- package/tasks/ask-test-type.task.md +20 -0
- package/tasks/ask-validation-library.task.md +38 -0
- package/tasks/detect-repository-state.task.md +92 -0
- package/tasks/generate-app.task.md +146 -0
- package/tasks/generate-common.task.md +330 -0
- package/tasks/generate-constants.task.md +123 -0
- package/tasks/generate-database.task.md +168 -0
- package/tasks/generate-docker-compose.task.md +298 -0
- package/tasks/generate-dockerfile.task.md +126 -0
- package/tasks/generate-dockerignore.task.md +123 -0
- package/tasks/generate-enc-dec-html.task.md +127 -0
- package/tasks/generate-enc-dec-php.task.md +145 -0
- package/tasks/generate-encryption.task.md +159 -0
- package/tasks/generate-fast-defaults.task.md +68 -0
- package/tasks/generate-gitignore.task.md +79 -0
- package/tasks/generate-headerValidator.task.md +377 -0
- package/tasks/generate-ide-configs.task.md +114 -0
- package/tasks/generate-ioRedis.task.md +120 -0
- package/tasks/generate-language-en.task.md +155 -0
- package/tasks/generate-logging.task.md +257 -0
- package/tasks/generate-model.task.md +180 -0
- package/tasks/generate-notification.task.md +251 -0
- package/tasks/generate-package-json.task.md +114 -0
- package/tasks/generate-rateLimiter.task.md +125 -0
- package/tasks/generate-react-api-client.task.md +169 -0
- package/tasks/generate-react-api-handler.task.md +102 -0
- package/tasks/generate-react-app-jsx.task.md +56 -0
- package/tasks/generate-react-dockerfile.task.md +175 -0
- package/tasks/generate-react-env.task.md +58 -0
- package/tasks/generate-react-gitignore.task.md +49 -0
- package/tasks/generate-react-htaccess.task.md +54 -0
- package/tasks/generate-react-index-html.task.md +53 -0
- package/tasks/generate-react-index-jsx.task.md +51 -0
- package/tasks/generate-react-package-json.task.md +77 -0
- package/tasks/generate-react-welcome-page.task.md +71 -0
- package/tasks/generate-readme.task.md +160 -0
- package/tasks/generate-response.task.md +202 -0
- package/tasks/generate-route-manager.task.md +173 -0
- package/tasks/generate-route.task.md +203 -0
- package/tasks/generate-swagger.task.md +290 -0
- package/tasks/generate-tbl-user-deviceinfo.task.md +75 -0
- package/tasks/generate-template.task.md +129 -0
- package/tasks/generate-validator.task.md +122 -0
- package/tasks/show-db-table-summary.task.md +66 -0
- package/tasks/show-final-summary.task.md +108 -0
- package/tasks/show-init-summary.task.md +257 -0
- package/tasks/write-context.task.md +314 -0
package/mcp-server.js
ADDED
|
@@ -0,0 +1,842 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Code Ninja MCP Server
|
|
5
|
+
* Execution layer for the Code Ninja agent system.
|
|
6
|
+
* Owns: context management, filesystem intelligence,
|
|
7
|
+
* surgical file edits, code analysis, and environment validation.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const { Server } = require('@modelcontextprotocol/sdk/server/index.js');
|
|
11
|
+
const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');
|
|
12
|
+
const { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema } = require('@modelcontextprotocol/sdk/types.js');
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const { execSync } = require('child_process');
|
|
16
|
+
|
|
17
|
+
const server = new Server(
|
|
18
|
+
{ name: 'codeninja', version: '2.0.0' },
|
|
19
|
+
{ capabilities: { tools: {}, resources: {} } }
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
// ─── Path Helpers ─────────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
// mcp-server.js lives at <project-root>/.codeninja/mcp-server.js
|
|
25
|
+
// __dirname = <project-root>/.codeninja
|
|
26
|
+
// path.dirname(__dirname) = <project-root> ← always correct regardless of IDE CWD
|
|
27
|
+
//
|
|
28
|
+
// We deliberately do NOT use process.cwd() here. IDEs launch the MCP server
|
|
29
|
+
// with their own working directory, which is NOT the project root. Using
|
|
30
|
+
// process.cwd() caused context.json to be written into the IDE's installation
|
|
31
|
+
// folder. __dirname is always relative to this file's actual location on disk.
|
|
32
|
+
const ROOT = path.dirname(__dirname);
|
|
33
|
+
const CONTEXT_DIR = path.join(ROOT, '.codeninja', 'context');
|
|
34
|
+
const CONTEXT_FILE = path.join(CONTEXT_DIR, 'context.json');
|
|
35
|
+
|
|
36
|
+
function abs(rel) { return path.resolve(ROOT, rel); }
|
|
37
|
+
|
|
38
|
+
function readJSON(filePath) {
|
|
39
|
+
const full = abs(filePath);
|
|
40
|
+
if (!fs.existsSync(full)) return null;
|
|
41
|
+
return JSON.parse(fs.readFileSync(full, 'utf8')); // ← throws on bad JSON
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function writeJSON(filePath, obj) {
|
|
45
|
+
const full = abs(filePath);
|
|
46
|
+
fs.mkdirSync(path.dirname(full), { recursive: true });
|
|
47
|
+
fs.writeFileSync(full, JSON.stringify(obj, null, 2), 'utf8');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function deepMerge(target, source) {
|
|
51
|
+
for (const key of Object.keys(source)) {
|
|
52
|
+
if (
|
|
53
|
+
source[key] !== null &&
|
|
54
|
+
typeof source[key] === 'object' &&
|
|
55
|
+
!Array.isArray(source[key]) &&
|
|
56
|
+
target[key] !== null &&
|
|
57
|
+
typeof target[key] === 'object' &&
|
|
58
|
+
!Array.isArray(target[key])
|
|
59
|
+
) {
|
|
60
|
+
deepMerge(target[key], source[key]);
|
|
61
|
+
} else {
|
|
62
|
+
target[key] = source[key];
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return target;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const EMPTY_CONTEXT = {
|
|
69
|
+
context_version: 0,
|
|
70
|
+
project_name: '',
|
|
71
|
+
initialized_at: '',
|
|
72
|
+
last_updated_at: '',
|
|
73
|
+
last_command: '',
|
|
74
|
+
last_command_at: '',
|
|
75
|
+
repository_state: '',
|
|
76
|
+
has_context_file: false,
|
|
77
|
+
detected_services: [],
|
|
78
|
+
detected_db: false,
|
|
79
|
+
project_info: {
|
|
80
|
+
has_doc: false, doc_url: '', doc_source: '', doc_content: '',
|
|
81
|
+
has_sow: false, sow_url: '', sow_source: '', sow_content: '',
|
|
82
|
+
has_figma: false, figma_url: '', figma_accessible: false, figma_screens: '',
|
|
83
|
+
summary: '', detected_entities: [],
|
|
84
|
+
from_doc: { project_name: '', domain: '', purpose: '', features: [], entities: [], tech_preferences: [] },
|
|
85
|
+
from_sow: { phases: [], deliverables: [], apis_expected: [], tables_expected: [], integrations: [], constraints: [], timeline: '' },
|
|
86
|
+
from_figma: { screens: [], components_hinted: [], color_theme: '' }
|
|
87
|
+
},
|
|
88
|
+
db: {
|
|
89
|
+
type: '', name: '', host: '', port: 0, user: '',
|
|
90
|
+
schema: { tables: {}, change_log: [] }
|
|
91
|
+
},
|
|
92
|
+
services: {},
|
|
93
|
+
api_routes: [],
|
|
94
|
+
designs: [],
|
|
95
|
+
change_log: [],
|
|
96
|
+
current_init: {}, current_api: {}, current_action: {},
|
|
97
|
+
current_refactor: {}, current_design: {}, current_test: {}, current_db: {}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// ─── Tool Definitions ─────────────────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
103
|
+
tools: [
|
|
104
|
+
|
|
105
|
+
// ── CATEGORY 1: Context Engine ────────────────────────────────────────────
|
|
106
|
+
|
|
107
|
+
{
|
|
108
|
+
name: 'context_read',
|
|
109
|
+
description: 'Reads context.json and returns the full parsed object. Returns the empty schema if file does not exist. Always call this at agent activation instead of reading the file manually.',
|
|
110
|
+
inputSchema: { type: 'object', properties: {}, required: [] }
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
name: 'context_write',
|
|
114
|
+
description: 'Merges provided updates into context.json. Reads existing file first, deep-merges updates on top, auto-increments context_version, sets timestamps, writes atomically. This is the ONLY way context.json should ever be written.',
|
|
115
|
+
inputSchema: {
|
|
116
|
+
type: 'object',
|
|
117
|
+
properties: {
|
|
118
|
+
updates: { type: 'object', description: 'Keys to merge into existing context. Supports deep merge for nested objects.' },
|
|
119
|
+
operation: { type: 'string', description: 'The command that triggered this write. e.g. "initialize-project", "create-api". Stored in last_command.' }
|
|
120
|
+
},
|
|
121
|
+
required: ['updates', 'operation']
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
name: 'context_clear_scratchpad',
|
|
126
|
+
description: 'Clears one or more current_* scratchpad keys in context.json by setting them to {}. Call at the end of every workflow to clean up in-progress state.',
|
|
127
|
+
inputSchema: {
|
|
128
|
+
type: 'object',
|
|
129
|
+
properties: {
|
|
130
|
+
keys: { type: 'array', items: { type: 'string' }, description: 'Array of current_* keys to clear. e.g. ["current_init", "current_api"]' }
|
|
131
|
+
},
|
|
132
|
+
required: ['keys']
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
name: 'context_check_stale',
|
|
137
|
+
description: 'Checks if any current_* scratchpad key is non-empty and last_command_at is older than the threshold. Returns stale keys with their content and age. Use at activation to detect interrupted workflows.',
|
|
138
|
+
inputSchema: {
|
|
139
|
+
type: 'object',
|
|
140
|
+
properties: {
|
|
141
|
+
threshold_minutes: { type: 'number', description: 'Minutes threshold to consider a key stale. Default: 10.' }
|
|
142
|
+
},
|
|
143
|
+
required: []
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
|
|
147
|
+
// ── CATEGORY 2: Filesystem Intelligence ──────────────────────────────────
|
|
148
|
+
|
|
149
|
+
{
|
|
150
|
+
name: 'fs_exists',
|
|
151
|
+
description: 'Checks whether a file or directory exists at a path relative to project root. Returns { exists, type } where type is "file", "directory", or null.',
|
|
152
|
+
inputSchema: {
|
|
153
|
+
type: 'object',
|
|
154
|
+
properties: {
|
|
155
|
+
path: { type: 'string', description: 'Path relative to project root.' }
|
|
156
|
+
},
|
|
157
|
+
required: ['path']
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
name: 'fs_list',
|
|
162
|
+
description: 'Lists files in a directory relative to project root. Optionally filter by extension. Returns array of filenames sorted alphabetically. Returns empty array if directory does not exist.',
|
|
163
|
+
inputSchema: {
|
|
164
|
+
type: 'object',
|
|
165
|
+
properties: {
|
|
166
|
+
path: { type: 'string', description: 'Directory path relative to project root.' },
|
|
167
|
+
extension: { type: 'string', description: 'Optional. Filter by extension e.g. ".sql", ".js". Include the dot.' }
|
|
168
|
+
},
|
|
169
|
+
required: ['path']
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
name: 'fs_read',
|
|
174
|
+
description: 'Reads a file and returns its contents as a string. Use for reading generated files during drift detection, audit, or sync. Returns null if file does not exist.',
|
|
175
|
+
inputSchema: {
|
|
176
|
+
type: 'object',
|
|
177
|
+
properties: {
|
|
178
|
+
path: { type: 'string', description: 'File path relative to project root.' }
|
|
179
|
+
},
|
|
180
|
+
required: ['path']
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
name: 'migration_next_number',
|
|
185
|
+
description: 'Scans the migrations directory for a given db_type and returns the next available integer file number. Parses the numeric prefix from existing filenames. Returns 1 if directory is empty.',
|
|
186
|
+
inputSchema: {
|
|
187
|
+
type: 'object',
|
|
188
|
+
properties: {
|
|
189
|
+
db_type: { type: 'string', description: 'postgresql, mysql, or mongodb.' }
|
|
190
|
+
},
|
|
191
|
+
required: ['db_type']
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
name: 'service_scan',
|
|
196
|
+
description: 'Scans the project root for all service directories (folders containing app.js or package.json, excluding node_modules). Returns array of { name, type, has_env, has_modules } objects. Powers detect-repository-state and @sync.',
|
|
197
|
+
inputSchema: { type: 'object', properties: {}, required: [] }
|
|
198
|
+
},
|
|
199
|
+
|
|
200
|
+
// ── CATEGORY 3: Surgical Editor ───────────────────────────────────────────
|
|
201
|
+
|
|
202
|
+
{
|
|
203
|
+
name: 'file_insert_after',
|
|
204
|
+
description: 'Inserts a line of text immediately after the last occurrence of an anchor string in a file. Used for route_manager.js module registration and create-schema.sql \\i entries. Never rewrites the whole file.',
|
|
205
|
+
inputSchema: {
|
|
206
|
+
type: 'object',
|
|
207
|
+
properties: {
|
|
208
|
+
path: { type: 'string', description: 'File path relative to project root.' },
|
|
209
|
+
anchor: { type: 'string', description: 'Find the last occurrence of this string and insert after it.' },
|
|
210
|
+
line: { type: 'string', description: 'The line to insert. Do not include a trailing newline.' }
|
|
211
|
+
},
|
|
212
|
+
required: ['path', 'anchor', 'line']
|
|
213
|
+
}
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
name: 'file_contains',
|
|
217
|
+
description: 'Checks whether a file contains a specific string. Use before file_insert_after to implement duplicate detection — e.g. check if a module is already registered in route_manager.js before inserting.',
|
|
218
|
+
inputSchema: {
|
|
219
|
+
type: 'object',
|
|
220
|
+
properties: {
|
|
221
|
+
path: { type: 'string', description: 'File path relative to project root.' },
|
|
222
|
+
search: { type: 'string', description: 'String to search for.' }
|
|
223
|
+
},
|
|
224
|
+
required: ['path', 'search']
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
|
|
228
|
+
// ── CATEGORY 4: Code Intelligence ─────────────────────────────────────────
|
|
229
|
+
|
|
230
|
+
{
|
|
231
|
+
name: 'analyze_middleware_order',
|
|
232
|
+
description: 'Reads a route_manager.js file and extracts the ordered list of router.use() middleware registrations. Returns array of { middleware, position, has_asyncHandler } objects. Powers drift detection Check 1.',
|
|
233
|
+
inputSchema: {
|
|
234
|
+
type: 'object',
|
|
235
|
+
properties: {
|
|
236
|
+
service: { type: 'string', description: 'Service name. Reads <service>/modules/v1/route_manager.js.' }
|
|
237
|
+
},
|
|
238
|
+
required: ['service']
|
|
239
|
+
}
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
name: 'analyze_encryption_library',
|
|
243
|
+
description: 'Reads encryption.js for a service and detects which encryption library is imported (crypto-js, cryptlib, or neither/both). Returns { library, is_consistent } against the expected client_type from context.',
|
|
244
|
+
inputSchema: {
|
|
245
|
+
type: 'object',
|
|
246
|
+
properties: {
|
|
247
|
+
service: { type: 'string', description: 'Service name.' },
|
|
248
|
+
expected_client_type: { type: 'string', description: 'reactjs or app — the expected library based on context.' }
|
|
249
|
+
},
|
|
250
|
+
required: ['service', 'expected_client_type']
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
name: 'analyze_language_keys',
|
|
255
|
+
description: 'Reads all language files in a service\'s languages/ directory and compares their keys against en.js. Returns { missing_per_file, extra_per_file, total_keys_en } for drift detection Check 5.',
|
|
256
|
+
inputSchema: {
|
|
257
|
+
type: 'object',
|
|
258
|
+
properties: {
|
|
259
|
+
service: { type: 'string', description: 'Service name.' }
|
|
260
|
+
},
|
|
261
|
+
required: ['service']
|
|
262
|
+
}
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
name: 'analyze_dependencies',
|
|
266
|
+
description: 'Reads package.json for a service and compares installed dependencies against expected packages for the given client_type and db_type. Returns { missing, unexpected, present } arrays.',
|
|
267
|
+
inputSchema: {
|
|
268
|
+
type: 'object',
|
|
269
|
+
properties: {
|
|
270
|
+
service: { type: 'string', description: 'Service name.' },
|
|
271
|
+
client_type: { type: 'string', description: 'reactjs or app.' },
|
|
272
|
+
db_type: { type: 'string', description: 'postgresql, mysql, or mongodb.' }
|
|
273
|
+
},
|
|
274
|
+
required: ['service', 'client_type', 'db_type']
|
|
275
|
+
}
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
name: 'analyze_env_file',
|
|
279
|
+
description: 'Reads and parses a .env file for a service. Returns key-value pairs as an object. Sensitive values (KEY, IV, API_KEY, PASSWORD) are masked as "***" in the output. Use for drift detection and sync.',
|
|
280
|
+
inputSchema: {
|
|
281
|
+
type: 'object',
|
|
282
|
+
properties: {
|
|
283
|
+
service: { type: 'string', description: 'Service name.' }
|
|
284
|
+
},
|
|
285
|
+
required: ['service']
|
|
286
|
+
}
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
name: 'run_drift_check',
|
|
290
|
+
description: 'Runs all 7 drift detection checks for a service in one call. Returns a structured drift report with DRIFT items categorized by check number. Powers @sync Phase 7. Equivalent to running all analyze_* tools at once.',
|
|
291
|
+
inputSchema: {
|
|
292
|
+
type: 'object',
|
|
293
|
+
properties: {
|
|
294
|
+
service: { type: 'string', description: 'Service name to check.' }
|
|
295
|
+
},
|
|
296
|
+
required: ['service']
|
|
297
|
+
}
|
|
298
|
+
},
|
|
299
|
+
|
|
300
|
+
// ── CATEGORY 5: Environment Tools ─────────────────────────────────────────
|
|
301
|
+
|
|
302
|
+
{
|
|
303
|
+
name: 'npm_check_package',
|
|
304
|
+
description: 'Queries the npm registry to verify a package exists and returns its latest version and description. Use before adding a dependency to confirm the package name is correct.',
|
|
305
|
+
inputSchema: {
|
|
306
|
+
type: 'object',
|
|
307
|
+
properties: {
|
|
308
|
+
package_name: { type: 'string', description: 'npm package name to look up.' }
|
|
309
|
+
},
|
|
310
|
+
required: ['package_name']
|
|
311
|
+
}
|
|
312
|
+
},
|
|
313
|
+
{
|
|
314
|
+
name: 'npm_install',
|
|
315
|
+
description: 'Runs npm install in a service directory. Call after generating package.json so dependencies are ready immediately. Returns stdout/stderr output.',
|
|
316
|
+
inputSchema: {
|
|
317
|
+
type: 'object',
|
|
318
|
+
properties: {
|
|
319
|
+
service: { type: 'string', description: 'Service directory name.' }
|
|
320
|
+
},
|
|
321
|
+
required: ['service']
|
|
322
|
+
}
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
name: 'validate_redis_connection',
|
|
326
|
+
description: 'Attempts to connect to Redis at the given host and port and sends a PING command. Returns { reachable, latency_ms, error }. Use before finalizing .env to confirm Redis is available.',
|
|
327
|
+
inputSchema: {
|
|
328
|
+
type: 'object',
|
|
329
|
+
properties: {
|
|
330
|
+
host: { type: 'string', description: 'Redis host.' },
|
|
331
|
+
port: { type: 'number', description: 'Redis port.' }
|
|
332
|
+
},
|
|
333
|
+
required: ['host', 'port']
|
|
334
|
+
}
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
name: 'validate_postgres_connection',
|
|
338
|
+
description: 'Attempts to connect to PostgreSQL with the given credentials and runs SELECT 1. Returns { reachable, version, error }. Use before finalizing .env to confirm database is available.',
|
|
339
|
+
inputSchema: {
|
|
340
|
+
type: 'object',
|
|
341
|
+
properties: {
|
|
342
|
+
host: { type: 'string' },
|
|
343
|
+
port: { type: 'number' },
|
|
344
|
+
database: { type: 'string' },
|
|
345
|
+
user: { type: 'string' },
|
|
346
|
+
password: { type: 'string' }
|
|
347
|
+
},
|
|
348
|
+
required: ['host', 'port', 'database', 'user']
|
|
349
|
+
}
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
name: 'lint_file',
|
|
353
|
+
description: 'Runs basic structural checks on a generated JavaScript file. Checks for: syntax errors (via node --check), missing module.exports, direct res.json() calls in model files, express imports in model files. Returns { valid, issues[] }.',
|
|
354
|
+
inputSchema: {
|
|
355
|
+
type: 'object',
|
|
356
|
+
properties: {
|
|
357
|
+
path: { type: 'string', description: 'File path relative to project root.' },
|
|
358
|
+
file_type: { type: 'string', description: 'model, route, middleware, or utility. Determines which checks to apply.' }
|
|
359
|
+
},
|
|
360
|
+
required: ['path', 'file_type']
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
]
|
|
365
|
+
}));
|
|
366
|
+
|
|
367
|
+
// ─── Resources ────────────────────────────────────────────────────────────────
|
|
368
|
+
// Resources are readable data sources the agent can subscribe to.
|
|
369
|
+
|
|
370
|
+
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
|
|
371
|
+
resources: [
|
|
372
|
+
{
|
|
373
|
+
uri: 'codeninja://context',
|
|
374
|
+
name: 'Project Context',
|
|
375
|
+
description: 'The live context.json — full project state including services, db schema, api routes, and change log.',
|
|
376
|
+
mimeType: 'application/json'
|
|
377
|
+
},
|
|
378
|
+
{
|
|
379
|
+
uri: 'codeninja://services',
|
|
380
|
+
name: 'Discovered Services',
|
|
381
|
+
description: 'All service directories found on disk with their detected properties.',
|
|
382
|
+
mimeType: 'application/json'
|
|
383
|
+
},
|
|
384
|
+
{
|
|
385
|
+
uri: 'codeninja://migrations',
|
|
386
|
+
name: 'Migration Files',
|
|
387
|
+
description: 'All migration files across all db_types found in the database/ directory.',
|
|
388
|
+
mimeType: 'application/json'
|
|
389
|
+
}
|
|
390
|
+
]
|
|
391
|
+
}));
|
|
392
|
+
|
|
393
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
394
|
+
const uri = request.params.uri;
|
|
395
|
+
|
|
396
|
+
if (uri === 'codeninja://context') {
|
|
397
|
+
const ctx = readJSON(CONTEXT_FILE) || EMPTY_CONTEXT;
|
|
398
|
+
return { contents: [{ uri, mimeType: 'application/json', text: JSON.stringify(ctx, null, 2) }] };
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (uri === 'codeninja://services') {
|
|
402
|
+
const services = scanServices();
|
|
403
|
+
return { contents: [{ uri, mimeType: 'application/json', text: JSON.stringify(services, null, 2) }] };
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if (uri === 'codeninja://migrations') {
|
|
407
|
+
const migrations = scanMigrations();
|
|
408
|
+
return { contents: [{ uri, mimeType: 'application/json', text: JSON.stringify(migrations, null, 2) }] };
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
throw new Error(`Unknown resource: ${uri}`);
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
// ─── Tool Handlers ────────────────────────────────────────────────────────────
|
|
415
|
+
|
|
416
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
417
|
+
const { name, arguments: args } = request.params;
|
|
418
|
+
|
|
419
|
+
try {
|
|
420
|
+
|
|
421
|
+
// ── CATEGORY 1: Context Engine ──────────────────────────────────────────
|
|
422
|
+
|
|
423
|
+
if (name === 'context_read') {
|
|
424
|
+
const ctx = readJSON(CONTEXT_FILE) || structuredClone(EMPTY_CONTEXT);
|
|
425
|
+
return ok(JSON.stringify(ctx, null, 2));
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (name === 'context_write') {
|
|
429
|
+
fs.mkdirSync(CONTEXT_DIR, { recursive: true });
|
|
430
|
+
const existing = readJSON(CONTEXT_FILE) || structuredClone(EMPTY_CONTEXT);
|
|
431
|
+
const merged = deepMerge(structuredClone(existing), args.updates);
|
|
432
|
+
merged.context_version = (existing.context_version || 0) + 1;
|
|
433
|
+
const now = new Date().toISOString();
|
|
434
|
+
merged.last_updated_at = now;
|
|
435
|
+
merged.last_command_at = now;
|
|
436
|
+
merged.has_context_file = true;
|
|
437
|
+
if (args.operation) merged.last_command = args.operation;
|
|
438
|
+
const tmp = CONTEXT_FILE + '.tmp';
|
|
439
|
+
fs.writeFileSync(tmp, JSON.stringify(merged, null, 2), 'utf8');
|
|
440
|
+
fs.renameSync(tmp, CONTEXT_FILE);
|
|
441
|
+
return ok(`✓ context.json written. Version: ${merged.context_version}`);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
if (name === 'context_clear_scratchpad') {
|
|
445
|
+
const existing = readJSON(CONTEXT_FILE);
|
|
446
|
+
if (!existing) return ok('No context.json found — nothing to clear.');
|
|
447
|
+
for (const key of args.keys) {
|
|
448
|
+
if (key.startsWith('current_')) existing[key] = {};
|
|
449
|
+
}
|
|
450
|
+
existing.context_version = (existing.context_version || 0) + 1;
|
|
451
|
+
existing.last_updated_at = new Date().toISOString();
|
|
452
|
+
fs.writeFileSync(CONTEXT_FILE, JSON.stringify(existing, null, 2), 'utf8');
|
|
453
|
+
return ok(`✓ Cleared: ${args.keys.join(', ')}`);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (name === 'context_check_stale') {
|
|
457
|
+
const ctx = readJSON(CONTEXT_FILE);
|
|
458
|
+
if (!ctx) return ok(JSON.stringify({ stale: [], has_context: false }));
|
|
459
|
+
const threshold = (args.threshold_minutes || 10) * 60 * 1000;
|
|
460
|
+
const lastCommandAge = ctx.last_command_at
|
|
461
|
+
? Date.now() - new Date(ctx.last_command_at).getTime()
|
|
462
|
+
: Infinity;
|
|
463
|
+
const stale = [];
|
|
464
|
+
for (const key of Object.keys(ctx)) {
|
|
465
|
+
if (key.startsWith('current_') && ctx[key] && Object.keys(ctx[key]).length > 0) {
|
|
466
|
+
if (lastCommandAge > threshold) {
|
|
467
|
+
stale.push({ key, age_minutes: Math.round(lastCommandAge / 60000), content: ctx[key] });
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
return ok(JSON.stringify({ stale, has_context: true, last_command: ctx.last_command }));
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// ── CATEGORY 2: Filesystem Intelligence ────────────────────────────────
|
|
475
|
+
|
|
476
|
+
if (name === 'fs_exists') {
|
|
477
|
+
const full = abs(args.path);
|
|
478
|
+
if (!fs.existsSync(full)) return ok(JSON.stringify({ exists: false, type: null }));
|
|
479
|
+
const stat = fs.statSync(full);
|
|
480
|
+
return ok(JSON.stringify({ exists: true, type: stat.isDirectory() ? 'directory' : 'file' }));
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
if (name === 'fs_list') {
|
|
484
|
+
const full = abs(args.path);
|
|
485
|
+
if (!fs.existsSync(full)) return ok(JSON.stringify([]));
|
|
486
|
+
let files = fs.readdirSync(full).sort();
|
|
487
|
+
if (args.extension) files = files.filter(f => f.endsWith(args.extension));
|
|
488
|
+
return ok(JSON.stringify(files));
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
if (name === 'fs_read') {
|
|
492
|
+
const full = abs(args.path);
|
|
493
|
+
if (!fs.existsSync(full)) return ok('null');
|
|
494
|
+
return ok(fs.readFileSync(full, 'utf8'));
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
if (name === 'migration_next_number') {
|
|
498
|
+
const dir = abs(`database/${args.db_type}/migrations`);
|
|
499
|
+
if (!fs.existsSync(dir)) return ok('1');
|
|
500
|
+
const files = fs.readdirSync(dir).filter(f => f.endsWith('.sql'));
|
|
501
|
+
if (files.length === 0) return ok('1');
|
|
502
|
+
const nums = files
|
|
503
|
+
.filter(f => !f.includes('setup-database-indexes'))
|
|
504
|
+
.map(f => parseInt(f))
|
|
505
|
+
.filter(n => !isNaN(n));
|
|
506
|
+
return ok(String(nums.length > 0 ? Math.max(...nums) + 1 : 1));
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
if (name === 'service_scan') {
|
|
510
|
+
return ok(JSON.stringify(scanServices()));
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// ── CATEGORY 3: Surgical Editor ─────────────────────────────────────────
|
|
514
|
+
|
|
515
|
+
if (name === 'file_insert_after') {
|
|
516
|
+
const full = abs(args.path);
|
|
517
|
+
if (!fs.existsSync(full)) return ok(`ERROR: File not found: ${args.path}`);
|
|
518
|
+
const content = fs.readFileSync(full, 'utf8');
|
|
519
|
+
const idx = content.lastIndexOf(args.anchor);
|
|
520
|
+
if (idx === -1) return ok(`ERROR: Anchor not found: "${args.anchor}"`);
|
|
521
|
+
const insertAt = idx + args.anchor.length;
|
|
522
|
+
const updated = content.slice(0, insertAt) + '\n' + args.line + content.slice(insertAt);
|
|
523
|
+
fs.writeFileSync(full, updated, 'utf8');
|
|
524
|
+
return ok(`✓ Inserted line after anchor in ${args.path}`);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
if (name === 'file_contains') {
|
|
528
|
+
const full = abs(args.path);
|
|
529
|
+
if (!fs.existsSync(full)) return ok(JSON.stringify({ exists: false, contains: false }));
|
|
530
|
+
const content = fs.readFileSync(full, 'utf8');
|
|
531
|
+
return ok(JSON.stringify({ exists: true, contains: content.includes(args.search) }));
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// ── CATEGORY 4: Code Intelligence ───────────────────────────────────────
|
|
535
|
+
|
|
536
|
+
if (name === 'analyze_middleware_order') {
|
|
537
|
+
const filePath = `${args.service}/modules/v1/route_manager.js`;
|
|
538
|
+
const full = abs(filePath);
|
|
539
|
+
if (!fs.existsSync(full)) return ok(JSON.stringify({ error: 'File not found', path: filePath }));
|
|
540
|
+
const content = fs.readFileSync(full, 'utf8');
|
|
541
|
+
const lines = content.split('\n');
|
|
542
|
+
const middlewareLines = lines.filter(l => l.includes('router.use') && l.includes('asyncHandler'));
|
|
543
|
+
const parsed = middlewareLines.map((line, i) => {
|
|
544
|
+
const match = line.match(/asyncHandler\(middleware\.(\w+)|asyncHandler\((\w+)\)/);
|
|
545
|
+
const mw = match ? (match[1] || match[2]) : 'unknown';
|
|
546
|
+
const hasAsync = line.includes('asyncHandler');
|
|
547
|
+
return { middleware: mw, position: i + 1, has_asyncHandler: hasAsync };
|
|
548
|
+
});
|
|
549
|
+
return ok(JSON.stringify(parsed));
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
if (name === 'analyze_encryption_library') {
|
|
553
|
+
const filePath = `${args.service}/utilities/encryption.js`;
|
|
554
|
+
const full = abs(filePath);
|
|
555
|
+
if (!fs.existsSync(full)) return ok(JSON.stringify({ error: 'File not found' }));
|
|
556
|
+
const content = fs.readFileSync(full, 'utf8');
|
|
557
|
+
const hasCryptoJs = content.includes('crypto-js');
|
|
558
|
+
const hasCryptlib = content.includes('cryptlib');
|
|
559
|
+
const library = hasCryptoJs && hasCryptlib ? 'both' : hasCryptoJs ? 'crypto-js' : hasCryptlib ? 'cryptlib' : 'none';
|
|
560
|
+
const expected = args.expected_client_type === 'reactjs' ? 'crypto-js' : 'cryptlib';
|
|
561
|
+
return ok(JSON.stringify({ library, expected, is_consistent: library === expected }));
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
if (name === 'analyze_language_keys') {
|
|
565
|
+
const langDir = abs(`${args.service}/languages`);
|
|
566
|
+
if (!fs.existsSync(langDir)) return ok(JSON.stringify({ error: 'languages/ directory not found' }));
|
|
567
|
+
const files = fs.readdirSync(langDir).filter(f => f.endsWith('.js'));
|
|
568
|
+
const keyMap = {};
|
|
569
|
+
for (const file of files) {
|
|
570
|
+
const content = fs.readFileSync(path.join(langDir, file), 'utf8');
|
|
571
|
+
const keys = [...content.matchAll(/^\s{2}(\w+):/gm)].map(m => m[1]);
|
|
572
|
+
keyMap[file] = keys;
|
|
573
|
+
}
|
|
574
|
+
const enKeys = new Set(keyMap['en.js'] || []);
|
|
575
|
+
const report = {};
|
|
576
|
+
for (const [file, keys] of Object.entries(keyMap)) {
|
|
577
|
+
if (file === 'en.js') continue;
|
|
578
|
+
const fileKeys = new Set(keys);
|
|
579
|
+
report[file] = {
|
|
580
|
+
missing: [...enKeys].filter(k => !fileKeys.has(k)),
|
|
581
|
+
extra: [...fileKeys].filter(k => !enKeys.has(k))
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
return ok(JSON.stringify({ total_keys_en: enKeys.size, report }));
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
if (name === 'analyze_dependencies') {
|
|
588
|
+
const pkgPath = `${args.service}/package.json`;
|
|
589
|
+
const full = abs(pkgPath);
|
|
590
|
+
if (!fs.existsSync(full)) return ok(JSON.stringify({ error: 'package.json not found' }));
|
|
591
|
+
const pkg = JSON.parse(fs.readFileSync(full, 'utf8'));
|
|
592
|
+
const installed = new Set(Object.keys(pkg.dependencies || {}));
|
|
593
|
+
const required = new Set([
|
|
594
|
+
'express', 'dotenv', 'localizify', 'moment', 'jsonwebtoken',
|
|
595
|
+
'ioredis', 'nodemailer', 'firebase-admin', 'validatorjs',
|
|
596
|
+
'pg-format', 'express-rate-limit', 'cors', 'helmet', 'rand-token',
|
|
597
|
+
args.client_type === 'reactjs' ? 'crypto-js' : 'cryptlib',
|
|
598
|
+
args.db_type === 'postgresql' ? 'pg' : args.db_type === 'mysql' ? 'mysql2' : 'mongoose'
|
|
599
|
+
]);
|
|
600
|
+
return ok(JSON.stringify({
|
|
601
|
+
missing: [...required].filter(p => !installed.has(p)),
|
|
602
|
+
present: [...required].filter(p => installed.has(p)),
|
|
603
|
+
unexpected_crypto: args.client_type === 'reactjs' && installed.has('cryptlib')
|
|
604
|
+
? 'cryptlib present but client_type is reactjs'
|
|
605
|
+
: args.client_type === 'app' && installed.has('crypto-js')
|
|
606
|
+
? 'crypto-js present but client_type is app'
|
|
607
|
+
: null
|
|
608
|
+
}));
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
if (name === 'analyze_env_file') {
|
|
612
|
+
const envPath = abs(`${args.service}/.env`);
|
|
613
|
+
if (!fs.existsSync(envPath)) return ok(JSON.stringify({ error: '.env not found' }));
|
|
614
|
+
const lines = fs.readFileSync(envPath, 'utf8').split('\n');
|
|
615
|
+
const result = {};
|
|
616
|
+
const sensitive = new Set(['KEY', 'IV', 'API_KEY', 'DB_PASSWORD', 'SMTP_PASSWORD']);
|
|
617
|
+
for (const line of lines) {
|
|
618
|
+
if (!line.trim() || line.startsWith('#')) continue;
|
|
619
|
+
const eq = line.indexOf('=');
|
|
620
|
+
if (eq === -1) continue;
|
|
621
|
+
const k = line.slice(0, eq).trim();
|
|
622
|
+
const v = line.slice(eq + 1).trim();
|
|
623
|
+
result[k] = sensitive.has(k) ? '***' : v;
|
|
624
|
+
}
|
|
625
|
+
return ok(JSON.stringify(result));
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
if (name === 'run_drift_check') {
|
|
629
|
+
const service = args.service;
|
|
630
|
+
const ctx = readJSON(CONTEXT_FILE);
|
|
631
|
+
const serviceCtx = ctx?.services?.[service];
|
|
632
|
+
if (!serviceCtx) return ok(JSON.stringify({ error: `Service "${service}" not found in context` }));
|
|
633
|
+
const drifts = [];
|
|
634
|
+
|
|
635
|
+
// Check 1 — middleware order
|
|
636
|
+
const mwPath = abs(`${service}/modules/v1/route_manager.js`);
|
|
637
|
+
if (fs.existsSync(mwPath)) {
|
|
638
|
+
const content = fs.readFileSync(mwPath, 'utf8');
|
|
639
|
+
const expected = ['rateLimiter', 'extractLanguage', 'validateApiKey', 'validateToken'];
|
|
640
|
+
if (serviceCtx.encrypted_transport) expected.push('decryptRequest');
|
|
641
|
+
expected.forEach((mw, i) => {
|
|
642
|
+
if (!content.includes(mw)) drifts.push({ check: 1, issue: `Missing middleware: ${mw}` });
|
|
643
|
+
});
|
|
644
|
+
if (!serviceCtx.encrypted_transport && content.includes('decryptRequest'))
|
|
645
|
+
drifts.push({ check: 1, issue: 'decryptRequest present but encrypted_transport is false' });
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// Check 2 — encryption library
|
|
649
|
+
const encPath = abs(`${service}/utilities/encryption.js`);
|
|
650
|
+
if (fs.existsSync(encPath)) {
|
|
651
|
+
const content = fs.readFileSync(encPath, 'utf8');
|
|
652
|
+
const expected = serviceCtx.client_type === 'reactjs' ? 'crypto-js' : 'cryptlib';
|
|
653
|
+
const wrong = serviceCtx.client_type === 'reactjs' ? 'cryptlib' : 'crypto-js';
|
|
654
|
+
if (content.includes(wrong))
|
|
655
|
+
drifts.push({ check: 2, issue: `encryption.js uses ${wrong} but client_type is ${serviceCtx.client_type}` });
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// Check 3 — response.js encrypted_transport
|
|
659
|
+
const respPath = abs(`${service}/utilities/response.js`);
|
|
660
|
+
if (fs.existsSync(respPath)) {
|
|
661
|
+
const content = fs.readFileSync(respPath, 'utf8');
|
|
662
|
+
if (!content.includes('ENCRYPTED_TRANSPORT'))
|
|
663
|
+
drifts.push({ check: 3, issue: 'response.js does not reference ENCRYPTED_TRANSPORT' });
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// Check 4 — headerValidator decryptRequest
|
|
667
|
+
const hvPath = abs(`${service}/middleware/headerValidator.js`);
|
|
668
|
+
if (fs.existsSync(hvPath)) {
|
|
669
|
+
const content = fs.readFileSync(hvPath, 'utf8');
|
|
670
|
+
const hasDecrypt = content.includes('decryptRequest');
|
|
671
|
+
if (serviceCtx.encrypted_transport && !hasDecrypt)
|
|
672
|
+
drifts.push({ check: 4, issue: 'headerValidator missing decryptRequest but encrypted_transport is true' });
|
|
673
|
+
if (!serviceCtx.encrypted_transport && hasDecrypt)
|
|
674
|
+
drifts.push({ check: 4, issue: 'headerValidator has decryptRequest but encrypted_transport is false' });
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// Check 5 — language key parity
|
|
678
|
+
const langDir = abs(`${service}/languages`);
|
|
679
|
+
if (fs.existsSync(langDir)) {
|
|
680
|
+
|
|
681
|
+
const enPath = path.join(langDir, 'en.js');
|
|
682
|
+
if (!fs.existsSync(enPath)) {
|
|
683
|
+
drifts.push({ check: 5, issue: 'languages/en.js not found — language key parity check skipped' });
|
|
684
|
+
} else {
|
|
685
|
+
const files = fs.readdirSync(langDir).filter(f => f.endsWith('.js'));
|
|
686
|
+
const enContent = fs.readFileSync(enPath, 'utf8');
|
|
687
|
+
const enKeys = new Set([...enContent.matchAll(/^\s{2}(\w+):/gm)].map(m => m[1]));
|
|
688
|
+
for (const file of files) {
|
|
689
|
+
if (file === 'en.js') continue;
|
|
690
|
+
const content = fs.readFileSync(path.join(langDir, file), 'utf8');
|
|
691
|
+
const keys = new Set([...content.matchAll(/^\s{2}(\w+):/gm)].map(m => m[1]));
|
|
692
|
+
const missing = [...enKeys].filter(k => !keys.has(k));
|
|
693
|
+
if (missing.length > 0)
|
|
694
|
+
drifts.push({ check: 5, issue: `${file} missing ${missing.length} keys: ${missing.slice(0, 5).join(', ')}${missing.length > 5 ? '...' : ''}` });
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// Check 6 — package.json dependencies
|
|
700
|
+
const pkgPath = abs(`${service}/package.json`);
|
|
701
|
+
if (fs.existsSync(pkgPath)) {
|
|
702
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
703
|
+
const installed = Object.keys(pkg.dependencies || {});
|
|
704
|
+
const dbPkg = ctx.db?.type === 'postgresql' ? 'pg' : ctx.db?.type === 'mysql' ? 'mysql2' : 'mongoose';
|
|
705
|
+
const encPkg = serviceCtx.client_type === 'reactjs' ? 'crypto-js' : 'cryptlib';
|
|
706
|
+
for (const required of ['express', 'jsonwebtoken', 'ioredis', dbPkg, encPkg]) {
|
|
707
|
+
if (!installed.includes(required))
|
|
708
|
+
drifts.push({ check: 6, issue: `package.json missing required dependency: ${required}` });
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
return ok(JSON.stringify({
|
|
713
|
+
service,
|
|
714
|
+
drift_count: drifts.length,
|
|
715
|
+
clean: drifts.length === 0,
|
|
716
|
+
drifts
|
|
717
|
+
}));
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// ── CATEGORY 5: Environment Tools ───────────────────────────────────────
|
|
721
|
+
|
|
722
|
+
if (name === 'npm_check_package') {
|
|
723
|
+
try {
|
|
724
|
+
const result = execSync(`npm view ${args.package_name} name version description --json 2>/dev/null`, { encoding: 'utf8', timeout: 10000 });
|
|
725
|
+
return ok(result);
|
|
726
|
+
} catch {
|
|
727
|
+
return ok(JSON.stringify({ error: `Package "${args.package_name}" not found on npm registry` }));
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
if (name === 'npm_install') {
|
|
732
|
+
const serviceDir = abs(args.service);
|
|
733
|
+
if (!fs.existsSync(serviceDir)) return ok(`ERROR: Service directory not found: ${args.service}`);
|
|
734
|
+
try {
|
|
735
|
+
const result = execSync('npm install', { cwd: serviceDir, encoding: 'utf8', timeout: 120000 });
|
|
736
|
+
return ok(`✓ npm install completed in ${args.service}\n${result}`);
|
|
737
|
+
} catch (e) {
|
|
738
|
+
return ok(`ERROR: npm install failed\n${e.stderr || e.message}`);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
if (name === 'validate_redis_connection') {
|
|
743
|
+
try {
|
|
744
|
+
const start = Date.now();
|
|
745
|
+
execSync(`redis-cli -h ${args.host} -p ${args.port} PING`, { encoding: 'utf8', timeout: 5000 });
|
|
746
|
+
return ok(JSON.stringify({ reachable: true, latency_ms: Date.now() - start }));
|
|
747
|
+
} catch (e) {
|
|
748
|
+
return ok(JSON.stringify({ reachable: false, error: e.message }));
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
if (name === 'validate_postgres_connection') {
|
|
753
|
+
try {
|
|
754
|
+
const connStr = `postgresql://${args.user}${args.password ? ':' + args.password : ''}@${args.host}:${args.port}/${args.database}`;
|
|
755
|
+
const result = execSync(`psql "${connStr}" -c "SELECT version();" --no-password 2>&1`, { encoding: 'utf8', timeout: 8000 });
|
|
756
|
+
const versionLine = result.split('\n').find(l => l.includes('PostgreSQL'));
|
|
757
|
+
return ok(JSON.stringify({ reachable: true, version: versionLine?.trim() || 'connected' }));
|
|
758
|
+
} catch (e) {
|
|
759
|
+
return ok(JSON.stringify({ reachable: false, error: e.message }));
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
if (name === 'lint_file') {
|
|
764
|
+
const full = abs(args.path);
|
|
765
|
+
if (!fs.existsSync(full)) return ok(JSON.stringify({ valid: false, issues: ['File not found'] }));
|
|
766
|
+
const content = fs.readFileSync(full, 'utf8');
|
|
767
|
+
const issues = [];
|
|
768
|
+
|
|
769
|
+
// Syntax check
|
|
770
|
+
try {
|
|
771
|
+
execSync(`node --check "${full}"`, { encoding: 'utf8', timeout: 5000 });
|
|
772
|
+
} catch (e) {
|
|
773
|
+
issues.push(`Syntax error: ${e.stderr?.split('\n')[0] || e.message}`);
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// Model-specific checks
|
|
777
|
+
if (args.file_type === 'model') {
|
|
778
|
+
if (content.includes("require('express')") || content.includes('require("express")'))
|
|
779
|
+
issues.push('Model file imports express — express objects must not exist in model files');
|
|
780
|
+
if (content.includes('res.json(') || content.includes('res.status('))
|
|
781
|
+
issues.push('Model file calls res.json/res.status — all responses must go through response.js');
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// General checks
|
|
785
|
+
if (!content.includes('module.exports'))
|
|
786
|
+
issues.push('Missing module.exports — file does not export anything');
|
|
787
|
+
|
|
788
|
+
return ok(JSON.stringify({ valid: issues.length === 0, issues }));
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
return ok(`Unknown tool: ${name}`);
|
|
792
|
+
|
|
793
|
+
} catch (err) {
|
|
794
|
+
return ok(`ERROR: ${err.message}`);
|
|
795
|
+
}
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
799
|
+
|
|
800
|
+
function ok(text) {
|
|
801
|
+
return { content: [{ type: 'text', text }] };
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
function scanServices() {
|
|
805
|
+
const entries = fs.readdirSync(ROOT, { withFileTypes: true });
|
|
806
|
+
const services = [];
|
|
807
|
+
const skip = new Set(['.codeninja', 'node_modules', 'database', '.git']);
|
|
808
|
+
for (const entry of entries) {
|
|
809
|
+
if (!entry.isDirectory() || skip.has(entry.name)) continue;
|
|
810
|
+
const dir = path.join(ROOT, entry.name);
|
|
811
|
+
const hasAppJs = fs.existsSync(path.join(dir, 'app.js'));
|
|
812
|
+
const hasPkgJson = fs.existsSync(path.join(dir, 'package.json'));
|
|
813
|
+
const hasReact = fs.existsSync(path.join(dir, 'src', 'main.jsx')) ||
|
|
814
|
+
fs.existsSync(path.join(dir, 'src', 'index.jsx'));
|
|
815
|
+
if (hasAppJs || hasPkgJson) {
|
|
816
|
+
services.push({
|
|
817
|
+
name: entry.name,
|
|
818
|
+
type: hasReact ? 'reactjs' : 'nodejs',
|
|
819
|
+
has_env: fs.existsSync(path.join(dir, '.env')),
|
|
820
|
+
has_modules: fs.existsSync(path.join(dir, 'modules'))
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
return services;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
function scanMigrations() {
|
|
828
|
+
const dbDir = abs('database');
|
|
829
|
+
if (!fs.existsSync(dbDir)) return {};
|
|
830
|
+
const result = {};
|
|
831
|
+
for (const dbType of fs.readdirSync(dbDir)) {
|
|
832
|
+
const migrDir = path.join(dbDir, dbType, 'migrations');
|
|
833
|
+
if (!fs.existsSync(migrDir)) continue;
|
|
834
|
+
result[dbType] = fs.readdirSync(migrDir).filter(f => f.endsWith('.sql')).sort();
|
|
835
|
+
}
|
|
836
|
+
return result;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
// ─── Start ────────────────────────────────────────────────────────────────────
|
|
840
|
+
|
|
841
|
+
const transport = new StdioServerTransport();
|
|
842
|
+
server.connect(transport).catch(console.error);
|