@devshub198211/devguard 2.0.1

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.
@@ -0,0 +1,493 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import * as crypto from 'crypto';
4
+
5
+ // src/ai/agent-schema.ts
6
+ function cleanLLMOutput(raw) {
7
+ let s = raw.trim();
8
+ s = s.replace(/^```(?:json|JSON)?\s*/m, "").replace(/```\s*$/m, "").trim();
9
+ const openBrace = s.indexOf("{");
10
+ const openBracket = s.indexOf("[");
11
+ let startIdx = -1;
12
+ let openChar = "", closeChar = "";
13
+ if (openBrace === -1 && openBracket === -1) return s;
14
+ if (openBrace === -1) {
15
+ startIdx = openBracket;
16
+ openChar = "[";
17
+ closeChar = "]";
18
+ } else if (openBracket === -1) {
19
+ startIdx = openBrace;
20
+ openChar = "{";
21
+ closeChar = "}";
22
+ } else if (openBrace < openBracket) {
23
+ startIdx = openBrace;
24
+ openChar = "{";
25
+ closeChar = "}";
26
+ } else {
27
+ startIdx = openBracket;
28
+ openChar = "[";
29
+ closeChar = "]";
30
+ }
31
+ let depth = 0, inString = false, escape = false, end = -1;
32
+ for (let i = startIdx; i < s.length; i++) {
33
+ const ch = s[i];
34
+ if (escape) {
35
+ escape = false;
36
+ continue;
37
+ }
38
+ if (ch === "\\" && inString) {
39
+ escape = true;
40
+ continue;
41
+ }
42
+ if (ch === '"') {
43
+ inString = !inString;
44
+ continue;
45
+ }
46
+ if (inString) continue;
47
+ if (ch === openChar) depth++;
48
+ else if (ch === closeChar) {
49
+ depth--;
50
+ if (depth === 0) {
51
+ end = i;
52
+ break;
53
+ }
54
+ }
55
+ }
56
+ return end !== -1 ? s.slice(startIdx, end + 1) : s;
57
+ }
58
+ function repairJSON(raw) {
59
+ let result = "";
60
+ let inString = false;
61
+ let escape = false;
62
+ for (let i = 0; i < raw.length; i++) {
63
+ const ch = raw[i];
64
+ if (escape) {
65
+ result += ch;
66
+ escape = false;
67
+ continue;
68
+ }
69
+ if (ch === "\\" && inString) {
70
+ result += ch;
71
+ escape = true;
72
+ continue;
73
+ }
74
+ if (ch === '"') {
75
+ inString = !inString;
76
+ result += ch;
77
+ continue;
78
+ }
79
+ if (inString) {
80
+ result += ch;
81
+ continue;
82
+ }
83
+ if (ch === ",") {
84
+ let j = i + 1;
85
+ while (j < raw.length && /\s/.test(raw[j])) j++;
86
+ if (j < raw.length && (raw[j] === "}" || raw[j] === "]")) {
87
+ continue;
88
+ }
89
+ }
90
+ result += ch;
91
+ }
92
+ return result;
93
+ }
94
+ function parseSchema(schema, raw) {
95
+ const cleaned = cleanLLMOutput(raw);
96
+ try {
97
+ const parsed = JSON.parse(cleaned);
98
+ const r = schema.safeParse(parsed);
99
+ if (r.success) return { success: true, data: r.data, raw: cleaned, errors: [], attempts: 1 };
100
+ return { success: false, raw: cleaned, errors: r.errors.map((e) => (e.path ? e.path + ": " : "") + e.message), attempts: 1 };
101
+ } catch {
102
+ const repaired = repairJSON(cleaned);
103
+ if (repaired !== cleaned) {
104
+ try {
105
+ const parsed = JSON.parse(repaired);
106
+ const r = schema.safeParse(parsed);
107
+ if (r.success) return { success: true, data: r.data, raw: repaired, errors: [], attempts: 2 };
108
+ return { success: false, raw: repaired, errors: r.errors.map((e) => (e.path ? e.path + ": " : "") + e.message), attempts: 2 };
109
+ } catch {
110
+ }
111
+ }
112
+ return { success: false, errors: ["Could not parse LLM output as JSON"], attempts: 2 };
113
+ }
114
+ }
115
+ async function parseWithRetry(schema, promptFn, maxRetries = 3) {
116
+ let context = "Respond with valid JSON only. No markdown fences, no explanation, just the JSON.";
117
+ let totalAttempts = 0;
118
+ for (let i = 0; i <= maxRetries; i++) {
119
+ const raw = await promptFn(context);
120
+ totalAttempts++;
121
+ const result = parseSchema(schema, raw);
122
+ if (result.success) return { ...result, attempts: totalAttempts };
123
+ context = `Your response failed validation: ${result.errors.join("; ")}. Fix these and respond with valid JSON only.`;
124
+ }
125
+ return { success: false, errors: [`Max retries (${maxRetries}) exceeded`], attempts: totalAttempts };
126
+ }
127
+
128
+ // src/ai/mcp-server-kit.ts
129
+ var ERR = { PARSE: -32700, INVALID: -32600, NOT_FOUND: -32601, PARAMS: -32602, INTERNAL: -32603 };
130
+ var MAX_MESSAGE_SIZE = 10 * 1024 * 1024;
131
+ var MAX_NAME_LENGTH = 256;
132
+ var HANDLER_TIMEOUT_MS = 3e4;
133
+ function validateToolInput(args, schema) {
134
+ if (typeof args !== "object" || args === null || Array.isArray(args)) {
135
+ return "Arguments must be an object";
136
+ }
137
+ if (schema.required) {
138
+ for (const req of schema.required) {
139
+ if (!(req in args) || args[req] === void 0 || args[req] === null) {
140
+ return `Missing required argument: ${req}`;
141
+ }
142
+ }
143
+ }
144
+ for (const [key, val] of Object.entries(args)) {
145
+ if (key === "__proto__" || key === "constructor" || key === "prototype") {
146
+ return `Invalid argument name: ${key}`;
147
+ }
148
+ const propSchema = schema.properties[key];
149
+ if (propSchema) {
150
+ const expectedType = propSchema.type;
151
+ if (expectedType === "string" && typeof val !== "string") return `Argument '${key}' must be a string`;
152
+ if (expectedType === "number" && typeof val !== "number") return `Argument '${key}' must be a number`;
153
+ if (expectedType === "boolean" && typeof val !== "boolean") return `Argument '${key}' must be a boolean`;
154
+ if (expectedType === "integer" && (typeof val !== "number" || !Number.isInteger(val))) return `Argument '${key}' must be an integer`;
155
+ if (propSchema.enum && !propSchema.enum.includes(val)) return `Argument '${key}' must be one of: ${propSchema.enum.join(", ")}`;
156
+ }
157
+ }
158
+ return null;
159
+ }
160
+ var MCPServerBuilder = class {
161
+ constructor(name, version = "1.0.0", description = "") {
162
+ this.name = name;
163
+ this.version = version;
164
+ this.description = description;
165
+ this.tools = /* @__PURE__ */ new Map();
166
+ this.resources = /* @__PURE__ */ new Map();
167
+ this.prompts = /* @__PURE__ */ new Map();
168
+ this.capabilities = {};
169
+ this.initialized = false;
170
+ if (!name || typeof name !== "string") throw new Error("Server name is required");
171
+ }
172
+ addTool(tool) {
173
+ if (!tool.name || typeof tool.name !== "string" || tool.name.length > MAX_NAME_LENGTH) throw new Error("Tool must have a valid name");
174
+ if (typeof tool.handler !== "function") throw new Error("Tool must have a handler function");
175
+ if (!tool.inputSchema || tool.inputSchema.type !== "object") throw new Error("Tool must have a valid inputSchema with type: 'object'");
176
+ this.tools.set(tool.name, tool);
177
+ this.capabilities.tools = {};
178
+ return this;
179
+ }
180
+ addResource(resource) {
181
+ if (!resource.uri || typeof resource.uri !== "string") throw new Error("Resource must have a URI");
182
+ if (typeof resource.fetch !== "function") throw new Error("Resource must have a fetch function");
183
+ this.resources.set(resource.uri, resource);
184
+ this.capabilities.resources = {};
185
+ return this;
186
+ }
187
+ addPrompt(prompt) {
188
+ if (!prompt.name || typeof prompt.name !== "string") throw new Error("Prompt must have a name");
189
+ if (typeof prompt.handler !== "function") throw new Error("Prompt must have a handler function");
190
+ this.prompts.set(prompt.name, prompt);
191
+ this.capabilities.prompts = {};
192
+ return this;
193
+ }
194
+ respond(id, result) {
195
+ return JSON.stringify({ jsonrpc: "2.0", id, result }) + "\n";
196
+ }
197
+ error(id, code, message, data) {
198
+ return JSON.stringify({ jsonrpc: "2.0", id, error: { code, message, data } }) + "\n";
199
+ }
200
+ /** Run a handler with a timeout */
201
+ async runWithTimeout(fn, timeoutMs) {
202
+ return new Promise((resolve2, reject) => {
203
+ const timer = setTimeout(() => reject(new Error("Handler execution timeout")), timeoutMs);
204
+ fn().then(
205
+ (result) => {
206
+ clearTimeout(timer);
207
+ resolve2(result);
208
+ },
209
+ (err) => {
210
+ clearTimeout(timer);
211
+ reject(err);
212
+ }
213
+ );
214
+ });
215
+ }
216
+ async dispatch(req) {
217
+ const { id, method, params } = req;
218
+ if (req.jsonrpc !== "2.0") {
219
+ if (id !== null && id !== void 0) return this.error(id, ERR.INVALID, "Invalid JSON-RPC version \u2014 must be '2.0'");
220
+ return null;
221
+ }
222
+ if (typeof method !== "string" || method.length === 0) {
223
+ if (id !== null && id !== void 0) return this.error(id, ERR.INVALID, "Method must be a non-empty string");
224
+ return null;
225
+ }
226
+ if (method === "initialize") {
227
+ this.initialized = true;
228
+ return this.respond(id, {
229
+ protocolVersion: "2024-11-05",
230
+ capabilities: this.capabilities,
231
+ serverInfo: { name: this.name, version: this.version },
232
+ instructions: this.description || void 0
233
+ });
234
+ }
235
+ if (method === "initialized") return null;
236
+ if (method === "ping") return this.respond(id, {});
237
+ if (method === "tools/list") {
238
+ return this.respond(id, {
239
+ tools: [...this.tools.values()].map((t) => ({
240
+ name: t.name,
241
+ description: t.description,
242
+ inputSchema: t.inputSchema
243
+ }))
244
+ });
245
+ }
246
+ if (method === "tools/call") {
247
+ const toolName = params?.name;
248
+ if (typeof toolName !== "string") return this.error(id, ERR.PARAMS, "Tool name is required");
249
+ const tool = this.tools.get(toolName);
250
+ if (!tool) return this.error(id, ERR.NOT_FOUND, `Tool not found: ${toolName}`);
251
+ const args = params?.arguments ?? {};
252
+ const validationError = validateToolInput(args, tool.inputSchema);
253
+ if (validationError) return this.error(id, ERR.PARAMS, validationError);
254
+ try {
255
+ const result = await this.runWithTimeout(
256
+ () => tool.handler(args, { progressToken: params?._meta?.progressToken }),
257
+ HANDLER_TIMEOUT_MS
258
+ );
259
+ const content = typeof result === "string" ? [{ type: "text", text: result }] : [{ type: "text", text: JSON.stringify(result) }];
260
+ return this.respond(id, { content, isError: false });
261
+ } catch (e) {
262
+ const msg = e?.message ?? String(e);
263
+ return this.respond(id, { content: [{ type: "text", text: msg.slice(0, 4096) }], isError: true });
264
+ }
265
+ }
266
+ if (method === "resources/list") {
267
+ return this.respond(id, {
268
+ resources: [...this.resources.values()].map((r) => ({
269
+ uri: r.uri,
270
+ name: r.name,
271
+ description: r.description,
272
+ mimeType: r.mimeType
273
+ }))
274
+ });
275
+ }
276
+ if (method === "resources/read") {
277
+ const uri = params?.uri;
278
+ if (typeof uri !== "string") return this.error(id, ERR.PARAMS, "Resource URI is required");
279
+ const resource = this.resources.get(uri);
280
+ if (!resource) return this.error(id, ERR.NOT_FOUND, `Resource not found: ${uri}`);
281
+ try {
282
+ const raw = await this.runWithTimeout(() => resource.fetch(), HANDLER_TIMEOUT_MS);
283
+ const content = typeof raw === "string" ? { uri: resource.uri, mimeType: resource.mimeType ?? "text/plain", text: raw } : { uri: resource.uri, mimeType: raw.mimeType ?? resource.mimeType ?? "text/plain", ...raw.text ? { text: raw.text } : { blob: raw.blob } };
284
+ return this.respond(id, { contents: [content] });
285
+ } catch (e) {
286
+ return this.error(id, ERR.INTERNAL, `Resource fetch failed: ${(e?.message ?? "Unknown error").slice(0, 1024)}`);
287
+ }
288
+ }
289
+ if (method === "prompts/list") {
290
+ return this.respond(id, {
291
+ prompts: [...this.prompts.values()].map((p) => ({ name: p.name, description: p.description, arguments: p.arguments }))
292
+ });
293
+ }
294
+ if (method === "prompts/get") {
295
+ const promptName = params?.name;
296
+ if (typeof promptName !== "string") return this.error(id, ERR.PARAMS, "Prompt name is required");
297
+ const prompt = this.prompts.get(promptName);
298
+ if (!prompt) return this.error(id, ERR.NOT_FOUND, `Prompt not found: ${promptName}`);
299
+ try {
300
+ const messages = await this.runWithTimeout(
301
+ () => prompt.handler(params?.arguments ?? {}),
302
+ HANDLER_TIMEOUT_MS
303
+ );
304
+ return this.respond(id, { description: prompt.description, messages });
305
+ } catch (e) {
306
+ return this.error(id, ERR.INTERNAL, `Prompt handler failed: ${(e?.message ?? "Unknown error").slice(0, 1024)}`);
307
+ }
308
+ }
309
+ if (id !== null && id !== void 0) return this.error(id, ERR.NOT_FOUND, `Method not found: ${method}`);
310
+ return null;
311
+ }
312
+ startStdio() {
313
+ process.stdin.setEncoding("utf-8");
314
+ let buf = "";
315
+ let processing = Promise.resolve();
316
+ process.stdin.on("data", (chunk) => {
317
+ buf += chunk;
318
+ if (buf.length > MAX_MESSAGE_SIZE) {
319
+ process.stdout.write(this.error(null, ERR.PARSE, "Message too large") + "\n");
320
+ buf = "";
321
+ return;
322
+ }
323
+ const lines = buf.split("\n");
324
+ buf = lines.pop() ?? "";
325
+ for (const line of lines) {
326
+ const trimmed = line.trim();
327
+ if (!trimmed) continue;
328
+ processing = processing.then(async () => {
329
+ let req;
330
+ try {
331
+ req = JSON.parse(trimmed);
332
+ } catch {
333
+ process.stdout.write(this.error(null, ERR.PARSE, "Parse error") + "\n");
334
+ return;
335
+ }
336
+ if (typeof req !== "object" || req === null || Array.isArray(req)) {
337
+ process.stdout.write(this.error(null, ERR.INVALID, "Request must be a JSON object") + "\n");
338
+ return;
339
+ }
340
+ try {
341
+ const response = await this.dispatch(req);
342
+ if (response) process.stdout.write(response);
343
+ } catch (e) {
344
+ const reqId = req?.id;
345
+ if (reqId !== null && reqId !== void 0) {
346
+ process.stdout.write(this.error(reqId, ERR.INTERNAL, (e?.message ?? "Internal error").slice(0, 1024)) + "\n");
347
+ }
348
+ }
349
+ });
350
+ }
351
+ });
352
+ process.stdin.on("end", () => process.exit(0));
353
+ process.stderr.write(`[devguard/mcp] "${this.name}" v${this.version} started
354
+ `);
355
+ }
356
+ };
357
+ var FileSystemAdapter = class {
358
+ constructor(dir = ".devguard-memory") {
359
+ this.keyIndex = /* @__PURE__ */ new Map();
360
+ this.baseDir = path.resolve(dir);
361
+ if (!fs.existsSync(this.baseDir)) {
362
+ fs.mkdirSync(this.baseDir, { recursive: true });
363
+ }
364
+ }
365
+ getFilePath(agentId) {
366
+ const safeId = crypto.createHash("sha256").update(agentId).digest("hex");
367
+ return path.join(this.baseDir, `${safeId}.json`);
368
+ }
369
+ async save(agentId, entry) {
370
+ const file = this.getFilePath(agentId);
371
+ let history = [];
372
+ if (fs.existsSync(file)) {
373
+ try {
374
+ const raw = fs.readFileSync(file, "utf-8");
375
+ history = JSON.parse(raw);
376
+ if (!Array.isArray(history)) history = [];
377
+ } catch {
378
+ history = [];
379
+ }
380
+ }
381
+ history.push(entry);
382
+ const tmp = `${file}.tmp.${crypto.randomBytes(4).toString("hex")}`;
383
+ fs.writeFileSync(tmp, JSON.stringify(history, null, 2));
384
+ fs.renameSync(tmp, file);
385
+ }
386
+ async getHistory(agentId, limit = 50) {
387
+ const file = this.getFilePath(agentId);
388
+ if (!fs.existsSync(file)) return [];
389
+ try {
390
+ const raw = fs.readFileSync(file, "utf-8");
391
+ const history = JSON.parse(raw);
392
+ if (!Array.isArray(history)) return [];
393
+ return history.slice(-limit);
394
+ } catch {
395
+ return [];
396
+ }
397
+ }
398
+ async clear(agentId) {
399
+ const file = this.getFilePath(agentId);
400
+ if (fs.existsSync(file)) fs.unlinkSync(file);
401
+ }
402
+ };
403
+ var RedisAdapter = class {
404
+ constructor(redisClient) {
405
+ this.client = redisClient;
406
+ }
407
+ async save(agentId, entry) {
408
+ const key = `devguard:mem:${agentId}`;
409
+ await this.client.rPush(key, JSON.stringify(entry));
410
+ await this.client.lTrim(key, -100, -1);
411
+ }
412
+ async getHistory(agentId, limit = 50) {
413
+ const key = `devguard:mem:${agentId}`;
414
+ const raw = await this.client.lRange(key, -limit, -1);
415
+ return raw.map((r) => {
416
+ try {
417
+ return JSON.parse(r);
418
+ } catch {
419
+ return null;
420
+ }
421
+ }).filter(Boolean);
422
+ }
423
+ async clear(agentId) {
424
+ await this.client.del(`devguard:mem:${agentId}`);
425
+ }
426
+ };
427
+ var AgentMemory = class {
428
+ constructor(adapter) {
429
+ this.adapter = adapter ?? new FileSystemAdapter();
430
+ }
431
+ async track(agentId, role, content, metadata) {
432
+ const entry = {
433
+ id: crypto.randomUUID(),
434
+ role,
435
+ content,
436
+ metadata,
437
+ timestamp: Date.now()
438
+ };
439
+ await this.adapter.save(agentId, entry);
440
+ return entry;
441
+ }
442
+ async getContext(agentId, limit = 10) {
443
+ const history = await this.adapter.getHistory(agentId, limit);
444
+ return history.map((h) => `${h.role.toUpperCase()}: ${h.content}`).join("\n");
445
+ }
446
+ async clear(agentId) {
447
+ await this.adapter.clear(agentId);
448
+ }
449
+ };
450
+ var MAX_RECORDS = 5e3;
451
+ var LLMBudget = class {
452
+ constructor(config) {
453
+ this.records = [];
454
+ this.totalCost = 0;
455
+ this.config = config;
456
+ }
457
+ track(usage) {
458
+ const record = {
459
+ ...usage,
460
+ id: crypto.randomUUID(),
461
+ timestamp: Date.now()
462
+ };
463
+ this.records.push(record);
464
+ this.totalCost += record.costUSD;
465
+ if (this.records.length > MAX_RECORDS) {
466
+ this.records.shift();
467
+ }
468
+ if (this.totalCost >= this.config.monthlyLimitUSD) {
469
+ console.warn(`[devguard] LLM Budget Exceeded: $${this.totalCost.toFixed(4)} / $${this.config.monthlyLimitUSD}`);
470
+ } else if (this.config.warnAtUSD && this.totalCost >= this.config.warnAtUSD) {
471
+ console.warn(`[devguard] LLM Budget Warning: $${this.totalCost.toFixed(4)} / $${this.config.monthlyLimitUSD}`);
472
+ }
473
+ return record;
474
+ }
475
+ getSummary() {
476
+ return {
477
+ totalCost: this.totalCost,
478
+ recordCount: this.records.length,
479
+ limit: this.config.monthlyLimitUSD,
480
+ remaining: Math.max(0, this.config.monthlyLimitUSD - this.totalCost),
481
+ isExceeded: this.totalCost >= this.config.monthlyLimitUSD
482
+ };
483
+ }
484
+ getHistory(limit = 100) {
485
+ return this.records.slice(-limit);
486
+ }
487
+ reset() {
488
+ this.records = [];
489
+ this.totalCost = 0;
490
+ }
491
+ };
492
+
493
+ export { AgentMemory, FileSystemAdapter, LLMBudget, MCPServerBuilder, RedisAdapter, cleanLLMOutput, parseSchema, parseWithRetry };