Compare commits
3 Commits
5672127593
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
08e6f66c7d | ||
|
|
8a95c428c8 | ||
|
|
1dc0ab515d |
6
bun.lock
6
bun.lock
@@ -8,6 +8,7 @@
|
|||||||
"@kubernetes/client-node": "^1.4.0",
|
"@kubernetes/client-node": "^1.4.0",
|
||||||
"@modelcontextprotocol/sdk": "^1.25.2",
|
"@modelcontextprotocol/sdk": "^1.25.2",
|
||||||
"axios": "^1.13.2",
|
"axios": "^1.13.2",
|
||||||
|
"bcryptjs": "^3.0.3",
|
||||||
"bullmq": "^5.66.5",
|
"bullmq": "^5.66.5",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
@@ -21,6 +22,7 @@
|
|||||||
"zod": "^4.3.5",
|
"zod": "^4.3.5",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/bcryptjs": "^3.0.0",
|
||||||
"@types/bun": "latest",
|
"@types/bun": "latest",
|
||||||
"@types/cors": "^2.8.19",
|
"@types/cors": "^2.8.19",
|
||||||
"@types/express": "^5.0.6",
|
"@types/express": "^5.0.6",
|
||||||
@@ -153,6 +155,8 @@
|
|||||||
|
|
||||||
"@socket.io/component-emitter": ["@socket.io/component-emitter@3.1.2", "", {}, "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="],
|
"@socket.io/component-emitter": ["@socket.io/component-emitter@3.1.2", "", {}, "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="],
|
||||||
|
|
||||||
|
"@types/bcryptjs": ["@types/bcryptjs@3.0.0", "", { "dependencies": { "bcryptjs": "*" } }, "sha512-WRZOuCuaz8UcZZE4R5HXTco2goQSI2XxjGY3hbM/xDvwmqFWd4ivooImsMx65OKM6CtNKbnZ5YL+YwAwK7c1dg=="],
|
||||||
|
|
||||||
"@types/body-parser": ["@types/body-parser@1.19.6", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="],
|
"@types/body-parser": ["@types/body-parser@1.19.6", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="],
|
||||||
|
|
||||||
"@types/bun": ["@types/bun@1.3.6", "", { "dependencies": { "bun-types": "1.3.6" } }, "sha512-uWCv6FO/8LcpREhenN1d1b6fcspAB+cefwD7uti8C8VffIv0Um08TKMn98FynpTiU38+y2dUO55T11NgDt8VAA=="],
|
"@types/bun": ["@types/bun@1.3.6", "", { "dependencies": { "bun-types": "1.3.6" } }, "sha512-uWCv6FO/8LcpREhenN1d1b6fcspAB+cefwD7uti8C8VffIv0Um08TKMn98FynpTiU38+y2dUO55T11NgDt8VAA=="],
|
||||||
@@ -235,6 +239,8 @@
|
|||||||
|
|
||||||
"base64id": ["base64id@2.0.0", "", {}, "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog=="],
|
"base64id": ["base64id@2.0.0", "", {}, "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog=="],
|
||||||
|
|
||||||
|
"bcryptjs": ["bcryptjs@3.0.3", "", { "bin": { "bcrypt": "bin/bcrypt" } }, "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g=="],
|
||||||
|
|
||||||
"body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="],
|
"body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="],
|
||||||
|
|
||||||
"brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
|
"brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
|
||||||
|
|||||||
24
drizzle/migrations/0001_opposite_warbird.sql
Normal file
24
drizzle/migrations/0001_opposite_warbird.sql
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
CREATE TABLE `sessions` (
|
||||||
|
`id` varchar(255) NOT NULL,
|
||||||
|
`user_id` varchar(36) NOT NULL,
|
||||||
|
`expires_at` timestamp NOT NULL,
|
||||||
|
`created_at` timestamp DEFAULT (now()),
|
||||||
|
CONSTRAINT `sessions_id` PRIMARY KEY(`id`)
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE `users` (
|
||||||
|
`id` varchar(36) NOT NULL,
|
||||||
|
`email` varchar(255) NOT NULL,
|
||||||
|
`username` varchar(100) NOT NULL,
|
||||||
|
`password_hash` varchar(255) NOT NULL,
|
||||||
|
`created_at` timestamp DEFAULT (now()),
|
||||||
|
`updated_at` timestamp DEFAULT (now()) ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
CONSTRAINT `users_id` PRIMARY KEY(`id`),
|
||||||
|
CONSTRAINT `users_email_unique` UNIQUE(`email`),
|
||||||
|
CONSTRAINT `users_username_unique` UNIQUE(`username`)
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE `sessions` ADD CONSTRAINT `sessions_user_id_users_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
CREATE INDEX `idx_user_id` ON `sessions` (`user_id`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `idx_expires_at` ON `sessions` (`expires_at`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `idx_email` ON `users` (`email`);
|
||||||
578
drizzle/migrations/meta/0001_snapshot.json
Normal file
578
drizzle/migrations/meta/0001_snapshot.json
Normal file
@@ -0,0 +1,578 @@
|
|||||||
|
{
|
||||||
|
"version": "5",
|
||||||
|
"dialect": "mysql",
|
||||||
|
"id": "ada6a640-177d-4c35-b7e0-e1d20c9adabb",
|
||||||
|
"prevId": "3f9067f3-0e1d-440b-917e-e2c879a5e283",
|
||||||
|
"tables": {
|
||||||
|
"agents": {
|
||||||
|
"name": "agents",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "varchar(36)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"pod_name": {
|
||||||
|
"name": "pod_name",
|
||||||
|
"type": "varchar(253)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"k8s_namespace": {
|
||||||
|
"name": "k8s_namespace",
|
||||||
|
"type": "varchar(63)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "'agents'"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"name": "status",
|
||||||
|
"type": "enum('idle','busy','error','offline')",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "'idle'"
|
||||||
|
},
|
||||||
|
"current_task_id": {
|
||||||
|
"name": "current_task_id",
|
||||||
|
"type": "varchar(36)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"tasks_completed": {
|
||||||
|
"name": "tasks_completed",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": 0
|
||||||
|
},
|
||||||
|
"last_heartbeat": {
|
||||||
|
"name": "last_heartbeat",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(now())"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"onUpdate": true,
|
||||||
|
"default": "(now())"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"idx_status": {
|
||||||
|
"name": "idx_status",
|
||||||
|
"columns": [
|
||||||
|
"status"
|
||||||
|
],
|
||||||
|
"isUnique": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"agents_id": {
|
||||||
|
"name": "agents_id",
|
||||||
|
"columns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"agents_pod_name_unique": {
|
||||||
|
"name": "agents_pod_name_unique",
|
||||||
|
"columns": [
|
||||||
|
"pod_name"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"checkConstraint": {}
|
||||||
|
},
|
||||||
|
"projects": {
|
||||||
|
"name": "projects",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "varchar(36)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"name": "description",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"gitea_repo_id": {
|
||||||
|
"name": "gitea_repo_id",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"gitea_repo_url": {
|
||||||
|
"name": "gitea_repo_url",
|
||||||
|
"type": "varchar(512)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"gitea_owner": {
|
||||||
|
"name": "gitea_owner",
|
||||||
|
"type": "varchar(100)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"gitea_repo_name": {
|
||||||
|
"name": "gitea_repo_name",
|
||||||
|
"type": "varchar(100)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"default_branch": {
|
||||||
|
"name": "default_branch",
|
||||||
|
"type": "varchar(100)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "'main'"
|
||||||
|
},
|
||||||
|
"k8s_namespace": {
|
||||||
|
"name": "k8s_namespace",
|
||||||
|
"type": "varchar(63)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"docker_image": {
|
||||||
|
"name": "docker_image",
|
||||||
|
"type": "varchar(512)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"env_vars": {
|
||||||
|
"name": "env_vars",
|
||||||
|
"type": "json",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"replicas": {
|
||||||
|
"name": "replicas",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": 1
|
||||||
|
},
|
||||||
|
"cpu_limit": {
|
||||||
|
"name": "cpu_limit",
|
||||||
|
"type": "varchar(20)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "'500m'"
|
||||||
|
},
|
||||||
|
"memory_limit": {
|
||||||
|
"name": "memory_limit",
|
||||||
|
"type": "varchar(20)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "'512Mi'"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"name": "status",
|
||||||
|
"type": "enum('active','paused','archived')",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "'active'"
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(now())"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"onUpdate": true,
|
||||||
|
"default": "(now())"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"idx_status": {
|
||||||
|
"name": "idx_status",
|
||||||
|
"columns": [
|
||||||
|
"status"
|
||||||
|
],
|
||||||
|
"isUnique": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"projects_id": {
|
||||||
|
"name": "projects_id",
|
||||||
|
"columns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"projects_k8s_namespace_unique": {
|
||||||
|
"name": "projects_k8s_namespace_unique",
|
||||||
|
"columns": [
|
||||||
|
"k8s_namespace"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"checkConstraint": {}
|
||||||
|
},
|
||||||
|
"sessions": {
|
||||||
|
"name": "sessions",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "varchar(36)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"expires_at": {
|
||||||
|
"name": "expires_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(now())"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"idx_user_id": {
|
||||||
|
"name": "idx_user_id",
|
||||||
|
"columns": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"isUnique": false
|
||||||
|
},
|
||||||
|
"idx_expires_at": {
|
||||||
|
"name": "idx_expires_at",
|
||||||
|
"columns": [
|
||||||
|
"expires_at"
|
||||||
|
],
|
||||||
|
"isUnique": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {
|
||||||
|
"sessions_user_id_users_id_fk": {
|
||||||
|
"name": "sessions_user_id_users_id_fk",
|
||||||
|
"tableFrom": "sessions",
|
||||||
|
"tableTo": "users",
|
||||||
|
"columnsFrom": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"sessions_id": {
|
||||||
|
"name": "sessions_id",
|
||||||
|
"columns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraint": {}
|
||||||
|
},
|
||||||
|
"tasks": {
|
||||||
|
"name": "tasks",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "varchar(36)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"project_id": {
|
||||||
|
"name": "project_id",
|
||||||
|
"type": "varchar(36)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"name": "title",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"name": "description",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"priority": {
|
||||||
|
"name": "priority",
|
||||||
|
"type": "enum('low','medium','high','urgent')",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "'medium'"
|
||||||
|
},
|
||||||
|
"state": {
|
||||||
|
"name": "state",
|
||||||
|
"type": "enum('backlog','in_progress','needs_input','ready_to_test','approved','staging','production')",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "'backlog'"
|
||||||
|
},
|
||||||
|
"assigned_agent_id": {
|
||||||
|
"name": "assigned_agent_id",
|
||||||
|
"type": "varchar(36)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"branch_name": {
|
||||||
|
"name": "branch_name",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"pr_url": {
|
||||||
|
"name": "pr_url",
|
||||||
|
"type": "varchar(512)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"preview_url": {
|
||||||
|
"name": "preview_url",
|
||||||
|
"type": "varchar(512)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(now())"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"onUpdate": true,
|
||||||
|
"default": "(now())"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"idx_project_state": {
|
||||||
|
"name": "idx_project_state",
|
||||||
|
"columns": [
|
||||||
|
"project_id",
|
||||||
|
"state"
|
||||||
|
],
|
||||||
|
"isUnique": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {
|
||||||
|
"tasks_project_id_projects_id_fk": {
|
||||||
|
"name": "tasks_project_id_projects_id_fk",
|
||||||
|
"tableFrom": "tasks",
|
||||||
|
"tableTo": "projects",
|
||||||
|
"columnsFrom": [
|
||||||
|
"project_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"tasks_assigned_agent_id_agents_id_fk": {
|
||||||
|
"name": "tasks_assigned_agent_id_agents_id_fk",
|
||||||
|
"tableFrom": "tasks",
|
||||||
|
"tableTo": "agents",
|
||||||
|
"columnsFrom": [
|
||||||
|
"assigned_agent_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "set null",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"tasks_id": {
|
||||||
|
"name": "tasks_id",
|
||||||
|
"columns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraint": {}
|
||||||
|
},
|
||||||
|
"users": {
|
||||||
|
"name": "users",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "varchar(36)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"username": {
|
||||||
|
"name": "username",
|
||||||
|
"type": "varchar(100)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"password_hash": {
|
||||||
|
"name": "password_hash",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(now())"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"onUpdate": true,
|
||||||
|
"default": "(now())"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"idx_email": {
|
||||||
|
"name": "idx_email",
|
||||||
|
"columns": [
|
||||||
|
"email"
|
||||||
|
],
|
||||||
|
"isUnique": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"users_id": {
|
||||||
|
"name": "users_id",
|
||||||
|
"columns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"users_email_unique": {
|
||||||
|
"name": "users_email_unique",
|
||||||
|
"columns": [
|
||||||
|
"email"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"users_username_unique": {
|
||||||
|
"name": "users_username_unique",
|
||||||
|
"columns": [
|
||||||
|
"username"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"checkConstraint": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"views": {},
|
||||||
|
"_meta": {
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {},
|
||||||
|
"columns": {}
|
||||||
|
},
|
||||||
|
"internal": {
|
||||||
|
"tables": {},
|
||||||
|
"indexes": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,13 @@
|
|||||||
"when": 1768858601280,
|
"when": 1768858601280,
|
||||||
"tag": "0000_charming_stature",
|
"tag": "0000_charming_stature",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 1,
|
||||||
|
"version": "5",
|
||||||
|
"when": 1768868010676,
|
||||||
|
"tag": "0001_opposite_warbird",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "bun --watch src/index.ts",
|
"dev": "bun --watch src/index.ts",
|
||||||
"start": "bun src/index.ts",
|
"start": "bun src/index.ts",
|
||||||
|
"mcp": "bun src/mcp/server.ts",
|
||||||
"build": "bun build src/index.ts --outdir dist --target bun",
|
"build": "bun build src/index.ts --outdir dist --target bun",
|
||||||
"db:generate": "drizzle-kit generate",
|
"db:generate": "drizzle-kit generate",
|
||||||
"db:migrate": "bun src/db/migrate.ts",
|
"db:migrate": "bun src/db/migrate.ts",
|
||||||
@@ -15,6 +16,7 @@
|
|||||||
"format": "prettier --write src/**/*.ts"
|
"format": "prettier --write src/**/*.ts"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/bcryptjs": "^3.0.0",
|
||||||
"@types/bun": "latest",
|
"@types/bun": "latest",
|
||||||
"@types/cors": "^2.8.19",
|
"@types/cors": "^2.8.19",
|
||||||
"@types/express": "^5.0.6",
|
"@types/express": "^5.0.6",
|
||||||
@@ -32,6 +34,7 @@
|
|||||||
"@kubernetes/client-node": "^1.4.0",
|
"@kubernetes/client-node": "^1.4.0",
|
||||||
"@modelcontextprotocol/sdk": "^1.25.2",
|
"@modelcontextprotocol/sdk": "^1.25.2",
|
||||||
"axios": "^1.13.2",
|
"axios": "^1.13.2",
|
||||||
|
"bcryptjs": "^3.0.3",
|
||||||
"bullmq": "^5.66.5",
|
"bullmq": "^5.66.5",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
|
|||||||
392
src/api/routes/auth.ts
Normal file
392
src/api/routes/auth.ts
Normal file
@@ -0,0 +1,392 @@
|
|||||||
|
/**
|
||||||
|
* Authentication Routes
|
||||||
|
* Using session-based auth with HTTPOnly cookies (Lucia Auth pattern)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { db } from '../../db/client'
|
||||||
|
import { users, sessions } from '../../db/schema'
|
||||||
|
import { eq } from 'drizzle-orm'
|
||||||
|
import { randomUUID } from 'crypto'
|
||||||
|
import bcrypt from 'bcryptjs'
|
||||||
|
|
||||||
|
// Session duration: 7 days
|
||||||
|
const SESSION_DURATION_MS = 7 * 24 * 60 * 60 * 1000
|
||||||
|
|
||||||
|
// Cookie options
|
||||||
|
const COOKIE_OPTIONS = {
|
||||||
|
httpOnly: true,
|
||||||
|
secure: process.env.NODE_ENV === 'production',
|
||||||
|
sameSite: 'lax' as const,
|
||||||
|
path: '/',
|
||||||
|
maxAge: SESSION_DURATION_MS / 1000, // in seconds
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hash password using bcrypt
|
||||||
|
*/
|
||||||
|
async function hashPassword(password: string): Promise<string> {
|
||||||
|
return await bcrypt.hash(password, 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify password against hash
|
||||||
|
*/
|
||||||
|
async function verifyPassword(password: string, hash: string): Promise<boolean> {
|
||||||
|
return await bcrypt.compare(password, hash)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate random session ID
|
||||||
|
*/
|
||||||
|
function generateSessionId(): string {
|
||||||
|
return randomUUID().replace(/-/g, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create session in database and return session cookie header
|
||||||
|
*/
|
||||||
|
async function createSession(userId: string): Promise<{ sessionId: string; cookieHeader: string }> {
|
||||||
|
const sessionId = generateSessionId()
|
||||||
|
const expiresAt = new Date(Date.now() + SESSION_DURATION_MS)
|
||||||
|
|
||||||
|
await db.insert(sessions).values({
|
||||||
|
id: sessionId,
|
||||||
|
userId,
|
||||||
|
expiresAt,
|
||||||
|
})
|
||||||
|
|
||||||
|
const cookieHeader = serializeCookie('session', sessionId, COOKIE_OPTIONS)
|
||||||
|
|
||||||
|
return { sessionId, cookieHeader }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate session from cookie and return user
|
||||||
|
*/
|
||||||
|
async function validateSession(sessionId: string) {
|
||||||
|
const [session] = await db
|
||||||
|
.select()
|
||||||
|
.from(sessions)
|
||||||
|
.where(eq(sessions.id, sessionId))
|
||||||
|
.limit(1)
|
||||||
|
|
||||||
|
if (!session) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if session expired
|
||||||
|
if (session.expiresAt < new Date()) {
|
||||||
|
await db.delete(sessions).where(eq(sessions.id, sessionId))
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get user
|
||||||
|
const [user] = await db
|
||||||
|
.select({
|
||||||
|
id: users.id,
|
||||||
|
email: users.email,
|
||||||
|
username: users.username,
|
||||||
|
createdAt: users.createdAt,
|
||||||
|
})
|
||||||
|
.from(users)
|
||||||
|
.where(eq(users.id, session.userId))
|
||||||
|
.limit(1)
|
||||||
|
|
||||||
|
return user || null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete session from database
|
||||||
|
*/
|
||||||
|
async function deleteSession(sessionId: string) {
|
||||||
|
await db.delete(sessions).where(eq(sessions.id, sessionId))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serialize cookie header
|
||||||
|
*/
|
||||||
|
function serializeCookie(
|
||||||
|
name: string,
|
||||||
|
value: string,
|
||||||
|
options: typeof COOKIE_OPTIONS
|
||||||
|
): string {
|
||||||
|
const parts = [`${name}=${value}`]
|
||||||
|
|
||||||
|
if (options.httpOnly) parts.push('HttpOnly')
|
||||||
|
if (options.secure) parts.push('Secure')
|
||||||
|
if (options.sameSite) parts.push(`SameSite=${options.sameSite}`)
|
||||||
|
if (options.path) parts.push(`Path=${options.path}`)
|
||||||
|
if (options.maxAge) parts.push(`Max-Age=${options.maxAge}`)
|
||||||
|
|
||||||
|
return parts.join('; ')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse cookies from request header
|
||||||
|
*/
|
||||||
|
function parseCookies(cookieHeader: string | null): Record<string, string> {
|
||||||
|
if (!cookieHeader) return {}
|
||||||
|
|
||||||
|
return cookieHeader.split(';').reduce((cookies, cookie) => {
|
||||||
|
const [name, value] = cookie.trim().split('=')
|
||||||
|
if (name && value) {
|
||||||
|
cookies[name] = value
|
||||||
|
}
|
||||||
|
return cookies
|
||||||
|
}, {} as Record<string, string>)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get session ID from request cookies
|
||||||
|
*/
|
||||||
|
function getSessionId(req: Request): string | null {
|
||||||
|
const cookieHeader = req.headers.get('cookie')
|
||||||
|
const cookies = parseCookies(cookieHeader)
|
||||||
|
return cookies.session || null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/auth/register
|
||||||
|
* Create new user account
|
||||||
|
*/
|
||||||
|
async function handleRegister(req: Request): Promise<Response> {
|
||||||
|
try {
|
||||||
|
const body = await req.json()
|
||||||
|
const { email, username, password } = body
|
||||||
|
|
||||||
|
// Validation
|
||||||
|
if (!email || !username || !password) {
|
||||||
|
return Response.json(
|
||||||
|
{ success: false, message: 'Email, username, and password are required' },
|
||||||
|
{ status: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password.length < 8) {
|
||||||
|
return Response.json(
|
||||||
|
{ success: false, message: 'Password must be at least 8 characters' },
|
||||||
|
{ status: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user already exists
|
||||||
|
const [existingUser] = await db
|
||||||
|
.select()
|
||||||
|
.from(users)
|
||||||
|
.where(eq(users.email, email))
|
||||||
|
.limit(1)
|
||||||
|
|
||||||
|
if (existingUser) {
|
||||||
|
return Response.json(
|
||||||
|
{ success: false, message: 'User with this email already exists' },
|
||||||
|
{ status: 409 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create user
|
||||||
|
const userId = randomUUID()
|
||||||
|
const passwordHash = await hashPassword(password)
|
||||||
|
|
||||||
|
await db.insert(users).values({
|
||||||
|
id: userId,
|
||||||
|
email,
|
||||||
|
username,
|
||||||
|
passwordHash,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create session
|
||||||
|
const { cookieHeader } = await createSession(userId)
|
||||||
|
|
||||||
|
// Return user (without password hash)
|
||||||
|
const [user] = await db
|
||||||
|
.select({
|
||||||
|
id: users.id,
|
||||||
|
email: users.email,
|
||||||
|
username: users.username,
|
||||||
|
createdAt: users.createdAt,
|
||||||
|
})
|
||||||
|
.from(users)
|
||||||
|
.where(eq(users.id, userId))
|
||||||
|
.limit(1)
|
||||||
|
|
||||||
|
return Response.json(
|
||||||
|
{
|
||||||
|
success: true,
|
||||||
|
data: { user },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: 201,
|
||||||
|
headers: {
|
||||||
|
'Set-Cookie': cookieHeader,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Registration error:', error)
|
||||||
|
return Response.json(
|
||||||
|
{ success: false, message: 'Registration failed' },
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/auth/login
|
||||||
|
* Authenticate user and create session
|
||||||
|
*/
|
||||||
|
async function handleLogin(req: Request): Promise<Response> {
|
||||||
|
try {
|
||||||
|
const body = await req.json()
|
||||||
|
const { email, password } = body
|
||||||
|
|
||||||
|
if (!email || !password) {
|
||||||
|
return Response.json(
|
||||||
|
{ success: false, message: 'Email and password are required' },
|
||||||
|
{ status: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get user
|
||||||
|
const [user] = await db
|
||||||
|
.select()
|
||||||
|
.from(users)
|
||||||
|
.where(eq(users.email, email))
|
||||||
|
.limit(1)
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return Response.json(
|
||||||
|
{ success: false, message: 'Invalid email or password' },
|
||||||
|
{ status: 401 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify password
|
||||||
|
const valid = await verifyPassword(password, user.passwordHash)
|
||||||
|
|
||||||
|
if (!valid) {
|
||||||
|
return Response.json(
|
||||||
|
{ success: false, message: 'Invalid email or password' },
|
||||||
|
{ status: 401 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create session
|
||||||
|
const { cookieHeader } = await createSession(user.id)
|
||||||
|
|
||||||
|
return Response.json(
|
||||||
|
{
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
user: {
|
||||||
|
id: user.id,
|
||||||
|
email: user.email,
|
||||||
|
username: user.username,
|
||||||
|
createdAt: user.createdAt,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Set-Cookie': cookieHeader,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Login error:', error)
|
||||||
|
return Response.json({ success: false, message: 'Login failed' }, { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/auth/logout
|
||||||
|
* Delete session
|
||||||
|
*/
|
||||||
|
async function handleLogout(req: Request): Promise<Response> {
|
||||||
|
try {
|
||||||
|
const sessionId = getSessionId(req)
|
||||||
|
|
||||||
|
if (sessionId) {
|
||||||
|
await deleteSession(sessionId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear cookie
|
||||||
|
const clearCookieHeader = serializeCookie('session', '', {
|
||||||
|
...COOKIE_OPTIONS,
|
||||||
|
maxAge: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
return Response.json(
|
||||||
|
{ success: true, message: 'Logged out successfully' },
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Set-Cookie': clearCookieHeader,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Logout error:', error)
|
||||||
|
return Response.json({ success: false, message: 'Logout failed' }, { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/auth/me
|
||||||
|
* Get current user from session
|
||||||
|
*/
|
||||||
|
async function handleMe(req: Request): Promise<Response> {
|
||||||
|
try {
|
||||||
|
const sessionId = getSessionId(req)
|
||||||
|
|
||||||
|
if (!sessionId) {
|
||||||
|
return Response.json(
|
||||||
|
{ success: false, message: 'Not authenticated' },
|
||||||
|
{ status: 401 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await validateSession(sessionId)
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return Response.json(
|
||||||
|
{ success: false, message: 'Invalid or expired session' },
|
||||||
|
{ status: 401 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response.json({
|
||||||
|
success: true,
|
||||||
|
data: user,
|
||||||
|
})
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Get current user error:', error)
|
||||||
|
return Response.json(
|
||||||
|
{ success: false, message: 'Failed to get current user' },
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Route handler for /api/auth/*
|
||||||
|
*/
|
||||||
|
export async function handleAuthRoutes(req: Request, url: URL): Promise<Response> {
|
||||||
|
const path = url.pathname.replace('/api/auth', '')
|
||||||
|
|
||||||
|
if (path === '/register' && req.method === 'POST') {
|
||||||
|
return handleRegister(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path === '/login' && req.method === 'POST') {
|
||||||
|
return handleLogin(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path === '/logout' && req.method === 'POST') {
|
||||||
|
return handleLogout(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path === '/me' && req.method === 'GET') {
|
||||||
|
return handleMe(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response.json({ message: 'Not found' }, { status: 404 })
|
||||||
|
}
|
||||||
@@ -3,6 +3,8 @@
|
|||||||
* Exports all route handlers
|
* Exports all route handlers
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
export { handleAuthRoutes } from './auth'
|
||||||
export { handleProjectRoutes } from './projects'
|
export { handleProjectRoutes } from './projects'
|
||||||
export { handleTaskRoutes } from './tasks'
|
export { handleTaskRoutes } from './tasks'
|
||||||
export { handleAgentRoutes } from './agents'
|
export { handleAgentRoutes } from './agents'
|
||||||
|
export { handleMCPRoutes } from './mcp'
|
||||||
|
|||||||
362
src/api/routes/mcp.ts
Normal file
362
src/api/routes/mcp.ts
Normal file
@@ -0,0 +1,362 @@
|
|||||||
|
/**
|
||||||
|
* MCP HTTP API for Agent Communication
|
||||||
|
* Provides MCP tools via HTTP POST endpoints
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { db } from '../../db/client'
|
||||||
|
import { tasks, agents, projects } from '../../db/schema'
|
||||||
|
import { eq, and, or } from 'drizzle-orm'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Route handler for /api/mcp/*
|
||||||
|
*/
|
||||||
|
export async function handleMCPRoutes(req: Request, url: URL): Promise<Response> {
|
||||||
|
const path = url.pathname.replace('/api/mcp', '')
|
||||||
|
|
||||||
|
// POST /api/mcp/get_next_task
|
||||||
|
if (path === '/get_next_task' && req.method === 'POST') {
|
||||||
|
return await handleGetNextTask(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST /api/mcp/update_task_status
|
||||||
|
if (path === '/update_task_status' && req.method === 'POST') {
|
||||||
|
return await handleUpdateTaskStatus(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST /api/mcp/create_branch
|
||||||
|
if (path === '/create_branch' && req.method === 'POST') {
|
||||||
|
return await handleCreateBranch(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST /api/mcp/create_pull_request
|
||||||
|
if (path === '/create_pull_request' && req.method === 'POST') {
|
||||||
|
return await handleCreatePullRequest(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST /api/mcp/ask_user_question
|
||||||
|
if (path === '/ask_user_question' && req.method === 'POST') {
|
||||||
|
return await handleAskUserQuestion(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /api/mcp/tools - List available tools
|
||||||
|
if (path === '/tools' && req.method === 'GET') {
|
||||||
|
return Response.json({
|
||||||
|
success: true,
|
||||||
|
tools: [
|
||||||
|
{
|
||||||
|
name: 'get_next_task',
|
||||||
|
description: 'Get the next available task from the backlog',
|
||||||
|
endpoint: '/api/mcp/get_next_task',
|
||||||
|
method: 'POST',
|
||||||
|
params: {
|
||||||
|
agentId: 'string (required)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'update_task_status',
|
||||||
|
description: 'Update the status of a task',
|
||||||
|
endpoint: '/api/mcp/update_task_status',
|
||||||
|
method: 'POST',
|
||||||
|
params: {
|
||||||
|
taskId: 'string (required)',
|
||||||
|
status: 'string (required)',
|
||||||
|
errorMessage: 'string (optional)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'create_branch',
|
||||||
|
description: 'Create a Git branch for a task',
|
||||||
|
endpoint: '/api/mcp/create_branch',
|
||||||
|
method: 'POST',
|
||||||
|
params: {
|
||||||
|
taskId: 'string (required)',
|
||||||
|
branchName: 'string (required)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'create_pull_request',
|
||||||
|
description: 'Create a pull request in Gitea',
|
||||||
|
endpoint: '/api/mcp/create_pull_request',
|
||||||
|
method: 'POST',
|
||||||
|
params: {
|
||||||
|
taskId: 'string (required)',
|
||||||
|
title: 'string (required)',
|
||||||
|
description: 'string (optional)',
|
||||||
|
branchName: 'string (required)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'ask_user_question',
|
||||||
|
description: 'Ask the user a question',
|
||||||
|
endpoint: '/api/mcp/ask_user_question',
|
||||||
|
method: 'POST',
|
||||||
|
params: {
|
||||||
|
taskId: 'string (required)',
|
||||||
|
question: 'string (required)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response.json({ message: 'Not found' }, { status: 404 })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get next available task
|
||||||
|
*/
|
||||||
|
async function handleGetNextTask(req: Request): Promise<Response> {
|
||||||
|
try {
|
||||||
|
const body = await req.json()
|
||||||
|
const { agentId } = body
|
||||||
|
|
||||||
|
if (!agentId) {
|
||||||
|
return Response.json(
|
||||||
|
{ success: false, message: 'agentId is required' },
|
||||||
|
{ status: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get next task in backlog, ordered by priority
|
||||||
|
const [task] = await db
|
||||||
|
.select()
|
||||||
|
.from(tasks)
|
||||||
|
.where(eq(tasks.state, 'backlog'))
|
||||||
|
.orderBy(tasks.priority, tasks.createdAt)
|
||||||
|
.limit(1)
|
||||||
|
|
||||||
|
if (!task) {
|
||||||
|
return Response.json({
|
||||||
|
success: true,
|
||||||
|
message: 'No tasks available in backlog',
|
||||||
|
data: null,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get project info
|
||||||
|
const [project] = await db
|
||||||
|
.select()
|
||||||
|
.from(projects)
|
||||||
|
.where(eq(projects.id, task.projectId))
|
||||||
|
.limit(1)
|
||||||
|
|
||||||
|
// Assign task to agent
|
||||||
|
await db
|
||||||
|
.update(tasks)
|
||||||
|
.set({
|
||||||
|
state: 'in_progress',
|
||||||
|
assignedAgentId: agentId,
|
||||||
|
})
|
||||||
|
.where(eq(tasks.id, task.id))
|
||||||
|
|
||||||
|
// Update agent status
|
||||||
|
await db
|
||||||
|
.update(agents)
|
||||||
|
.set({
|
||||||
|
status: 'busy',
|
||||||
|
currentTaskId: task.id,
|
||||||
|
})
|
||||||
|
.where(eq(agents.id, agentId))
|
||||||
|
|
||||||
|
return Response.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
task,
|
||||||
|
project,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('get_next_task error:', error)
|
||||||
|
return Response.json(
|
||||||
|
{ success: false, message: error.message },
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update task status
|
||||||
|
*/
|
||||||
|
async function handleUpdateTaskStatus(req: Request): Promise<Response> {
|
||||||
|
try {
|
||||||
|
const body = await req.json()
|
||||||
|
const { taskId, status, errorMessage } = body
|
||||||
|
|
||||||
|
if (!taskId || !status) {
|
||||||
|
return Response.json(
|
||||||
|
{ success: false, message: 'taskId and status are required' },
|
||||||
|
{ status: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const validStatuses = [
|
||||||
|
'backlog',
|
||||||
|
'in_progress',
|
||||||
|
'needs_input',
|
||||||
|
'ready_to_test',
|
||||||
|
'approved',
|
||||||
|
'staging',
|
||||||
|
'production',
|
||||||
|
]
|
||||||
|
|
||||||
|
if (!validStatuses.includes(status)) {
|
||||||
|
return Response.json(
|
||||||
|
{ success: false, message: 'Invalid status' },
|
||||||
|
{ status: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
await db
|
||||||
|
.update(tasks)
|
||||||
|
.set({
|
||||||
|
state: status,
|
||||||
|
...(errorMessage && { errorMessage }),
|
||||||
|
})
|
||||||
|
.where(eq(tasks.id, taskId))
|
||||||
|
|
||||||
|
// If task is completed, update agent
|
||||||
|
if (['ready_to_test', 'approved'].includes(status)) {
|
||||||
|
const [task] = await db
|
||||||
|
.select()
|
||||||
|
.from(tasks)
|
||||||
|
.where(eq(tasks.id, taskId))
|
||||||
|
.limit(1)
|
||||||
|
|
||||||
|
if (task?.assignedAgentId) {
|
||||||
|
await db
|
||||||
|
.update(agents)
|
||||||
|
.set({
|
||||||
|
status: 'idle',
|
||||||
|
currentTaskId: null,
|
||||||
|
tasksCompleted: db.sql`${agents.tasksCompleted} + 1`,
|
||||||
|
})
|
||||||
|
.where(eq(agents.id, task.assignedAgentId))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response.json({
|
||||||
|
success: true,
|
||||||
|
message: `Task ${taskId} updated to ${status}`,
|
||||||
|
})
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('update_task_status error:', error)
|
||||||
|
return Response.json(
|
||||||
|
{ success: false, message: error.message },
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create branch
|
||||||
|
*/
|
||||||
|
async function handleCreateBranch(req: Request): Promise<Response> {
|
||||||
|
try {
|
||||||
|
const body = await req.json()
|
||||||
|
const { taskId, branchName } = body
|
||||||
|
|
||||||
|
if (!taskId || !branchName) {
|
||||||
|
return Response.json(
|
||||||
|
{ success: false, message: 'taskId and branchName are required' },
|
||||||
|
{ status: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
await db
|
||||||
|
.update(tasks)
|
||||||
|
.set({
|
||||||
|
branchName,
|
||||||
|
})
|
||||||
|
.where(eq(tasks.id, taskId))
|
||||||
|
|
||||||
|
return Response.json({
|
||||||
|
success: true,
|
||||||
|
message: `Branch ${branchName} created for task ${taskId}`,
|
||||||
|
})
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('create_branch error:', error)
|
||||||
|
return Response.json(
|
||||||
|
{ success: false, message: error.message },
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create pull request
|
||||||
|
*/
|
||||||
|
async function handleCreatePullRequest(req: Request): Promise<Response> {
|
||||||
|
try {
|
||||||
|
const body = await req.json()
|
||||||
|
const { taskId, title, description, branchName } = body
|
||||||
|
|
||||||
|
if (!taskId || !title || !branchName) {
|
||||||
|
return Response.json(
|
||||||
|
{ success: false, message: 'taskId, title, and branchName are required' },
|
||||||
|
{ status: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Integrate with Gitea API to actually create PR
|
||||||
|
// For now, just update task with placeholder PR URL
|
||||||
|
const prUrl = `https://git.fuq.tv/pulls/${taskId}`
|
||||||
|
|
||||||
|
await db
|
||||||
|
.update(tasks)
|
||||||
|
.set({
|
||||||
|
prUrl,
|
||||||
|
state: 'ready_to_test',
|
||||||
|
})
|
||||||
|
.where(eq(tasks.id, taskId))
|
||||||
|
|
||||||
|
return Response.json({
|
||||||
|
success: true,
|
||||||
|
message: 'Pull request created',
|
||||||
|
data: {
|
||||||
|
prUrl,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('create_pull_request error:', error)
|
||||||
|
return Response.json(
|
||||||
|
{ success: false, message: error.message },
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ask user question
|
||||||
|
*/
|
||||||
|
async function handleAskUserQuestion(req: Request): Promise<Response> {
|
||||||
|
try {
|
||||||
|
const body = await req.json()
|
||||||
|
const { taskId, question } = body
|
||||||
|
|
||||||
|
if (!taskId || !question) {
|
||||||
|
return Response.json(
|
||||||
|
{ success: false, message: 'taskId and question are required' },
|
||||||
|
{ status: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
await db
|
||||||
|
.update(tasks)
|
||||||
|
.set({
|
||||||
|
state: 'needs_input',
|
||||||
|
errorMessage: question,
|
||||||
|
})
|
||||||
|
.where(eq(tasks.id, taskId))
|
||||||
|
|
||||||
|
return Response.json({
|
||||||
|
success: true,
|
||||||
|
message: 'Question saved. Task marked as needs_input.',
|
||||||
|
})
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('ask_user_question error:', error)
|
||||||
|
return Response.json(
|
||||||
|
{ success: false, message: error.message },
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
60
src/db/apply-migrations.ts
Normal file
60
src/db/apply-migrations.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
/**
|
||||||
|
* Apply pending migrations directly
|
||||||
|
*/
|
||||||
|
|
||||||
|
import mysql from 'mysql2/promise'
|
||||||
|
import { readFileSync } from 'fs'
|
||||||
|
import { join } from 'path'
|
||||||
|
|
||||||
|
async function applyMigrations() {
|
||||||
|
console.log('📝 Applying pending migrations...')
|
||||||
|
|
||||||
|
const connection = await mysql.createConnection({
|
||||||
|
host: process.env.DB_HOST,
|
||||||
|
port: parseInt(process.env.DB_PORT || '3306'),
|
||||||
|
user: process.env.DB_USER,
|
||||||
|
password: process.env.DB_PASSWORD,
|
||||||
|
database: process.env.DB_NAME,
|
||||||
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Read the latest migration file
|
||||||
|
const migrationPath = join(import.meta.dir, '../../drizzle/migrations/0001_opposite_warbird.sql')
|
||||||
|
const migrationSQL = readFileSync(migrationPath, 'utf-8')
|
||||||
|
|
||||||
|
// Split by statement-breakpoint and execute each statement
|
||||||
|
const statements = migrationSQL
|
||||||
|
.split('-->')
|
||||||
|
.map((s) => s.trim())
|
||||||
|
.filter((s) => s && !s.startsWith('statement-breakpoint'))
|
||||||
|
|
||||||
|
console.log(`Found ${statements.length} statements to execute`)
|
||||||
|
|
||||||
|
for (const statement of statements) {
|
||||||
|
if (statement) {
|
||||||
|
try {
|
||||||
|
await connection.execute(statement)
|
||||||
|
console.log('✅ Executed statement')
|
||||||
|
} catch (error: any) {
|
||||||
|
// Skip if table already exists
|
||||||
|
if (error.code === 'ER_TABLE_EXISTS_ERROR' || error.errno === 1050) {
|
||||||
|
console.log('⚠️ Table already exists, skipping...')
|
||||||
|
} else if (error.code === 'ER_DUP_KEYNAME' || error.errno === 1061) {
|
||||||
|
console.log('⚠️ Index already exists, skipping...')
|
||||||
|
} else {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('✅ Migrations applied successfully')
|
||||||
|
await connection.end()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Migration failed:', error)
|
||||||
|
await connection.end()
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await applyMigrations()
|
||||||
76
src/db/force-migrate.ts
Normal file
76
src/db/force-migrate.ts
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
/**
|
||||||
|
* Force apply all migration statements
|
||||||
|
*/
|
||||||
|
|
||||||
|
import mysql from 'mysql2/promise'
|
||||||
|
|
||||||
|
const statements = [
|
||||||
|
`CREATE TABLE IF NOT EXISTS \`sessions\` (
|
||||||
|
\`id\` varchar(255) NOT NULL,
|
||||||
|
\`user_id\` varchar(36) NOT NULL,
|
||||||
|
\`expires_at\` timestamp NOT NULL,
|
||||||
|
\`created_at\` timestamp DEFAULT (now()),
|
||||||
|
CONSTRAINT \`sessions_id\` PRIMARY KEY(\`id\`)
|
||||||
|
)`,
|
||||||
|
`CREATE TABLE IF NOT EXISTS \`users\` (
|
||||||
|
\`id\` varchar(36) NOT NULL,
|
||||||
|
\`email\` varchar(255) NOT NULL,
|
||||||
|
\`username\` varchar(100) NOT NULL,
|
||||||
|
\`password_hash\` varchar(255) NOT NULL,
|
||||||
|
\`created_at\` timestamp DEFAULT (now()),
|
||||||
|
\`updated_at\` timestamp DEFAULT (now()) ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
CONSTRAINT \`users_id\` PRIMARY KEY(\`id\`),
|
||||||
|
CONSTRAINT \`users_email_unique\` UNIQUE(\`email\`),
|
||||||
|
CONSTRAINT \`users_username_unique\` UNIQUE(\`username\`)
|
||||||
|
)`,
|
||||||
|
`ALTER TABLE \`sessions\`
|
||||||
|
ADD CONSTRAINT \`sessions_user_id_users_id_fk\`
|
||||||
|
FOREIGN KEY (\`user_id\`) REFERENCES \`users\`(\`id\`)
|
||||||
|
ON DELETE cascade ON UPDATE no action`,
|
||||||
|
`CREATE INDEX IF NOT EXISTS \`idx_user_id\` ON \`sessions\` (\`user_id\`)`,
|
||||||
|
`CREATE INDEX IF NOT EXISTS \`idx_expires_at\` ON \`sessions\` (\`expires_at\`)`,
|
||||||
|
`CREATE INDEX IF NOT EXISTS \`idx_email\` ON \`users\` (\`email\`)`,
|
||||||
|
]
|
||||||
|
|
||||||
|
async function forceMigrate() {
|
||||||
|
console.log('🔧 Force migrating database...')
|
||||||
|
|
||||||
|
const connection = await mysql.createConnection({
|
||||||
|
host: process.env.DB_HOST,
|
||||||
|
port: parseInt(process.env.DB_PORT || '3306'),
|
||||||
|
user: process.env.DB_USER,
|
||||||
|
password: process.env.DB_PASSWORD,
|
||||||
|
database: process.env.DB_NAME,
|
||||||
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (const [index, statement] of statements.entries()) {
|
||||||
|
console.log(`Executing statement ${index + 1}/${statements.length}...`)
|
||||||
|
try {
|
||||||
|
await connection.execute(statement)
|
||||||
|
console.log('✅ Success')
|
||||||
|
} catch (error: any) {
|
||||||
|
if (
|
||||||
|
error.code === 'ER_TABLE_EXISTS_ERROR' ||
|
||||||
|
error.code === 'ER_DUP_KEYNAME' ||
|
||||||
|
error.errno === 1050 ||
|
||||||
|
error.errno === 1061
|
||||||
|
) {
|
||||||
|
console.log('⚠️ Already exists, skipping...')
|
||||||
|
} else {
|
||||||
|
console.error('Error:', error.message)
|
||||||
|
// Continue with other statements
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('✅ All migrations applied')
|
||||||
|
await connection.end()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Migration failed:', error)
|
||||||
|
await connection.end()
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await forceMigrate()
|
||||||
@@ -17,6 +17,39 @@ import {
|
|||||||
index,
|
index,
|
||||||
} from 'drizzle-orm/mysql-core'
|
} from 'drizzle-orm/mysql-core'
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// USERS TABLE
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
export const users = mysqlTable('users', {
|
||||||
|
id: varchar('id', { length: 36 }).primaryKey(),
|
||||||
|
email: varchar('email', { length: 255 }).notNull().unique(),
|
||||||
|
username: varchar('username', { length: 100 }).notNull().unique(),
|
||||||
|
passwordHash: varchar('password_hash', { length: 255 }).notNull(),
|
||||||
|
|
||||||
|
// Timestamps
|
||||||
|
createdAt: timestamp('created_at').defaultNow(),
|
||||||
|
updatedAt: timestamp('updated_at').defaultNow().onUpdateNow(),
|
||||||
|
}, (table) => ({
|
||||||
|
emailIdx: index('idx_email').on(table.email),
|
||||||
|
}))
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// SESSIONS TABLE
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
export const sessions = mysqlTable('sessions', {
|
||||||
|
id: varchar('id', { length: 255 }).primaryKey(),
|
||||||
|
userId: varchar('user_id', { length: 36 }).notNull().references(() => users.id, { onDelete: 'cascade' }),
|
||||||
|
expiresAt: timestamp('expires_at').notNull(),
|
||||||
|
|
||||||
|
// Timestamps
|
||||||
|
createdAt: timestamp('created_at').defaultNow(),
|
||||||
|
}, (table) => ({
|
||||||
|
userIdIdx: index('idx_user_id').on(table.userId),
|
||||||
|
expiresAtIdx: index('idx_expires_at').on(table.expiresAt),
|
||||||
|
}))
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// PROJECTS TABLE
|
// PROJECTS TABLE
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|||||||
16
src/index.ts
16
src/index.ts
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
import { runMigrations } from './db/migrate'
|
import { runMigrations } from './db/migrate'
|
||||||
import { testConnection } from './db/client'
|
import { testConnection } from './db/client'
|
||||||
import { handleProjectRoutes, handleTaskRoutes, handleAgentRoutes } from './api/routes'
|
import { handleAuthRoutes, handleProjectRoutes, handleTaskRoutes, handleAgentRoutes, handleMCPRoutes } from './api/routes'
|
||||||
|
|
||||||
console.log('🚀 Starting AiWorker Backend...')
|
console.log('🚀 Starting AiWorker Backend...')
|
||||||
console.log(`Bun version: ${Bun.version}`)
|
console.log(`Bun version: ${Bun.version}`)
|
||||||
@@ -41,6 +41,11 @@ const server = Bun.serve({
|
|||||||
return handleHealthCheck()
|
return handleHealthCheck()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Auth routes
|
||||||
|
if (url.pathname.startsWith('/api/auth')) {
|
||||||
|
return handleAuthRoutes(req, url)
|
||||||
|
}
|
||||||
|
|
||||||
// API routes
|
// API routes
|
||||||
if (url.pathname.startsWith('/api/projects')) {
|
if (url.pathname.startsWith('/api/projects')) {
|
||||||
return handleProjectRoutes(req, url)
|
return handleProjectRoutes(req, url)
|
||||||
@@ -54,6 +59,11 @@ const server = Bun.serve({
|
|||||||
return handleAgentRoutes(req, url)
|
return handleAgentRoutes(req, url)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MCP routes for agent communication
|
||||||
|
if (url.pathname.startsWith('/api/mcp')) {
|
||||||
|
return handleMCPRoutes(req, url)
|
||||||
|
}
|
||||||
|
|
||||||
// Generic API info
|
// Generic API info
|
||||||
if (url.pathname.startsWith('/api/')) {
|
if (url.pathname.startsWith('/api/')) {
|
||||||
return Response.json({
|
return Response.json({
|
||||||
@@ -61,6 +71,10 @@ const server = Bun.serve({
|
|||||||
version: '1.0.0',
|
version: '1.0.0',
|
||||||
endpoints: [
|
endpoints: [
|
||||||
'GET /api/health',
|
'GET /api/health',
|
||||||
|
'POST /api/auth/register',
|
||||||
|
'POST /api/auth/login',
|
||||||
|
'POST /api/auth/logout',
|
||||||
|
'GET /api/auth/me',
|
||||||
'GET /api/projects',
|
'GET /api/projects',
|
||||||
'GET /api/projects/:id',
|
'GET /api/projects/:id',
|
||||||
'POST /api/projects',
|
'POST /api/projects',
|
||||||
|
|||||||
327
src/mcp/server.ts
Normal file
327
src/mcp/server.ts
Normal file
@@ -0,0 +1,327 @@
|
|||||||
|
/**
|
||||||
|
* MCP Server for Agent Communication
|
||||||
|
* Port 3100
|
||||||
|
*
|
||||||
|
* Provides tools for Claude Code agents:
|
||||||
|
* - get_next_task: Get next available task
|
||||||
|
* - update_task_status: Update task state
|
||||||
|
* - create_branch: Create Git branch for task
|
||||||
|
* - create_pull_request: Create PR in Gitea
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
|
||||||
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
||||||
|
import {
|
||||||
|
CallToolRequestSchema,
|
||||||
|
ListToolsRequestSchema,
|
||||||
|
ToolSchema,
|
||||||
|
} from '@modelcontextprotocol/sdk/types.js'
|
||||||
|
import { db } from '../db/client'
|
||||||
|
import { tasks, agents } from '../db/schema'
|
||||||
|
import { eq, and } from 'drizzle-orm'
|
||||||
|
|
||||||
|
// MCP Server instance
|
||||||
|
const server = new Server(
|
||||||
|
{
|
||||||
|
name: 'aiworker-mcp-server',
|
||||||
|
version: '1.0.0',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
capabilities: {
|
||||||
|
tools: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List available tools
|
||||||
|
*/
|
||||||
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||||
|
return {
|
||||||
|
tools: [
|
||||||
|
{
|
||||||
|
name: 'get_next_task',
|
||||||
|
description: 'Get the next available task from the backlog',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object' as const,
|
||||||
|
properties: {
|
||||||
|
agentId: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'ID of the agent requesting the task',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['agentId'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'update_task_status',
|
||||||
|
description: 'Update the status of a task',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object' as const,
|
||||||
|
properties: {
|
||||||
|
taskId: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'ID of the task to update',
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
type: 'string',
|
||||||
|
enum: ['backlog', 'in_progress', 'needs_input', 'ready_to_test', 'approved', 'staging', 'production'],
|
||||||
|
description: 'New status for the task',
|
||||||
|
},
|
||||||
|
errorMessage: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Optional error message if task failed',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['taskId', 'status'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'create_branch',
|
||||||
|
description: 'Create a Git branch for a task',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object' as const,
|
||||||
|
properties: {
|
||||||
|
taskId: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'ID of the task',
|
||||||
|
},
|
||||||
|
branchName: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Name of the branch to create',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['taskId', 'branchName'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'create_pull_request',
|
||||||
|
description: 'Create a pull request in Gitea for a task',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object' as const,
|
||||||
|
properties: {
|
||||||
|
taskId: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'ID of the task',
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'PR title',
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'PR description',
|
||||||
|
},
|
||||||
|
branchName: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Source branch name',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['taskId', 'title', 'branchName'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'ask_user_question',
|
||||||
|
description: 'Ask the user a question when clarification is needed',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object' as const,
|
||||||
|
properties: {
|
||||||
|
taskId: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'ID of the task',
|
||||||
|
},
|
||||||
|
question: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Question to ask the user',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['taskId', 'question'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle tool calls
|
||||||
|
*/
|
||||||
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||||
|
const { name, arguments: args } = request.params
|
||||||
|
|
||||||
|
try {
|
||||||
|
switch (name) {
|
||||||
|
case 'get_next_task':
|
||||||
|
return await handleGetNextTask(args as any)
|
||||||
|
|
||||||
|
case 'update_task_status':
|
||||||
|
return await handleUpdateTaskStatus(args as any)
|
||||||
|
|
||||||
|
case 'create_branch':
|
||||||
|
return await handleCreateBranch(args as any)
|
||||||
|
|
||||||
|
case 'create_pull_request':
|
||||||
|
return await handleCreatePullRequest(args as any)
|
||||||
|
|
||||||
|
case 'ask_user_question':
|
||||||
|
return await handleAskUserQuestion(args as any)
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown tool: ${name}`)
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text' as const,
|
||||||
|
text: `Error: ${error.message}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
isError: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get next available task
|
||||||
|
*/
|
||||||
|
async function handleGetNextTask(args: { agentId: string }) {
|
||||||
|
const [task] = await db
|
||||||
|
.select()
|
||||||
|
.from(tasks)
|
||||||
|
.where(eq(tasks.state, 'backlog'))
|
||||||
|
.orderBy(tasks.priority, tasks.createdAt)
|
||||||
|
.limit(1)
|
||||||
|
|
||||||
|
if (!task) {
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text' as const,
|
||||||
|
text: 'No tasks available in backlog',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assign task to agent
|
||||||
|
await db.update(tasks).set({
|
||||||
|
state: 'in_progress',
|
||||||
|
assignedAgentId: args.agentId,
|
||||||
|
}).where(eq(tasks.id, task.id))
|
||||||
|
|
||||||
|
// Update agent status
|
||||||
|
await db.update(agents).set({
|
||||||
|
status: 'busy',
|
||||||
|
currentTaskId: task.id,
|
||||||
|
}).where(eq(agents.id, args.agentId))
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text' as const,
|
||||||
|
text: JSON.stringify(task, null, 2),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update task status
|
||||||
|
*/
|
||||||
|
async function handleUpdateTaskStatus(args: {
|
||||||
|
taskId: string
|
||||||
|
status: string
|
||||||
|
errorMessage?: string
|
||||||
|
}) {
|
||||||
|
await db.update(tasks).set({
|
||||||
|
state: args.status as any,
|
||||||
|
...(args.errorMessage && { errorMessage: args.errorMessage }),
|
||||||
|
}).where(eq(tasks.id, args.taskId))
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text' as const,
|
||||||
|
text: `Task ${args.taskId} updated to ${args.status}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create branch
|
||||||
|
*/
|
||||||
|
async function handleCreateBranch(args: { taskId: string; branchName: string }) {
|
||||||
|
await db.update(tasks).set({
|
||||||
|
branchName: args.branchName,
|
||||||
|
}).where(eq(tasks.id, args.taskId))
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text' as const,
|
||||||
|
text: `Branch ${args.branchName} created for task ${args.taskId}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create pull request
|
||||||
|
*/
|
||||||
|
async function handleCreatePullRequest(args: {
|
||||||
|
taskId: string
|
||||||
|
title: string
|
||||||
|
description?: string
|
||||||
|
branchName: string
|
||||||
|
}) {
|
||||||
|
// TODO: Integrate with Gitea API to actually create PR
|
||||||
|
const prUrl = `https://git.fuq.tv/pulls/${args.taskId}`
|
||||||
|
|
||||||
|
await db.update(tasks).set({
|
||||||
|
prUrl,
|
||||||
|
state: 'ready_to_test',
|
||||||
|
}).where(eq(tasks.id, args.taskId))
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text' as const,
|
||||||
|
text: `Pull request created: ${prUrl}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ask user question
|
||||||
|
*/
|
||||||
|
async function handleAskUserQuestion(args: { taskId: string; question: string }) {
|
||||||
|
await db.update(tasks).set({
|
||||||
|
state: 'needs_input',
|
||||||
|
errorMessage: args.question,
|
||||||
|
}).where(eq(tasks.id, args.taskId))
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text' as const,
|
||||||
|
text: `Question saved for task ${args.taskId}. Task marked as needs_input.`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start MCP server
|
||||||
|
*/
|
||||||
|
async function main() {
|
||||||
|
const transport = new StdioServerTransport()
|
||||||
|
await server.connect(transport)
|
||||||
|
console.error('MCP server running on stdio')
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((error) => {
|
||||||
|
console.error('Fatal error in MCP server:', error)
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
37
test-auth.ts
Normal file
37
test-auth.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
/**
|
||||||
|
* Test authentication directly
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { db } from './src/db/client'
|
||||||
|
import { users } from './src/db/schema'
|
||||||
|
import { randomUUID } from 'crypto'
|
||||||
|
import bcrypt from 'bcryptjs'
|
||||||
|
|
||||||
|
async function testAuth() {
|
||||||
|
try {
|
||||||
|
console.log('Testing auth...')
|
||||||
|
|
||||||
|
// Hash password
|
||||||
|
const passwordHash = await bcrypt.hash('test123', 10)
|
||||||
|
console.log('Password hash:', passwordHash)
|
||||||
|
|
||||||
|
// Insert user
|
||||||
|
const userId = randomUUID()
|
||||||
|
console.log('Inserting user with ID:', userId)
|
||||||
|
|
||||||
|
await db.insert(users).values({
|
||||||
|
id: userId,
|
||||||
|
email: 'test@test.com',
|
||||||
|
username: 'testuser',
|
||||||
|
passwordHash,
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('User inserted successfully!')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error:', error)
|
||||||
|
}
|
||||||
|
|
||||||
|
process.exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
testAuth()
|
||||||
Reference in New Issue
Block a user