atabey-mcp 0.0.4
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/constants.js +64 -0
- package/dist/index.js +119 -0
- package/dist/tools/control_plane/locking.js +82 -0
- package/dist/tools/control_plane/registry.js +34 -0
- package/dist/tools/definitions.js +290 -0
- package/dist/tools/file_system/batch_surgical_edit.js +59 -0
- package/dist/tools/file_system/patch_file.js +29 -0
- package/dist/tools/file_system/read_file.js +51 -0
- package/dist/tools/file_system/replace_text.js +45 -0
- package/dist/tools/file_system/write_file.js +38 -0
- package/dist/tools/framework/audit_deps.js +41 -0
- package/dist/tools/framework/get_status.js +5 -0
- package/dist/tools/framework/orchestrate.js +5 -0
- package/dist/tools/framework/run_tests.js +27 -0
- package/dist/tools/framework/update_contract_hash.js +5 -0
- package/dist/tools/framework/update_memory.js +8 -0
- package/dist/tools/index.js +60 -0
- package/dist/tools/memory/get_insights.js +34 -0
- package/dist/tools/memory/read_memory.js +28 -0
- package/dist/tools/messaging/log_action.js +22 -0
- package/dist/tools/messaging/send_message.js +94 -0
- package/dist/tools/observability/check_ports.js +26 -0
- package/dist/tools/observability/get_health.js +19 -0
- package/dist/tools/quality/check_lint.js +30 -0
- package/dist/tools/search/get_gaps.js +48 -0
- package/dist/tools/search/get_map.js +43 -0
- package/dist/tools/search/grep_search.js +75 -0
- package/dist/tools/search/list_dir.js +28 -0
- package/dist/tools/shell/run_command.js +56 -0
- package/dist/tools/types.js +1 -0
- package/dist/utils/cli.js +59 -0
- package/dist/utils/compliance.js +78 -0
- package/dist/utils/fs.js +44 -0
- package/dist/utils/metrics.js +56 -0
- package/dist/utils/security.js +60 -0
- package/package.json +26 -0
- package/src/constants.ts +78 -0
- package/src/declarations.d.ts +17 -0
- package/src/index.ts +144 -0
- package/src/tools/control_plane/locking.ts +89 -0
- package/src/tools/control_plane/registry.ts +38 -0
- package/src/tools/definitions.ts +292 -0
- package/src/tools/file_system/batch_surgical_edit.ts +79 -0
- package/src/tools/file_system/patch_file.ts +39 -0
- package/src/tools/file_system/read_file.ts +58 -0
- package/src/tools/file_system/replace_text.ts +54 -0
- package/src/tools/file_system/write_file.ts +45 -0
- package/src/tools/framework/audit_deps.ts +49 -0
- package/src/tools/framework/get_status.ts +7 -0
- package/src/tools/framework/orchestrate.ts +7 -0
- package/src/tools/framework/run_tests.ts +30 -0
- package/src/tools/framework/update_contract_hash.ts +7 -0
- package/src/tools/framework/update_memory.ts +10 -0
- package/src/tools/index.ts +64 -0
- package/src/tools/memory/get_insights.ts +41 -0
- package/src/tools/memory/read_memory.ts +31 -0
- package/src/tools/messaging/log_action.ts +28 -0
- package/src/tools/messaging/send_message.ts +97 -0
- package/src/tools/observability/check_ports.ts +30 -0
- package/src/tools/observability/get_health.ts +24 -0
- package/src/tools/quality/check_lint.ts +36 -0
- package/src/tools/search/get_gaps.ts +54 -0
- package/src/tools/search/get_map.ts +48 -0
- package/src/tools/search/grep_search.ts +75 -0
- package/src/tools/search/list_dir.ts +34 -0
- package/src/tools/shell/run_command.ts +66 -0
- package/src/tools/types.ts +89 -0
- package/src/utils/cli.ts +53 -0
- package/src/utils/compliance.ts +95 -0
- package/src/utils/fs.ts +45 -0
- package/src/utils/metrics.ts +73 -0
- package/src/utils/security.ts +66 -0
- package/tsconfig.json +14 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { safePath } from "../../utils/security.js";
|
|
4
|
+
import { GetProjectGapsArgs, ToolResult } from "../types.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Scans the codebase for TODOs, FIXMEs, and empty function bodies.
|
|
8
|
+
* Helps identify what's left and where the agent might have skipped logic.
|
|
9
|
+
*/
|
|
10
|
+
export function handleGetProjectGaps(projectRoot: string, args: GetProjectGapsArgs): ToolResult {
|
|
11
|
+
const srcDir = safePath(projectRoot, args.path || "src");
|
|
12
|
+
const results: string[] = [];
|
|
13
|
+
|
|
14
|
+
const walk = (dir: string) => {
|
|
15
|
+
if (!fs.existsSync(dir)) return;
|
|
16
|
+
const files = fs.readdirSync(dir);
|
|
17
|
+
for (const file of files) {
|
|
18
|
+
const fullPath = path.join(dir, file);
|
|
19
|
+
const relativePath = path.relative(projectRoot, fullPath);
|
|
20
|
+
|
|
21
|
+
if (fs.statSync(fullPath).isDirectory()) {
|
|
22
|
+
if (file !== "node_modules" && file !== "dist" && !file.startsWith(".")) {
|
|
23
|
+
walk(fullPath);
|
|
24
|
+
}
|
|
25
|
+
} else if (file.endsWith(".ts") || file.endsWith(".tsx")) {
|
|
26
|
+
const content = fs.readFileSync(fullPath, "utf8");
|
|
27
|
+
const lines = content.split("\n");
|
|
28
|
+
|
|
29
|
+
lines.forEach((line, index) => {
|
|
30
|
+
// 1. Scan for markers
|
|
31
|
+
if (line.includes("TODO") || line.includes("FIXME") || line.includes("!!!")) {
|
|
32
|
+
results.push(`[${relativePath}:${index + 1}] Marker found: ${line.trim()}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// 2. Scan for empty function placeholders (heuristic)
|
|
36
|
+
if (line.includes("throw new Error(\"Not implemented") || line.includes("// ... rest of code")) {
|
|
37
|
+
results.push(`[${relativePath}:${index + 1}] Gap found: ${line.trim()}`);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
walk(srcDir);
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
content: [{
|
|
48
|
+
type: "text",
|
|
49
|
+
text: results.length > 0
|
|
50
|
+
? `Found ${results.length} gaps/todos:\n\n${results.join("\n")}`
|
|
51
|
+
: "✅ No major gaps or TODOs found in the scanned directory."
|
|
52
|
+
}]
|
|
53
|
+
};
|
|
54
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { GetProjectMapArgs, ToolResult } from "../types.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Generates a tree-view map of the project structure.
|
|
7
|
+
* Helps agents visualize the entire project layout quickly.
|
|
8
|
+
*/
|
|
9
|
+
export function handleGetProjectMap(projectRoot: string, args: GetProjectMapArgs): ToolResult {
|
|
10
|
+
const maxDepth = args.maxDepth || 3;
|
|
11
|
+
const includeFiles = args.includeFiles !== false;
|
|
12
|
+
|
|
13
|
+
const buildTree = (dir: string, depth: number): string[] => {
|
|
14
|
+
if (depth > maxDepth) return [];
|
|
15
|
+
|
|
16
|
+
const results: string[] = [];
|
|
17
|
+
const files = fs.readdirSync(dir);
|
|
18
|
+
|
|
19
|
+
files.forEach(file => {
|
|
20
|
+
if (file === "node_modules" || file === ".git" || file === "dist" || file.startsWith(".")) return;
|
|
21
|
+
|
|
22
|
+
const fullPath = path.join(dir, file);
|
|
23
|
+
const stat = fs.statSync(fullPath);
|
|
24
|
+
const indent = " ".repeat(depth);
|
|
25
|
+
|
|
26
|
+
if (stat.isDirectory()) {
|
|
27
|
+
results.push(`${indent}📁 ${file}/`);
|
|
28
|
+
results.push(...buildTree(fullPath, depth + 1));
|
|
29
|
+
} else if (includeFiles) {
|
|
30
|
+
results.push(`${indent}📄 ${file}`);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
return results;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const tree = buildTree(projectRoot, 0);
|
|
39
|
+
return {
|
|
40
|
+
content: [{
|
|
41
|
+
type: "text",
|
|
42
|
+
text: `🗺️ **Project Map (Depth: ${maxDepth})**\n\n${tree.join("\n")}`
|
|
43
|
+
}]
|
|
44
|
+
};
|
|
45
|
+
} catch (e) {
|
|
46
|
+
return { isError: true, content: [{ type: "text", text: `Failed to map project: ${String(e)}` }] };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { GrepSearchArgs, ToolResult } from "../types.js";
|
|
4
|
+
import { Metrics } from "../../utils/metrics.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Searches for a regex pattern within files in the project.
|
|
8
|
+
*/
|
|
9
|
+
export function handleGrepSearch(projectRoot: string, args: GrepSearchArgs): ToolResult {
|
|
10
|
+
const pattern = args.pattern as string;
|
|
11
|
+
const includePattern = args.includePattern as string || ""; // e.g., ".ts"
|
|
12
|
+
const excludePattern = args.excludePattern as string || "node_modules";
|
|
13
|
+
|
|
14
|
+
if (!pattern) {
|
|
15
|
+
const err = "Search pattern is required.";
|
|
16
|
+
Metrics.logError(projectRoot, "@mcp", "grep_search", err);
|
|
17
|
+
return { isError: true, content: [{ type: "text", text: `❌ ${err}` }] };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const results: string[] = [];
|
|
21
|
+
try {
|
|
22
|
+
new RegExp(pattern);
|
|
23
|
+
} catch (e) {
|
|
24
|
+
const err = `Invalid regex pattern: ${String(e)}`;
|
|
25
|
+
Metrics.logError(projectRoot, "@mcp", "grep_search", err);
|
|
26
|
+
return { isError: true, content: [{ type: "text", text: `❌ ${err}` }] };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const walk = (dir: string) => {
|
|
30
|
+
if (results.length > 100) return;
|
|
31
|
+
try {
|
|
32
|
+
const files = fs.readdirSync(dir);
|
|
33
|
+
for (const file of files) {
|
|
34
|
+
if (results.length > 100) return;
|
|
35
|
+
const filePath = path.join(dir, file);
|
|
36
|
+
if (excludePattern && filePath.includes(excludePattern)) {
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
const stat = fs.statSync(filePath);
|
|
40
|
+
if (stat.isDirectory()) {
|
|
41
|
+
walk(filePath);
|
|
42
|
+
} else if (stat.isFile()) {
|
|
43
|
+
if (includePattern && !filePath.endsWith(includePattern)) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
47
|
+
// Create a new regex object for each line to avoid state issues with /g
|
|
48
|
+
if (new RegExp(pattern).test(content)) {
|
|
49
|
+
if (results.length < 100) {
|
|
50
|
+
results.push(filePath);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
} catch {
|
|
56
|
+
// Ignore directories that cannot be read
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
try {
|
|
60
|
+
walk(projectRoot);
|
|
61
|
+
} catch (e) {
|
|
62
|
+
const err = `Search failed: ${String(e)}`;
|
|
63
|
+
Metrics.logError(projectRoot, "@mcp", "grep_search", err);
|
|
64
|
+
return { isError: true, content: [{ type: "text", text: `❌ ${err}` }] };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
content: [{
|
|
69
|
+
type: "text",
|
|
70
|
+
text: results.length > 0
|
|
71
|
+
? `Found ${results.length} matches:\n\n${results.join("\n")}`
|
|
72
|
+
: "No matches found."
|
|
73
|
+
}]
|
|
74
|
+
};
|
|
75
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { safePath } from "../../utils/security.js";
|
|
4
|
+
import { ListDirArgs, ToolResult } from "../types.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Lists the contents of a directory.
|
|
8
|
+
*/
|
|
9
|
+
export function handleListDir(projectRoot: string, args: ListDirArgs): ToolResult {
|
|
10
|
+
const dirPath = safePath(projectRoot, args.path || ".");
|
|
11
|
+
|
|
12
|
+
if (!fs.existsSync(dirPath)) {
|
|
13
|
+
throw new Error(`Directory not found: ${args.path}`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const stats = fs.statSync(dirPath);
|
|
17
|
+
if (!stats.isDirectory()) {
|
|
18
|
+
throw new Error(`Path is not a directory: ${args.path}`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const files = fs.readdirSync(dirPath);
|
|
22
|
+
const results = files.map(file => {
|
|
23
|
+
const fullPath = path.join(dirPath, file);
|
|
24
|
+
const isDir = fs.statSync(fullPath).isDirectory();
|
|
25
|
+
return `${isDir ? "[DIR] " : " "}${file}`;
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
content: [{
|
|
30
|
+
type: "text",
|
|
31
|
+
text: `Directory listing for ${args.path || "."}:\n\n${results.join("\n")}`
|
|
32
|
+
}]
|
|
33
|
+
};
|
|
34
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { exec } from "child_process";
|
|
2
|
+
import { RunCommandArgs, ToolResult } from "../types.js";
|
|
3
|
+
import { Metrics } from "../../utils/metrics.js";
|
|
4
|
+
|
|
5
|
+
const COMMAND_ALLOW_LIST = [
|
|
6
|
+
"npm test",
|
|
7
|
+
"npm run lint",
|
|
8
|
+
"npm run build",
|
|
9
|
+
"git status",
|
|
10
|
+
"git diff",
|
|
11
|
+
"npx vitest run",
|
|
12
|
+
"go test",
|
|
13
|
+
"go fmt",
|
|
14
|
+
"go build",
|
|
15
|
+
"pytest",
|
|
16
|
+
"ruff check",
|
|
17
|
+
"dotnet test",
|
|
18
|
+
"dotnet format",
|
|
19
|
+
"dotnet build",
|
|
20
|
+
"./gradlew",
|
|
21
|
+
"mvn",
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
const TIMEOUT = 30000; // 30 seconds
|
|
25
|
+
|
|
26
|
+
export function handleRunCommand(projectRoot: string, args: RunCommandArgs): Promise<ToolResult> {
|
|
27
|
+
const command = args.command;
|
|
28
|
+
|
|
29
|
+
const isAllowed = COMMAND_ALLOW_LIST.some(allowedCmd => command.startsWith(allowedCmd));
|
|
30
|
+
|
|
31
|
+
if (!isAllowed) {
|
|
32
|
+
const errorMsg = `Command not allowed: "${command}". Only commands starting with the following are allowed: ${COMMAND_ALLOW_LIST.join(", ")}`;
|
|
33
|
+
Metrics.logError(projectRoot, "@mcp", `run_shell_command: ${command} (denied)`, errorMsg);
|
|
34
|
+
return Promise.resolve({
|
|
35
|
+
content: [{ type: "text", text: `ERROR: ${errorMsg}` }],
|
|
36
|
+
isError: true,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return new Promise((resolve) => {
|
|
41
|
+
exec(command, { cwd: projectRoot, timeout: TIMEOUT }, (error, stdout, stderr) => {
|
|
42
|
+
const output = stdout + stderr;
|
|
43
|
+
const tokens = Metrics.estimateTokens(output);
|
|
44
|
+
Metrics.logUsage(projectRoot, "@mcp", `run_shell_command: ${command}`, tokens);
|
|
45
|
+
|
|
46
|
+
if (error) {
|
|
47
|
+
const errorMsg = `Command failed with exit code ${error.code}: ${error.message}.`;
|
|
48
|
+
Metrics.logError(projectRoot, "@mcp", `run_shell_command: ${command}`, errorMsg);
|
|
49
|
+
resolve({
|
|
50
|
+
content: [{ type: "text", text: `ERROR: ${errorMsg}. Output: ${output}` }],
|
|
51
|
+
isError: true,
|
|
52
|
+
});
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Truncate long outputs
|
|
57
|
+
const MAX_OUTPUT_LENGTH = 5000;
|
|
58
|
+
let truncatedOutput = output;
|
|
59
|
+
if (output.length > MAX_OUTPUT_LENGTH) {
|
|
60
|
+
truncatedOutput = output.substring(0, MAX_OUTPUT_LENGTH) + "... [TRUNCATED] ..."; // Simplified
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
resolve({ content: [{ type: "text", text: truncatedOutput }] });
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
export interface ToolDefinition {
|
|
2
|
+
name: string;
|
|
3
|
+
description: string;
|
|
4
|
+
inputSchema: {
|
|
5
|
+
type: "object";
|
|
6
|
+
properties: Record<string, unknown>;
|
|
7
|
+
required?: string[];
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// ─── File System ────────────────────────────────────────────────
|
|
12
|
+
export interface ReadFileArgs { path: string; startLine?: number; endLine?: number; }
|
|
13
|
+
export interface WriteFileArgs { path: string; content: string; }
|
|
14
|
+
export interface ReplaceTextArgs { path: string; oldText: string; newText: string; allowMultiple?: boolean; }
|
|
15
|
+
export interface PatchFileArgs { path: string; startLine: number; endLine: number; newContent: string; }
|
|
16
|
+
export interface BatchSurgicalEditArgs {
|
|
17
|
+
edits: Array<{ path: string; oldText: string; newText: string; allowMultiple?: boolean; }>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// ─── Search & Discovery ──────────────────────────────────────────
|
|
21
|
+
export interface ListDirArgs { path?: string; }
|
|
22
|
+
export interface GrepSearchArgs { pattern: string; includePattern?: string; excludePattern?: string; }
|
|
23
|
+
export interface GetProjectMapArgs { maxDepth?: number; includeFiles?: boolean; }
|
|
24
|
+
export interface GetProjectGapsArgs { path?: string; }
|
|
25
|
+
|
|
26
|
+
// ─── Messaging (Hermes) ─────────────────────────────────────────
|
|
27
|
+
export interface SendAgentMessageArgs {
|
|
28
|
+
from: string;
|
|
29
|
+
to: string;
|
|
30
|
+
category: "ACTION" | "DELEGATION" | "SUBTASK" | "REPLY" | "ALERT";
|
|
31
|
+
content: string;
|
|
32
|
+
traceId: string;
|
|
33
|
+
parentId?: string;
|
|
34
|
+
priority?: "HIGH" | "NORMAL" | "LOW";
|
|
35
|
+
requiresApproval?: boolean;
|
|
36
|
+
}
|
|
37
|
+
export interface LogAgentActionArgs {
|
|
38
|
+
agent: string;
|
|
39
|
+
action: string;
|
|
40
|
+
traceId: string;
|
|
41
|
+
status: "SUCCESS" | "FAILURE";
|
|
42
|
+
summary: string;
|
|
43
|
+
findings?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ─── Control Plane ──────────────────────────────────────────────
|
|
47
|
+
export interface AcquireLockArgs { resource: string; agent: string; ttl?: number; }
|
|
48
|
+
export interface ReleaseLockArgs { resource: string; agent: string; }
|
|
49
|
+
export interface RegisterAgentArgs { agent: string; role: string; capability?: number; }
|
|
50
|
+
|
|
51
|
+
// ─── Observability & Utils ──────────────────────────────────────
|
|
52
|
+
export interface StartDashboardArgs { port?: number; }
|
|
53
|
+
export interface CheckActivePortsArgs { filter?: string; }
|
|
54
|
+
export interface RunTestsArgs { command?: string; timeout?: number; }
|
|
55
|
+
export interface UpdateProjectMemoryArgs { section: string; content: string; }
|
|
56
|
+
export interface GetStatusArgs { timeout?: number; }
|
|
57
|
+
export interface OrchestrateArgs { timeout?: number; }
|
|
58
|
+
export interface UpdateContractHashArgs { timeout?: number; }
|
|
59
|
+
export interface RunCommandArgs { command: string; }
|
|
60
|
+
|
|
61
|
+
export type ToolArgs =
|
|
62
|
+
| ReadFileArgs
|
|
63
|
+
| WriteFileArgs
|
|
64
|
+
| ReplaceTextArgs
|
|
65
|
+
| PatchFileArgs
|
|
66
|
+
| BatchSurgicalEditArgs
|
|
67
|
+
| ListDirArgs
|
|
68
|
+
| GrepSearchArgs
|
|
69
|
+
| GetProjectMapArgs
|
|
70
|
+
| GetProjectGapsArgs
|
|
71
|
+
| SendAgentMessageArgs
|
|
72
|
+
| LogAgentActionArgs
|
|
73
|
+
| AcquireLockArgs
|
|
74
|
+
| ReleaseLockArgs
|
|
75
|
+
| RegisterAgentArgs
|
|
76
|
+
| StartDashboardArgs
|
|
77
|
+
| CheckActivePortsArgs
|
|
78
|
+
| RunTestsArgs
|
|
79
|
+
| UpdateProjectMemoryArgs
|
|
80
|
+
| GetStatusArgs
|
|
81
|
+
| OrchestrateArgs
|
|
82
|
+
| UpdateContractHashArgs;
|
|
83
|
+
|
|
84
|
+
export interface ToolResult {
|
|
85
|
+
isError?: boolean;
|
|
86
|
+
content: Array<{ type: "text"; text: string }>;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export type ToolHandler = (projectRoot: string, args: unknown) => ToolResult | Promise<ToolResult>;
|
package/src/utils/cli.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { execFileSync } from "child_process";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Executes a command safely and returns the output.
|
|
7
|
+
*/
|
|
8
|
+
export function safeExec(cmd: string, args: string[], cwd: string, timeout = 30000): string {
|
|
9
|
+
try {
|
|
10
|
+
return execFileSync(cmd, args, { cwd, timeout, encoding: "utf8", stdio: "pipe" });
|
|
11
|
+
} catch (err: unknown) {
|
|
12
|
+
const error = err as { stdout?: Buffer | string; stderr?: Buffer | string; message?: string };
|
|
13
|
+
return error.stdout?.toString() || error.stderr?.toString() || error.message || String(err);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Detects the backend language from the framework configuration.
|
|
19
|
+
*/
|
|
20
|
+
export function getBackendLanguage(projectRoot: string): string {
|
|
21
|
+
try {
|
|
22
|
+
const configPath = path.join(projectRoot, ".atabey", "config.json");
|
|
23
|
+
if (fs.existsSync(configPath)) {
|
|
24
|
+
const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
25
|
+
return config.backendLanguage || "Node.js (TypeScript)";
|
|
26
|
+
}
|
|
27
|
+
} catch {
|
|
28
|
+
// Fallback to default
|
|
29
|
+
}
|
|
30
|
+
return "Node.js (TypeScript)";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Returns the default lint command for the given language.
|
|
35
|
+
*/
|
|
36
|
+
export function getDefaultLintCommand(language: string): string {
|
|
37
|
+
if (language.includes("Go")) return "go fmt ./...";
|
|
38
|
+
if (language.includes("Java")) return "./gradlew check"; // or mvn check
|
|
39
|
+
if (language.includes("Python")) return "ruff check .";
|
|
40
|
+
if (language.includes(".NET")) return "dotnet format";
|
|
41
|
+
return "npm run lint";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Returns the default test command for the given language.
|
|
46
|
+
*/
|
|
47
|
+
export function getDefaultTestCommand(language: string): string {
|
|
48
|
+
if (language.includes("Go")) return "go test ./...";
|
|
49
|
+
if (language.includes("Java")) return "./gradlew test"; // or mvn test
|
|
50
|
+
if (language.includes("Python")) return "pytest";
|
|
51
|
+
if (language.includes(".NET")) return "dotnet test";
|
|
52
|
+
return "npm test";
|
|
53
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Enterprise Compliance Guardrail
|
|
5
|
+
* Checks content against corporate standards using AST analysis before allowing file mutations.
|
|
6
|
+
*/
|
|
7
|
+
export function verifyCorporateCompliance(content: string, filePath: string): void {
|
|
8
|
+
// Skip compliance checks for non-source files or specific ignored files
|
|
9
|
+
if (filePath.endsWith(".json") || filePath.endsWith(".md") || filePath.endsWith(".env.example")) {
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const sourceFile = ts.createSourceFile(
|
|
14
|
+
filePath,
|
|
15
|
+
content,
|
|
16
|
+
ts.ScriptTarget.Latest,
|
|
17
|
+
true
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
const errors: string[] = [];
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Recursive AST Visitor
|
|
24
|
+
*/
|
|
25
|
+
function visit(node: ts.Node) {
|
|
26
|
+
// 1. Zero Console Policy
|
|
27
|
+
if (ts.isPropertyAccessExpression(node)) {
|
|
28
|
+
const expression = node.expression;
|
|
29
|
+
const name = node.name.text;
|
|
30
|
+
if (ts.isIdentifier(expression) && expression.text === "console") {
|
|
31
|
+
if (["log", "warn", "error"].includes(name)) {
|
|
32
|
+
// Check if file is exempt
|
|
33
|
+
if (!filePath.includes("logger.ts") && !filePath.includes("check.ts") && !filePath.includes("cli.ts")) {
|
|
34
|
+
errors.push(`❌ Corporate Compliance Breach: 'console.${name}' usage is forbidden at line ${sourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1}.`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// 2. No Explicit Any Policy
|
|
41
|
+
if (ts.isTypeReferenceNode(node)) {
|
|
42
|
+
if (ts.isIdentifier(node.typeName) && node.typeName.text === "any") {
|
|
43
|
+
if (!filePath.includes("definitions.ts") && !filePath.includes("types.ts")) {
|
|
44
|
+
errors.push(`❌ Corporate Compliance Breach: 'any' type is forbidden at line ${sourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1}.`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// 3. Zero UI Library Policy (No @chakra-ui, mui, @shadcn)
|
|
50
|
+
if (ts.isImportDeclaration(node)) {
|
|
51
|
+
const moduleSpecifier = node.moduleSpecifier;
|
|
52
|
+
if (ts.isStringLiteral(moduleSpecifier)) {
|
|
53
|
+
const forbiddenLibs = ["@chakra-ui", "mui", "@shadcn", "antd", "bootstrap"];
|
|
54
|
+
const lib = forbiddenLibs.find(l => moduleSpecifier.text.includes(l));
|
|
55
|
+
if (lib) {
|
|
56
|
+
errors.push(`❌ Corporate Compliance Breach: External UI library '${lib}' usage is FORBIDDEN at line ${sourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1}. Build atomic components manually instead.`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Handle 'any' as a keyword type (e.g., parameter: any)
|
|
62
|
+
if (node.kind === ts.SyntaxKind.AnyKeyword) {
|
|
63
|
+
if (!filePath.includes("definitions.ts") && !filePath.includes("types.ts")) {
|
|
64
|
+
errors.push(`❌ Corporate Compliance Breach: 'any' keyword is forbidden at line ${sourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1}.`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
ts.forEachChild(node, visit);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
visit(sourceFile);
|
|
72
|
+
|
|
73
|
+
// 3. Hardcoded Secrets & PII Guard
|
|
74
|
+
const piiKeywords = [
|
|
75
|
+
{ regex: /API_KEY\s*=\s*['"][^'"]+['"]/i, msg: "Hardcoded API Key" },
|
|
76
|
+
{ regex: /SECRET\s*=\s*['"][^'"]+['"]/i, msg: "Hardcoded Secret" },
|
|
77
|
+
{ regex: /PASSWORD\s*=\s*['"][^'"]+['"]/i, msg: "Hardcoded Password" },
|
|
78
|
+
{ regex: /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/, msg: "PII Detected: Email Address" },
|
|
79
|
+
{ regex: /\b\d{4}[- ]?\d{4}[- ]?\d{4}[- ]?\d{4}\b/, msg: "PII Detected: Credit Card Pattern" }
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
for (const { regex, msg } of piiKeywords) {
|
|
83
|
+
if (regex.test(content)) {
|
|
84
|
+
// Allow emails in specific files like README or package.json
|
|
85
|
+
if (msg.includes("Email") && (filePath.endsWith("README.md") || filePath.endsWith("package.json") || filePath.includes("CONTRIBUTING"))) {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
errors.push(`❌ Corporate Compliance Breach: ${msg} detected.`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (errors.length > 0) {
|
|
93
|
+
throw new Error(errors.join("\n"));
|
|
94
|
+
}
|
|
95
|
+
}
|
package/src/utils/fs.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Ensures directory existence.
|
|
6
|
+
*/
|
|
7
|
+
export function ensureDir(dirPath: string): void {
|
|
8
|
+
if (!fs.existsSync(dirPath)) {
|
|
9
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Atomically writes a text file.
|
|
15
|
+
*/
|
|
16
|
+
export function writeTextFileAtomic(filePath: string, content: string): void {
|
|
17
|
+
const dir = path.dirname(filePath);
|
|
18
|
+
ensureDir(dir);
|
|
19
|
+
|
|
20
|
+
const tempPath = `${filePath}.${Math.random().toString(36).slice(2, 9)}.tmp`;
|
|
21
|
+
const finalContent = content.endsWith("\n") ? content : `${content}\n`;
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
fs.writeFileSync(tempPath, finalContent, "utf8");
|
|
25
|
+
fs.renameSync(tempPath, filePath);
|
|
26
|
+
} catch (err) {
|
|
27
|
+
if (fs.existsSync(tempPath)) {
|
|
28
|
+
try { fs.unlinkSync(tempPath); } catch { /* ignore */ }
|
|
29
|
+
}
|
|
30
|
+
throw err;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Atomically appends to a file (if supported by OS) or simulates it.
|
|
36
|
+
* Note: Real atomic append on POSIX is a single write() call with O_APPEND.
|
|
37
|
+
* For simplicity and robustness across platforms, we use a simple append here
|
|
38
|
+
* as the risk of corruption is lower than a full rewrite, but for logs
|
|
39
|
+
* it's acceptable.
|
|
40
|
+
*/
|
|
41
|
+
export function appendFileSafe(filePath: string, content: string): void {
|
|
42
|
+
const dir = path.dirname(filePath);
|
|
43
|
+
ensureDir(dir);
|
|
44
|
+
fs.appendFileSync(filePath, content, "utf8");
|
|
45
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { resolveFrameworkDir } from "./security.js";
|
|
4
|
+
/**
|
|
5
|
+
* Token and Metric Tracker for Agent Atabey.
|
|
6
|
+
* Estimates token usage and logs operational costs.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
interface MetricEntry {
|
|
10
|
+
timestamp: string;
|
|
11
|
+
agent: string;
|
|
12
|
+
action: string;
|
|
13
|
+
estimatedTokens: number;
|
|
14
|
+
error?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const Metrics = {
|
|
18
|
+
/**
|
|
19
|
+
* Estimates tokens based on character count (rough heuristic: 1 token ~= 4 chars).
|
|
20
|
+
*/
|
|
21
|
+
estimateTokens: (text: string): number => {
|
|
22
|
+
return Math.ceil(text.length / 4);
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Logs the token usage and action to the observability metrics file.
|
|
27
|
+
*/
|
|
28
|
+
logUsage: (projectRoot: string, agent: string, action: string, tokens: number) => {
|
|
29
|
+
Metrics.saveMetric(projectRoot, {
|
|
30
|
+
timestamp: new Date().toISOString(),
|
|
31
|
+
agent,
|
|
32
|
+
action,
|
|
33
|
+
estimatedTokens: tokens
|
|
34
|
+
});
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Logs an error occurrence to the observability metrics file.
|
|
39
|
+
*/
|
|
40
|
+
logError: (projectRoot: string, agent: string, action: string, error: string) => {
|
|
41
|
+
Metrics.saveMetric(projectRoot, {
|
|
42
|
+
timestamp: new Date().toISOString(),
|
|
43
|
+
agent,
|
|
44
|
+
action: `ERROR: ${action}`,
|
|
45
|
+
estimatedTokens: 0,
|
|
46
|
+
error
|
|
47
|
+
});
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Internal helper to save metric entries.
|
|
52
|
+
*/
|
|
53
|
+
saveMetric: (projectRoot: string, entry: MetricEntry) => {
|
|
54
|
+
const frameworkDir = resolveFrameworkDir(projectRoot);
|
|
55
|
+
const metricsPath = path.join(projectRoot, frameworkDir, "observability/metrics.json");
|
|
56
|
+
try {
|
|
57
|
+
const metricsDir = path.dirname(metricsPath);
|
|
58
|
+
if (!fs.existsSync(metricsDir)) fs.mkdirSync(metricsDir, { recursive: true });
|
|
59
|
+
|
|
60
|
+
let currentMetrics: MetricEntry[] = [];
|
|
61
|
+
if (fs.existsSync(metricsPath)) {
|
|
62
|
+
currentMetrics = JSON.parse(fs.readFileSync(metricsPath, "utf8"));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
currentMetrics.push(entry);
|
|
66
|
+
|
|
67
|
+
// Keep only last 100 entries to save space
|
|
68
|
+
if (currentMetrics.length > 100) currentMetrics.shift();
|
|
69
|
+
|
|
70
|
+
fs.writeFileSync(metricsPath, JSON.stringify(currentMetrics, null, 2));
|
|
71
|
+
} catch { /* ignore: metrics should not block the main process */ }
|
|
72
|
+
}
|
|
73
|
+
};
|